ggaction 0.0.2 → 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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  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/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. 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
+ }
@@ -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
+ }
@@ -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/index.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
+ }