ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -0,0 +1,46 @@
1
+ const AXIS_COMPONENTS = Object.freeze(["Line", "Ticks", "Labels", "Title"]);
2
+
3
+ const CATEGORICAL_COMPONENTS = Object.freeze([
4
+ "Symbols", "SymbolLines", "SymbolPoints", "SymbolSwatches",
5
+ "Labels", "Title", "Background"
6
+ ]);
7
+
8
+ const LEGEND_GRAPHICS = Object.freeze({
9
+ series: Object.freeze(CATEGORICAL_COMPONENTS.map(
10
+ component => `seriesLegend${component}`
11
+ )),
12
+ color: Object.freeze(CATEGORICAL_COMPONENTS.map(
13
+ component => `colorLegend${component}`
14
+ )),
15
+ size: Object.freeze([
16
+ "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
17
+ ]),
18
+ gradient: Object.freeze([
19
+ "colorGradientBackground", "colorGradientStrips", "colorGradientTicks",
20
+ "colorGradientLabels", "colorGradientTitle"
21
+ ]),
22
+ interval: Object.freeze([
23
+ "colorLegendSymbols", "colorLegendLabels", "colorLegendTitle"
24
+ ]),
25
+ opacity: Object.freeze([
26
+ "opacityLegendBackground", "opacityLegendSymbols", "opacityLegendLabels",
27
+ "opacityLegendTitle"
28
+ ])
29
+ });
30
+
31
+ export function axisGraphicIds(channel) {
32
+ const prefix = channel === "radius" ? "radial" : channel;
33
+ return AXIS_COMPONENTS.map(component => `${prefix}Axis${component}`);
34
+ }
35
+
36
+ export function legendGraphicIds(kind) {
37
+ const ids = LEGEND_GRAPHICS[kind];
38
+ if (ids === undefined) {
39
+ throw new Error(`Unknown legend graphic kind "${kind}".`);
40
+ }
41
+ return ids;
42
+ }
43
+
44
+ export function allLegendGraphicIds(kinds) {
45
+ return [...new Set(kinds.flatMap(legendGraphicIds))];
46
+ }
@@ -1,23 +1,53 @@
1
1
  import { findDataset } from "../selectors/datasets.js";
2
+ import { findLayer } from "../selectors/layers.js";
2
3
  import { isAggregate } from "../grammar/aggregate.js";
3
- import { BAR_GRAINS, resolveBarGrain } from "../grammar/bars/policy.js";
4
+ import {
5
+ BAR_GRAINS,
6
+ resolveBarColorLayout,
7
+ resolveBarOffsetChannel,
8
+ resolveBarGrain
9
+ } from "../grammar/bars/policy.js";
4
10
  import { resolveRuleMode } from "../grammar/rules.js";
5
11
  import { findUpstreamTransform } from "./dataProvenance.js";
12
+ import { POSITION_CHANNELS } from "../core/vocabulary.js";
13
+ import { resolveRectMode } from "../grammar/rects.js";
6
14
 
7
- function hasPositionScales(layer) {
15
+ function hasCartesianPositionScales(layer) {
8
16
  return (
9
17
  layer.encoding?.x?.scale !== undefined &&
10
18
  layer.encoding?.y?.scale !== undefined
11
19
  );
12
20
  }
13
21
 
22
+ function hasPolarPositionScales(layer) {
23
+ return (
24
+ layer.encoding?.theta?.scale !== undefined &&
25
+ layer.encoding?.radius?.scale !== undefined
26
+ );
27
+ }
28
+
29
+ function hasPositionScales(layer) {
30
+ return hasCartesianPositionScales(layer);
31
+ }
32
+
14
33
  export function canMaterializePoint(_program, layer) {
15
- return layer.mark?.type === "point" && hasPositionScales(layer);
34
+ return layer.mark?.type === "point" && (
35
+ hasCartesianPositionScales(layer) || hasPolarPositionScales(layer)
36
+ );
16
37
  }
17
38
 
18
39
  export function canMaterializeLine(program, layer) {
19
40
  const dataset = findDataset(program, layer.data);
20
41
  const interval = dataset?.transform?.some(item => item.type === "interval");
42
+ if (hasPolarPositionScales(layer)) {
43
+ return (
44
+ layer.mark?.type === "line" &&
45
+ ["quantitative", "temporal", "ordinal", "nominal"].includes(
46
+ layer.encoding.theta.fieldType
47
+ ) &&
48
+ layer.encoding.radius.fieldType === "quantitative"
49
+ );
50
+ }
21
51
  return (
22
52
  layer.mark?.type === "line" &&
23
53
  hasPositionScales(layer) &&
@@ -50,15 +80,44 @@ export function canMaterializeArea(program, layer) {
50
80
  );
51
81
  }
52
82
 
83
+ export function canMaterializeArc(_program, layer) {
84
+ if (
85
+ layer.mark?.type !== "arc" ||
86
+ layer.encoding?.theta?.scale === undefined ||
87
+ !["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
88
+ ) {
89
+ return false;
90
+ }
91
+ if (layer.encoding.theta.aggregate === "count") {
92
+ return layer.encoding?.radius === undefined;
93
+ }
94
+ return (
95
+ layer.encoding.theta.aggregate === undefined &&
96
+ layer.encoding?.radius?.scale !== undefined &&
97
+ layer.encoding.radius.fieldType === "quantitative"
98
+ );
99
+ }
100
+
53
101
  export function canMaterializeBar(program, layer) {
54
102
  if (layer.mark?.type !== "bar" || !hasPositionScales(layer)) {
55
103
  return false;
56
104
  }
57
105
  const grain = resolveBarGrain(layer);
58
- return grain === BAR_GRAINS.histogram || (
59
- [BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(grain) &&
60
- program.markConfigs[layer.id]?.barWidth !== undefined
61
- );
106
+ if (![BAR_GRAINS.histogram, BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(grain)) {
107
+ return false;
108
+ }
109
+ if (
110
+ grain === BAR_GRAINS.aggregate &&
111
+ resolveBarColorLayout(layer) === "group"
112
+ ) {
113
+ const offsetChannel = resolveBarOffsetChannel(layer);
114
+ return (
115
+ layer.encoding?.color?.field !== undefined &&
116
+ layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
117
+ layer.encoding[offsetChannel].scale !== undefined
118
+ );
119
+ }
120
+ return true;
62
121
  }
63
122
 
64
123
  export function canMaterializeRule(program, layer) {
@@ -71,40 +130,187 @@ export function canMaterializeRule(program, layer) {
71
130
  );
72
131
  }
73
132
 
133
+ export function canMaterializeText(program, layer) {
134
+ if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
135
+ return false;
136
+ }
137
+ if (layer.source !== undefined) {
138
+ const source = findLayer(program, layer.source);
139
+ return source !== undefined &&
140
+ ["point", "bar", "rule", "rect"].includes(source.mark?.type) &&
141
+ Array.isArray(program.graphicSpec.objects[source.id]?.items);
142
+ }
143
+ return hasCartesianPositionScales(layer);
144
+ }
145
+
146
+ export function canMaterializeRect(_program, layer) {
147
+ return resolveRectMode(layer) !== undefined;
148
+ }
149
+
74
150
  const MARK_MATERIALIZATION_POLICIES = Object.freeze({
75
151
  point: Object.freeze({
76
152
  canMaterialize: canMaterializePoint,
77
- op: "rematerializePointMark"
153
+ op: "rematerializePointMark",
154
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
155
+ encoding: Object.freeze({ scaleFirst: true }),
156
+ scaleApplication: Object.freeze({
157
+ deferWithMark: true,
158
+ position: "rematerialize",
159
+ deferredChannels: Object.freeze(["size", "shape"]),
160
+ default: "direct"
161
+ }),
162
+ rematerializeIncompleteExisting: true
78
163
  }),
79
164
  line: Object.freeze({
80
165
  canMaterialize: canMaterializeLine,
81
- op: "rematerializeLineMark"
166
+ op: "rematerializeLineMark",
167
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
168
+ scaleApplication: Object.freeze({ default: "defer" })
82
169
  }),
83
170
  area: Object.freeze({
84
171
  canMaterialize: canMaterializeArea,
85
- op: "rematerializeAreaMark"
172
+ op: "rematerializeAreaMark",
173
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
174
+ encoding: Object.freeze({ sharedChannels: Object.freeze(["color"]) }),
175
+ scaleApplication: Object.freeze({ default: "defer" })
176
+ }),
177
+ arc: Object.freeze({
178
+ canMaterialize: canMaterializeArc,
179
+ op: "rematerializeArcMark",
180
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
181
+ encoding: Object.freeze({ completeOnly: true }),
182
+ scaleApplication: Object.freeze({ default: "defer" })
86
183
  }),
87
184
  bar: Object.freeze({
88
185
  canMaterialize: canMaterializeBar,
89
- op: "rematerializeBarMark"
186
+ op: "rematerializeBarMark",
187
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
188
+ scaleApplication: Object.freeze({ default: "defer" })
90
189
  }),
91
190
  rule: Object.freeze({
92
191
  canMaterialize: canMaterializeRule,
93
- op: "rematerializeRuleMark"
192
+ op: "rematerializeRuleMark",
193
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: false }),
194
+ scaleApplication: Object.freeze({ default: "defer" })
195
+ }),
196
+ text: Object.freeze({
197
+ canMaterialize: canMaterializeText,
198
+ op: "rematerializeTextMark",
199
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
200
+ encoding: Object.freeze({ scaleFirst: true }),
201
+ scaleApplication: Object.freeze({
202
+ deferWithMark: true,
203
+ position: "rematerialize",
204
+ default: "defer"
205
+ }),
206
+ rematerializeIncompleteExisting: true,
207
+ sourceDependent: true
208
+ }),
209
+ rect: Object.freeze({
210
+ canMaterialize: canMaterializeRect,
211
+ op: "rematerializeRectMark",
212
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
213
+ encoding: Object.freeze({ scaleFirst: true }),
214
+ scaleApplication: Object.freeze({
215
+ deferWithMark: true,
216
+ position: "rematerialize",
217
+ default: "defer"
218
+ }),
219
+ rematerializeIncompleteExisting: true
94
220
  })
95
221
  });
96
222
 
223
+ function getMarkPolicy(layer) {
224
+ return MARK_MATERIALIZATION_POLICIES[layer.mark?.type];
225
+ }
226
+
97
227
  export function getMarkRematerializationStep(layer) {
98
- const policy = MARK_MATERIALIZATION_POLICIES[layer.mark?.type];
228
+ const policy = getMarkPolicy(layer);
99
229
  return policy === undefined
100
230
  ? undefined
101
231
  : { op: policy.op, args: { id: layer.id } };
102
232
  }
103
233
 
104
234
  export function getMarkMaterializationStep(program, layer) {
105
- const policy = MARK_MATERIALIZATION_POLICIES[layer.mark?.type];
235
+ const policy = getMarkPolicy(layer);
106
236
  if (policy === undefined || !policy.canMaterialize(program, layer)) {
107
237
  return undefined;
108
238
  }
109
239
  return getMarkRematerializationStep(layer);
110
240
  }
241
+
242
+ export function getSourceDependentMarkSteps(program, sourceId) {
243
+ return program.semanticSpec.layers.flatMap(layer =>
244
+ layer.source === sourceId && getMarkPolicy(layer)?.sourceDependent === true
245
+ ? [getMarkMaterializationStep(program, layer)].filter(
246
+ step => step !== undefined
247
+ )
248
+ : []
249
+ );
250
+ }
251
+
252
+ export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
253
+ const policy = getMarkPolicy(layer);
254
+ if (policy === undefined) return [];
255
+ const scale = { op: "rematerializeScale", args: { id: scaleId } };
256
+ const complete = policy.canMaterialize(program, layer);
257
+ const mark = getMarkRematerializationStep(layer);
258
+ if (!complete) {
259
+ if (policy.positionEncoding.incomplete === "scale") return [scale];
260
+ return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
261
+ }
262
+ return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
263
+ }
264
+
265
+ export function getScaleConsumerMaterializationMode(layer, channel) {
266
+ const policy = getMarkPolicy(layer);
267
+ if (policy === undefined) return "direct";
268
+ if (POSITION_CHANNELS.includes(channel)) {
269
+ return policy.scaleApplication.position ?? policy.scaleApplication.default;
270
+ }
271
+ if (policy.scaleApplication.deferredChannels?.includes(channel)) {
272
+ return "defer";
273
+ }
274
+ return policy.scaleApplication.default;
275
+ }
276
+
277
+ export function canDeferScaleConsumerApplication(layer) {
278
+ return getMarkPolicy(layer)?.scaleApplication.deferWithMark === true;
279
+ }
280
+
281
+ export function getExistingMarkRematerializationStep(program, layer) {
282
+ const policy = getMarkPolicy(layer);
283
+ if (
284
+ policy?.rematerializeIncompleteExisting !== true ||
285
+ program.graphicSpec.objects[layer.id] === undefined
286
+ ) {
287
+ return undefined;
288
+ }
289
+ return getMarkRematerializationStep(layer);
290
+ }
291
+
292
+ export function getEncodingMaterializationStages(program, layer, channel, scale) {
293
+ const policy = getMarkPolicy(layer);
294
+ if (policy === undefined) return { scales: [], marks: [] };
295
+ const scales = policy.encoding?.scaleFirst === true && scale !== undefined
296
+ ? [{
297
+ op: "rematerializeScale",
298
+ args: { id: scale, guides: false, marks: false }
299
+ }]
300
+ : [];
301
+ const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
302
+ scale !== undefined;
303
+ const candidates = shared
304
+ ? program.semanticSpec.layers.filter(candidate =>
305
+ candidate.mark?.type === layer.mark?.type &&
306
+ candidate.encoding?.[channel]?.scale === scale
307
+ )
308
+ : [layer];
309
+ const marks = candidates.flatMap(candidate => {
310
+ const step = policy.encoding?.completeOnly === true
311
+ ? getMarkMaterializationStep(program, candidate)
312
+ : getMarkRematerializationStep(candidate);
313
+ return step === undefined ? [] : [step];
314
+ });
315
+ return { scales, marks };
316
+ }
@@ -6,30 +6,123 @@ const STAGES = Object.freeze([
6
6
  "highlights"
7
7
  ]);
8
8
 
9
+ const STAGE_SET = new Set(STAGES);
10
+
11
+ function isPlainObject(value) {
12
+ if (value === null || typeof value !== "object") return false;
13
+ const prototype = Object.getPrototypeOf(value);
14
+ return prototype === Object.prototype || prototype === null;
15
+ }
16
+
17
+ function clonePlanValue(value, path) {
18
+ if (
19
+ value === null ||
20
+ typeof value === "string" ||
21
+ typeof value === "boolean"
22
+ ) {
23
+ return value;
24
+ }
25
+ if (typeof value === "number" && Number.isFinite(value)) return value;
26
+ if (Array.isArray(value)) {
27
+ return Object.freeze(value.map((item, index) =>
28
+ clonePlanValue(item, `${path}[${index}]`)
29
+ ));
30
+ }
31
+ if (isPlainObject(value)) {
32
+ const clone = {};
33
+ for (const key of Object.keys(value)) {
34
+ if (value[key] === undefined) continue;
35
+ clone[key] = clonePlanValue(value[key], `${path}.${key}`);
36
+ }
37
+ return Object.freeze(clone);
38
+ }
39
+ throw new TypeError(
40
+ `${path} must contain only finite JSON-compatible values.`
41
+ );
42
+ }
43
+
44
+ function canonicalValue(value) {
45
+ if (Array.isArray(value)) {
46
+ return `[${value.map(canonicalValue).join(",")}]`;
47
+ }
48
+ if (isPlainObject(value)) {
49
+ return `{${Object.keys(value).sort().map(key =>
50
+ `${JSON.stringify(key)}:${canonicalValue(value[key])}`
51
+ ).join(",")}}`;
52
+ }
53
+ return JSON.stringify(value);
54
+ }
55
+
56
+ function normalizeStep(step, index) {
57
+ if (!isPlainObject(step)) {
58
+ throw new TypeError(`Materialization step ${index} must be a plain object.`);
59
+ }
60
+ if (typeof step.op !== "string" || step.op.length === 0) {
61
+ throw new TypeError(
62
+ `Materialization step ${index} op must be a non-empty string.`
63
+ );
64
+ }
65
+ if (step.args !== undefined && !isPlainObject(step.args)) {
66
+ throw new TypeError(
67
+ `Materialization step ${index} args must be a plain object.`
68
+ );
69
+ }
70
+ return Object.freeze({
71
+ op: step.op,
72
+ ...(step.args === undefined
73
+ ? {}
74
+ : { args: clonePlanValue(step.args, `Materialization step ${index} args`) })
75
+ });
76
+ }
77
+
78
+ function normalizeSteps(steps) {
79
+ if (!Array.isArray(steps)) {
80
+ throw new TypeError("Materialization plan stages must be arrays.");
81
+ }
82
+ return steps
83
+ .filter(step => step !== undefined)
84
+ .map((step, index) => normalizeStep(step, index));
85
+ }
86
+
9
87
  function stepKey(step) {
10
- return JSON.stringify([step.op, step.args ?? null]);
88
+ return `${JSON.stringify(step.op)}:${canonicalValue(step.args ?? null)}`;
11
89
  }
12
90
 
13
- export function buildMaterializationPlan(stages = {}) {
91
+ function deduplicateSteps(steps) {
14
92
  const plan = [];
15
93
  const seen = new Set();
94
+ for (const step of steps) {
95
+ const key = stepKey(step);
96
+ if (seen.has(key)) continue;
97
+ seen.add(key);
98
+ plan.push(step);
99
+ }
100
+ return Object.freeze(plan);
101
+ }
16
102
 
17
- for (const stage of STAGES) {
18
- for (const step of stages[stage] ?? []) {
19
- if (step === undefined) continue;
20
- const key = stepKey(step);
21
- if (seen.has(key)) continue;
22
- seen.add(key);
23
- plan.push(step);
103
+ export function buildMaterializationPlan(stages = {}) {
104
+ if (!isPlainObject(stages)) {
105
+ throw new TypeError("Materialization plan stages must be a plain object.");
106
+ }
107
+ for (const stage of Object.keys(stages)) {
108
+ if (!STAGE_SET.has(stage)) {
109
+ throw new Error(`Unknown materialization stage "${stage}".`);
24
110
  }
25
111
  }
26
-
27
- return plan;
112
+ return deduplicateSteps(STAGES.flatMap(stage =>
113
+ normalizeSteps(stages[stage] ?? [])
114
+ ));
28
115
  }
29
116
 
30
117
  export function applyMaterializationPlan(program, plan) {
118
+ const steps = deduplicateSteps(normalizeSteps(plan));
31
119
  let next = program;
32
- for (const step of buildMaterializationPlan({ marks: plan })) {
120
+ for (const step of steps) {
121
+ if (typeof next?.[step.op] !== "function") {
122
+ throw new Error(
123
+ `Materialization operation "${step.op}" is not available on the program.`
124
+ );
125
+ }
33
126
  next = step.args === undefined
34
127
  ? next[step.op]()
35
128
  : next[step.op](step.args);
@@ -0,0 +1,138 @@
1
+ import {
2
+ mapOrdinalPositionValues,
3
+ readScaleField
4
+ } from "../grammar/scales.js";
5
+ import { RECT_MODES, resolveRectMode } from "../grammar/rects.js";
6
+ import { DEFAULT_RECT_MARK } from "./rectConfig.js";
7
+ import { mapScaleConsumerValues } from "./scales/map.js";
8
+
9
+ function optionalValues(rows, encoding) {
10
+ return readScaleField(rows, encoding.field, encoding.fieldType, {
11
+ allowUnknown: true
12
+ });
13
+ }
14
+
15
+ function requiredChannels(layer, mode) {
16
+ return mode === RECT_MODES.ranged
17
+ ? ["x", "x2", "y", "y2", ...(layer.encoding?.color ? ["color"] : [])]
18
+ : ["x", "y", ...(layer.encoding?.color ? ["color"] : [])];
19
+ }
20
+
21
+ export function resolveRectConsumerValues(layer, dataset, channel) {
22
+ const requested = optionalValues(dataset.values, layer.encoding[channel]);
23
+ const mode = resolveRectMode(layer);
24
+ if (mode === undefined) return requested;
25
+ const complete = requiredChannels(layer, mode).map(candidate =>
26
+ optionalValues(dataset.values, layer.encoding[candidate])
27
+ );
28
+ return requested.map((value, index) =>
29
+ complete.every(values => values[index] !== undefined) ? value : undefined
30
+ );
31
+ }
32
+
33
+ function mapOptional(values, scale, channel, categorical = false) {
34
+ return values.map(value => {
35
+ if (value === undefined) return undefined;
36
+ return categorical
37
+ ? mapOrdinalPositionValues([value], scale)[0]
38
+ : mapScaleConsumerValues([value], scale, channel)[0];
39
+ });
40
+ }
41
+
42
+ function mappedEncoding(program, layer, dataset, channel) {
43
+ const encoding = layer.encoding[channel];
44
+ const scale = program.resolvedScales[encoding.scale];
45
+ if (scale === undefined) {
46
+ throw new Error(
47
+ `Rect mark "${layer.id}" requires resolved ${channel} scale "${encoding.scale}".`
48
+ );
49
+ }
50
+ const values = resolveRectConsumerValues(layer, dataset, channel);
51
+ const categorical = ["nominal", "ordinal"].includes(encoding.fieldType) &&
52
+ ["x", "y"].includes(channel);
53
+ return {
54
+ values: mapOptional(values, scale, channel, categorical),
55
+ scale
56
+ };
57
+ }
58
+
59
+ function appearance(config, fill) {
60
+ return {
61
+ fill,
62
+ opacity: config.opacity,
63
+ stroke: config.stroke === false ? "transparent" : config.stroke,
64
+ strokeWidth: config.stroke === false ? 0 : config.strokeWidth
65
+ };
66
+ }
67
+
68
+ export function resolveRectRows(program, layer, dataset) {
69
+ const mode = resolveRectMode(layer);
70
+ if (mode === undefined) return [];
71
+ const x = mappedEncoding(program, layer, dataset, "x");
72
+ const y = mappedEncoding(program, layer, dataset, "y");
73
+ const x2 = mode === RECT_MODES.ranged
74
+ ? mappedEncoding(program, layer, dataset, "x2")
75
+ : undefined;
76
+ const y2 = mode === RECT_MODES.ranged
77
+ ? mappedEncoding(program, layer, dataset, "y2")
78
+ : undefined;
79
+ const color = layer.encoding?.color === undefined
80
+ ? undefined
81
+ : mappedEncoding(program, layer, dataset, "color").values;
82
+ const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
83
+
84
+ return dataset.values.flatMap((row, index) => {
85
+ const fill = color === undefined ? config.fill : color[index];
86
+ if (
87
+ !Number.isFinite(x.values[index]) ||
88
+ !Number.isFinite(y.values[index]) ||
89
+ typeof fill !== "string"
90
+ ) return [];
91
+
92
+ let geometry;
93
+ if (mode === RECT_MODES.discrete) {
94
+ const width = x.scale.bandwidth;
95
+ const height = y.scale.bandwidth;
96
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
97
+ return [];
98
+ }
99
+ geometry = {
100
+ x: x.values[index] - width / 2,
101
+ y: y.values[index] - height / 2,
102
+ width,
103
+ height
104
+ };
105
+ } else {
106
+ const endX = x2.values[index];
107
+ const endY = y2.values[index];
108
+ if (!Number.isFinite(endX) || !Number.isFinite(endY)) return [];
109
+ geometry = {
110
+ x: Math.min(x.values[index], endX),
111
+ y: Math.min(y.values[index], endY),
112
+ width: Math.abs(endX - x.values[index]),
113
+ height: Math.abs(endY - y.values[index])
114
+ };
115
+ if (geometry.width <= 0 || geometry.height <= 0) return [];
116
+ }
117
+ return [{
118
+ row,
119
+ sourceIndex: index,
120
+ channels: Object.fromEntries(
121
+ ["x", "y", "x2", "y2", "color"].flatMap(channel => {
122
+ const encoding = layer.encoding?.[channel];
123
+ return encoding?.field === undefined
124
+ ? []
125
+ : [[channel, row[encoding.field]]];
126
+ })
127
+ ),
128
+ properties: { ...geometry, ...appearance(config, fill) }
129
+ }];
130
+ });
131
+ }
132
+
133
+ export function resolveRectGraphicItems(program, layer, dataset) {
134
+ return resolveRectRows(program, layer, dataset).map(item => ({
135
+ type: "rect",
136
+ properties: item.properties
137
+ }));
138
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ validateNonEmptyString,
3
+ validateNonNegativeFinite,
4
+ validateUnitInterval
5
+ } from "../core/validation.js";
6
+ import { DEFAULT_COLORS } from "../theme/defaults.js";
7
+
8
+ export const DEFAULT_RECT_MARK = Object.freeze({
9
+ fill: DEFAULT_COLORS.mark,
10
+ opacity: 1,
11
+ stroke: "white",
12
+ strokeWidth: 1
13
+ });
14
+
15
+ export function normalizeRectMarkConfig(patch, previous = DEFAULT_RECT_MARK) {
16
+ const result = { ...previous };
17
+ if (Object.hasOwn(patch, "fill")) {
18
+ result.fill = validateNonEmptyString(patch.fill, "Rect fill");
19
+ }
20
+ if (Object.hasOwn(patch, "opacity")) {
21
+ result.opacity = validateUnitInterval(patch.opacity, "Rect opacity");
22
+ }
23
+ if (Object.hasOwn(patch, "stroke")) {
24
+ result.stroke = patch.stroke === false
25
+ ? false
26
+ : validateNonEmptyString(patch.stroke, "Rect stroke");
27
+ }
28
+ if (Object.hasOwn(patch, "strokeWidth")) {
29
+ if (result.stroke === false) {
30
+ throw new Error("Rect strokeWidth requires an active stroke.");
31
+ }
32
+ result.strokeWidth = validateNonNegativeFinite(
33
+ patch.strokeWidth,
34
+ "Rect strokeWidth"
35
+ );
36
+ }
37
+ return Object.freeze(result);
38
+ }
@@ -0,0 +1,33 @@
1
+ import { POSITION_CHANNELS } from "../core/vocabulary.js";
2
+ import {
3
+ mapOrdinalPositionValues,
4
+ readNominalField,
5
+ readQuantitativeField,
6
+ readScaleField,
7
+ readTemporalField
8
+ } from "../grammar/scales.js";
9
+ import { mapScaleConsumerValues } from "./scales/map.js";
10
+
11
+ export function resolveRowEncodingValues(program, layer, dataset, channel) {
12
+ const encoding = layer.encoding?.[channel];
13
+ if (encoding === undefined) return undefined;
14
+ const scale = program.resolvedScales[encoding.scale];
15
+ if (scale === undefined) {
16
+ throw new Error(
17
+ `${layer.mark.type} mark "${layer.id}" requires resolved ${channel} scale "${encoding.scale}".`
18
+ );
19
+ }
20
+ const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
21
+ const values = Object.hasOwn(scale, "unknown")
22
+ ? readScaleField(dataset.values, encoding.field, encoding.fieldType, {
23
+ allowUnknown: true
24
+ })
25
+ : categorical
26
+ ? readNominalField(dataset.values, encoding.field)
27
+ : encoding.fieldType === "temporal"
28
+ ? readTemporalField(dataset.values, encoding.field)
29
+ : readQuantitativeField(dataset.values, encoding.field);
30
+ return categorical && POSITION_CHANNELS.includes(channel)
31
+ ? mapOrdinalPositionValues(values, scale)
32
+ : mapScaleConsumerValues(values, scale, channel);
33
+ }