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,66 @@
1
+ function replaceAlternateBinding(program, target, channel, previous, usesField) {
2
+ if (previous === undefined) return program;
3
+ const alternate = usesField ? "datum" : "field";
4
+ return Object.hasOwn(previous, alternate)
5
+ ? program.editSemantic({
6
+ property: `layer[${target}].encoding.${channel}.${alternate}`,
7
+ remove: true
8
+ })
9
+ : program;
10
+ }
11
+
12
+ function applyBin(program, target, channel, previous, bin) {
13
+ if (bin === undefined) return program;
14
+ const [mode] = Object.keys(bin);
15
+ let next = program;
16
+ for (const previousMode of Object.keys(previous?.bin ?? {})) {
17
+ if (previousMode === mode) continue;
18
+ next = next.editSemantic({
19
+ property: `layer[${target}].encoding.${channel}.bin.${previousMode}`,
20
+ remove: true
21
+ });
22
+ }
23
+ return next.editSemantic({
24
+ property: `layer[${target}].encoding.${channel}.bin.${mode}`,
25
+ value: bin[mode]
26
+ });
27
+ }
28
+
29
+ export function applyPositionSemantics(program, {
30
+ target,
31
+ channel,
32
+ previous,
33
+ field,
34
+ datum,
35
+ hasField,
36
+ fieldType,
37
+ bin,
38
+ aggregate,
39
+ stack
40
+ }) {
41
+ let next = replaceAlternateBinding(
42
+ program,
43
+ target,
44
+ channel,
45
+ previous,
46
+ hasField
47
+ )
48
+ .editSemantic({
49
+ property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
50
+ value: hasField ? field : datum
51
+ })
52
+ .editSemantic({
53
+ property: `layer[${target}].encoding.${channel}.fieldType`,
54
+ value: fieldType
55
+ });
56
+
57
+ next = applyBin(next, target, channel, previous, bin);
58
+ for (const [property, value] of Object.entries({ aggregate, stack })) {
59
+ if (value === undefined) continue;
60
+ next = next.editSemantic({
61
+ property: `layer[${target}].encoding.${channel}.${property}`,
62
+ value
63
+ });
64
+ }
65
+ return next;
66
+ }
@@ -0,0 +1,130 @@
1
+ import { action } from "../../../core/action.js";
2
+ import {
3
+ getPositionEncodingMaterializationSteps
4
+ } from "../../../materialization/marks/index.js";
5
+ import { resolvePositionEncoding } from "./resolve.js";
6
+ import { findLayer } from "../../../selectors/layers.js";
7
+ import { applyPositionSemantics } from "./apply.js";
8
+ import {
9
+ applyEncodingScale,
10
+ rebindPositionGuides
11
+ } from "../shared.js";
12
+
13
+ function encodePosition(program, channel, args, operation) {
14
+ const {
15
+ target,
16
+ layer,
17
+ previous,
18
+ requestedScale,
19
+ field,
20
+ datum,
21
+ hasField,
22
+ fieldType,
23
+ scale,
24
+ coordinate,
25
+ bin,
26
+ aggregate,
27
+ stack
28
+ } = resolvePositionEncoding(program, channel, args, operation);
29
+
30
+ let next = program
31
+ .createCoordinate({
32
+ id: coordinate.id,
33
+ type: coordinate.type,
34
+ layers: [target]
35
+ });
36
+ next = applyPositionSemantics(next, {
37
+ target,
38
+ channel,
39
+ previous,
40
+ field,
41
+ datum,
42
+ hasField,
43
+ fieldType,
44
+ bin,
45
+ aggregate,
46
+ stack
47
+ });
48
+
49
+ next = next.editSemantic({
50
+ property: `layer[${target}].encoding.${channel}.scale`,
51
+ value: scale.id
52
+ });
53
+ next = applyEncodingScale(next, scale, requestedScale, {
54
+ reassignment: previous?.scale === scale.id
55
+ });
56
+ next = rebindPositionGuides(
57
+ next,
58
+ channel,
59
+ previous?.scale,
60
+ scale.id,
61
+ target
62
+ );
63
+
64
+ if (layer.mark.type === "bar") {
65
+ const updated = findLayer(next, target);
66
+ const pendingBox = next.markConfigs[target]?.boxPlot;
67
+ if (pendingBox !== undefined && !pendingBox.materialized) {
68
+ return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
69
+ ? next.materializeBoxPlot({ id: target })
70
+ : next;
71
+ }
72
+ }
73
+
74
+ const updated = findLayer(next, target);
75
+ for (const step of getPositionEncodingMaterializationSteps(
76
+ next,
77
+ updated,
78
+ scale.id
79
+ )) {
80
+ next = next[step.op](step.args);
81
+ }
82
+ return next;
83
+ }
84
+
85
+ const encodeX = action(
86
+ {
87
+ op: "encodeX",
88
+ description: "Encode a field as horizontal position."
89
+ },
90
+ function (args = {}) {
91
+ return encodePosition(this, "x", args, "encodeX");
92
+ }
93
+ );
94
+
95
+ const encodeY = action(
96
+ {
97
+ op: "encodeY",
98
+ description: "Encode a field as vertical position."
99
+ },
100
+ function (args = {}) {
101
+ return encodePosition(this, "y", args, "encodeY");
102
+ }
103
+ );
104
+
105
+ const encodeTheta = action(
106
+ {
107
+ op: "encodeTheta",
108
+ description: "Encode a field as Polar angle in clockwise degrees."
109
+ },
110
+ function (args = {}) {
111
+ return encodePosition(this, "theta", args, "encodeTheta");
112
+ }
113
+ );
114
+
115
+ const encodeR = action(
116
+ {
117
+ op: "encodeR",
118
+ description: "Encode a quantitative field as Polar radius."
119
+ },
120
+ function (args = {}) {
121
+ return encodePosition(this, "radius", args, "encodeR");
122
+ }
123
+ );
124
+
125
+ export function registerPositionEncodingActions(ProgramClass) {
126
+ ProgramClass.prototype.encodeX = encodeX;
127
+ ProgramClass.prototype.encodeY = encodeY;
128
+ ProgramClass.prototype.encodeTheta = encodeTheta;
129
+ ProgramClass.prototype.encodeR = encodeR;
130
+ }
@@ -0,0 +1,26 @@
1
+ import { validateAggregate } from "../../../../grammar/aggregate.js";
2
+ import { emptyPositionPolicy } from "./common.js";
3
+
4
+ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
5
+ if (args.bin !== undefined) {
6
+ throw new Error(`Arc ${channel} encoding does not support bin.`);
7
+ }
8
+ if (args.stack !== undefined) {
9
+ throw new Error(`Arc ${channel} encoding does not support stack.`);
10
+ }
11
+ if (channel === "radius") {
12
+ if (args.aggregate !== undefined) {
13
+ throw new Error("Arc radius encoding does not support aggregate.");
14
+ }
15
+ return emptyPositionPolicy();
16
+ }
17
+ if (!["ordinal", "nominal"].includes(fieldType)) {
18
+ throw new Error("Arc theta encoding requires an ordinal or nominal field.");
19
+ }
20
+ if (args.aggregate === undefined) return emptyPositionPolicy();
21
+ const aggregate = validateAggregate(args.aggregate);
22
+ if (aggregate !== "count") {
23
+ throw new Error('Arc theta aggregate currently supports only "count".');
24
+ }
25
+ return { bin: undefined, aggregate, stack: undefined };
26
+ }
@@ -4,6 +4,7 @@ import {
4
4
  } from "../../../../grammar/aggregate.js";
5
5
  import {
6
6
  BAR_ORIENTATIONS,
7
+ resolveBarOffsetChannel,
7
8
  resolveBarOrientation
8
9
  } from "../../../../grammar/bars/policy.js";
9
10
  import { resolveBin, validateStack } from "./common.js";
@@ -98,12 +99,15 @@ export function resolveBarPositionPolicy({
98
99
  `Bar ${channel} encoding requires a quantitative field opposite a categorical position.`
99
100
  );
100
101
  }
101
- if (
102
- orientation === BAR_ORIENTATIONS.horizontal &&
103
- candidate.encoding?.xOffset !== undefined
104
- ) {
102
+ const expectedOffset = resolveBarOffsetChannel(candidate);
103
+ const incompatibleOffset = orientation === BAR_ORIENTATIONS.horizontal
104
+ ? "xOffset"
105
+ : orientation === BAR_ORIENTATIONS.vertical
106
+ ? "yOffset"
107
+ : undefined;
108
+ if (incompatibleOffset !== undefined && candidate.encoding?.[incompatibleOffset] !== undefined) {
105
109
  throw new Error(
106
- "Horizontal grouped bars require yOffset support, which is not available yet."
110
+ `${orientation} bars require ${expectedOffset}; remove the incompatible ${incompatibleOffset} encoding.`
107
111
  );
108
112
  }
109
113
  return { bin, aggregate, stack };
@@ -1,6 +1,7 @@
1
1
  import { validatePositionFieldCompatibility } from
2
2
  "../../../../grammar/positionCompatibility.js";
3
3
  import { resolveAreaPositionPolicy } from "./area.js";
4
+ import { resolveArcPositionPolicy } from "./arc.js";
4
5
  import { resolveBarPositionPolicy } from "./bar.js";
5
6
  import { resolveLinePositionPolicy } from "./line.js";
6
7
  import { resolvePointPositionPolicy } from "./point.js";
@@ -8,10 +9,13 @@ import { resolveRulePositionPolicy } from "./rule.js";
8
9
 
9
10
  const POSITION_POLICIES = Object.freeze({
10
11
  area: resolveAreaPositionPolicy,
12
+ arc: resolveArcPositionPolicy,
11
13
  bar: resolveBarPositionPolicy,
12
14
  line: resolveLinePositionPolicy,
13
15
  point: resolvePointPositionPolicy,
14
- rule: resolveRulePositionPolicy
16
+ rule: resolveRulePositionPolicy,
17
+ text: resolvePointPositionPolicy,
18
+ rect: resolvePointPositionPolicy
15
19
  });
16
20
 
17
21
  export function resolveMarkPositionPolicy(context) {
@@ -14,22 +14,39 @@ function rejectSharedOptions(args, channel) {
14
14
  }
15
15
 
16
16
  export function resolveLinePositionPolicy({
17
+ program,
17
18
  layer,
18
19
  dataset,
19
20
  channel,
20
21
  args,
21
22
  fieldType
22
23
  }) {
24
+ const polar = ["theta", "radius"].includes(channel);
25
+ const config = program.markConfigs[layer.id] ?? {};
26
+ if (polar) {
27
+ if (config.curve !== undefined && config.curve !== "linear") {
28
+ throw new Error("Polar line position currently requires curve \"linear\".");
29
+ }
30
+ rejectSharedOptions(args, channel);
31
+ if (args.aggregate !== undefined) {
32
+ throw new Error(`Line ${channel} encoding does not support aggregate.`);
33
+ }
34
+ return emptyPositionPolicy();
35
+ }
36
+ if (config.closed === true) {
37
+ throw new Error("Line closed requires theta/radius Polar position encodings.");
38
+ }
23
39
  const regression = dataset.transform?.some(item => item.type === "regression");
24
40
  const interval = dataset.transform?.some(item => item.type === "interval");
25
41
 
26
42
  if (channel === "x") {
27
- const horizontalDirect =
28
- layer.encoding?.y?.fieldType === "temporal" &&
43
+ const directPair =
44
+ layer.encoding?.y?.aggregate === undefined &&
45
+ ["quantitative", "temporal"].includes(layer.encoding?.y?.fieldType) &&
29
46
  fieldType === "quantitative";
30
47
  if (
31
48
  fieldType !== "temporal" &&
32
- !((regression || interval || horizontalDirect) && fieldType === "quantitative")
49
+ !((regression || interval || directPair) && fieldType === "quantitative")
33
50
  ) {
34
51
  throw new Error(
35
52
  "Line x encoding requires a temporal field or a compatible derived quantitative field."
@@ -7,7 +7,7 @@ import {
7
7
  readTemporalField,
8
8
  validateFieldType,
9
9
  validatePositionChannel
10
- } from "../../../grammar/scales.js";
10
+ } from "../../../grammar/scales/index.js";
11
11
  import { resolvePositionScaleDefinition } from "../../scales/definitions.js";
12
12
  import { findCoordinate } from "../../../selectors/coordinates.js";
13
13
  import {
@@ -22,12 +22,29 @@ import {
22
22
  } from "../../../grammar/aggregate.js";
23
23
  import { normalizeRuleDatum } from "../../../grammar/rules.js";
24
24
  import { resolveMarkPositionPolicy } from "./policies/index.js";
25
+ import {
26
+ getPositionChannelDefinition,
27
+ positionChannelsForFamily
28
+ } from "../../../core/vocabulary.js";
25
29
 
26
30
  const POSITION_ENCODING_OPTIONS = Object.freeze([
27
31
  "field", "datum", "target", "fieldType", "scale", "coordinate",
28
32
  "aggregate", "bin", "stack"
29
33
  ]);
30
34
 
35
+ function validateCoordinateFamily(layer, channel, operation) {
36
+ const family = getPositionChannelDefinition(channel).family;
37
+ const incompatible = positionChannelsForFamily(
38
+ family === "polar" ? "cartesian" : "polar"
39
+ );
40
+ const existing = incompatible.filter(name => layer.encoding?.[name] !== undefined);
41
+ if (existing.length > 0) {
42
+ throw new Error(
43
+ `${operation} cannot mix ${channel} with ${existing.join("/")} position encodings on layer "${layer.id}".`
44
+ );
45
+ }
46
+ }
47
+
31
48
  function resolveCoordinate(program, channel, layer, requestedId) {
32
49
  const defaults = getPositionCoordinateDefaults(channel);
33
50
  const existingId = layer.coordinate;
@@ -35,7 +52,19 @@ function resolveCoordinate(program, channel, layer, requestedId) {
35
52
  if (existingId !== undefined && requestedId !== undefined && existingId !== requestedId) {
36
53
  throw new Error(`Layer "${layer.id}" already uses coordinate "${existingId}".`);
37
54
  }
38
- const id = existingId ?? requestedId ?? defaults.id;
55
+ const compatible = program.semanticSpec.coordinates.filter(
56
+ coordinate => coordinate.type === defaults.type
57
+ );
58
+ if (
59
+ existingId === undefined &&
60
+ requestedId === undefined &&
61
+ compatible.length > 1
62
+ ) {
63
+ throw new Error(
64
+ `${channel} encoding requires coordinate when multiple ${defaults.type} coordinates are available.`
65
+ );
66
+ }
67
+ const id = existingId ?? requestedId ?? compatible[0]?.id ?? defaults.id;
39
68
  const coordinate = findCoordinate(program, id);
40
69
  if (coordinate !== undefined && coordinate.type !== defaults.type) {
41
70
  throw new Error(
@@ -51,8 +80,9 @@ export function resolvePositionEncoding(program, channel, args, operation) {
51
80
  const { id: target, dataset, layer } = resolveTarget(
52
81
  program,
53
82
  args.target,
54
- ["point", "line", "bar", "area", "rule"]
83
+ getPositionChannelDefinition(channel).markTypes
55
84
  );
85
+ validateCoordinateFamily(layer, channel, operation);
56
86
  const hasField = Object.hasOwn(args, "field");
57
87
  const hasDatum = Object.hasOwn(args, "datum");
58
88
  if (layer.mark.type === "rule") {
@@ -66,8 +96,11 @@ export function resolvePositionEncoding(program, channel, args, operation) {
66
96
  throw new Error(`${operation} does not support datum for a ${layer.mark.type} mark.`);
67
97
  }
68
98
  const previous = layer.encoding?.[channel];
69
- const requestedFieldType =
70
- args.fieldType ?? previous?.fieldType ?? "quantitative";
99
+ const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
100
+ layer.mark.type === "arc" && channel === "theta" && args.aggregate === "count"
101
+ ? "nominal"
102
+ : "quantitative"
103
+ );
71
104
  const fieldType = requestedFieldType === "nominal"
72
105
  ? "nominal"
73
106
  : validateFieldType(requestedFieldType);
@@ -98,7 +131,9 @@ export function resolvePositionEncoding(program, channel, args, operation) {
98
131
  throw new TypeError(`${operation} field must be a non-empty string.`);
99
132
  }
100
133
 
101
- const aggregateOutput = isAggregate(policy.aggregate);
134
+ const aggregateOutput = isAggregate(policy.aggregate) && !(
135
+ layer.mark.type === "arc" && channel === "theta"
136
+ );
102
137
  const requestedScale = resolveReassignmentScaleOptions(
103
138
  previous,
104
139
  Object.hasOwn(args, "scale") ? args.scale : {}
@@ -119,9 +154,21 @@ export function resolvePositionEncoding(program, channel, args, operation) {
119
154
  ? { nice: true }
120
155
  : { discreteType: "band" }
121
156
  : ["ordinal", "nominal"].includes(fieldType)
122
- ? { discreteType: "point" }
157
+ ? {
158
+ discreteType: ["arc", "rect"].includes(layer.mark.type)
159
+ ? "band"
160
+ : "point"
161
+ }
123
162
  : {}
124
163
  );
164
+ if (
165
+ layer.mark.type === "arc" &&
166
+ channel === "theta" &&
167
+ ["ordinal", "nominal"].includes(fieldType) &&
168
+ scale.type !== "band"
169
+ ) {
170
+ throw new Error("Categorical arc theta position requires a band scale.");
171
+ }
125
172
  if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
126
173
  throw new Error(
127
174
  "Position scale unknown currently requires a row-owned point mark."
@@ -140,6 +187,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
140
187
  throw new TypeError(`Field "${field}" must contain a finite number at row ${index}.`);
141
188
  }
142
189
  }
190
+ } else if (layer.mark.type === "rect") {
191
+ readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
143
192
  } else if (Object.hasOwn(scale, "unknown")) {
144
193
  readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
145
194
  } else if (fieldType === "temporal") readTemporalField(dataset.values, field);
@@ -148,11 +197,11 @@ export function resolvePositionEncoding(program, channel, args, operation) {
148
197
  } else readQuantitativeField(dataset.values, field);
149
198
 
150
199
  if (
151
- layer.mark.type === "bar" &&
200
+ ["bar", "rect"].includes(layer.mark.type) &&
152
201
  ["ordinal", "nominal"].includes(fieldType) &&
153
202
  scale.type === "point"
154
203
  ) {
155
- throw new Error("Categorical bar positions require a band scale.");
204
+ throw new Error(`Categorical ${layer.mark.type} positions require a band scale.`);
156
205
  }
157
206
  return {
158
207
  target,
@@ -4,14 +4,15 @@ import { isPlainObject } from "../../core/immutable.js";
4
4
  import {
5
5
  readNominalField,
6
6
  readQuantitativeField,
7
+ readScaleField,
7
8
  readTemporalField,
8
9
  validateSemanticFieldType
9
- } from "../../grammar/scales.js";
10
+ } from "../../grammar/scales/index.js";
10
11
  import { normalizeRuleDatum } from "../../grammar/rules.js";
11
12
  import {
12
13
  canMaterializeArea,
13
14
  canMaterializeLine
14
- } from "../../materialization/marks.js";
15
+ } from "../../materialization/marks/index.js";
15
16
  import { findLayer } from "../../selectors/layers.js";
16
17
  import {
17
18
  resolveTarget,
@@ -85,16 +86,35 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
85
86
  }
86
87
  const fieldType = rule
87
88
  ? validateSemanticFieldType(args.fieldType)
88
- : args.fieldType ?? "quantitative";
89
- if (!rule && fieldType !== "quantitative") {
89
+ : layer.mark.type === "rect"
90
+ ? args.fieldType ?? primary.fieldType
91
+ : args.fieldType ?? "quantitative";
92
+ if (
93
+ !rule &&
94
+ layer.mark.type !== "rect" &&
95
+ fieldType !== "quantitative"
96
+ ) {
90
97
  throw new Error(`${operation} requires a quantitative field.`);
91
98
  }
99
+ if (
100
+ layer.mark.type === "rect" &&
101
+ !["quantitative", "temporal"].includes(fieldType)
102
+ ) {
103
+ throw new Error(`${operation} requires a quantitative or temporal rect field.`);
104
+ }
105
+ if (layer.mark.type === "rect" && fieldType !== primary.fieldType) {
106
+ throw new Error(
107
+ `${operation} fieldType must match the primary ${primaryChannel} fieldType.`
108
+ );
109
+ }
92
110
  if (rule && fieldType !== primary.fieldType) {
93
111
  throw new Error(
94
112
  `${operation} fieldType must match the primary ${primaryChannel} fieldType.`
95
113
  );
96
114
  }
97
- if (hasField) validateSecondaryField(dataset, args.field, fieldType);
115
+ if (hasField && layer.mark.type === "rect") {
116
+ readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true });
117
+ } else if (hasField) validateSecondaryField(dataset, args.field, fieldType);
98
118
  else normalizeRuleDatum(args.datum, fieldType, channel);
99
119
 
100
120
  const previous = layer.encoding?.[channel];
@@ -135,7 +155,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
135
155
  if (rule) return next.rematerializeRuleMark({ id: target });
136
156
  return layer.mark.type === "bar"
137
157
  ? next.rematerializeBarMark({ id: target })
138
- : next.rematerializeAreaMark({ id: target });
158
+ : layer.mark.type === "rect"
159
+ ? next.rematerializeRectMark({ id: target })
160
+ : next.rematerializeAreaMark({ id: target });
139
161
  }
140
162
 
141
163
  const encodeX2 = action(
@@ -149,7 +171,7 @@ const encodeX2 = action(
149
171
  "x2",
150
172
  args,
151
173
  "encodeX2",
152
- ["area", "rule", "bar"]
174
+ ["area", "rule", "bar", "rect"]
153
175
  );
154
176
  }
155
177
  );
@@ -165,7 +187,7 @@ const encodeY2 = action(
165
187
  "y2",
166
188
  args,
167
189
  "encodeY2",
168
- ["area", "rule", "bar"]
190
+ ["area", "rule", "bar", "rect"]
169
191
  );
170
192
  }
171
193
  );
@@ -3,10 +3,14 @@ import { findDataset } from "../../selectors/datasets.js";
3
3
  import { resolveEligibleLayer } from "../../selectors/layers.js";
4
4
  import { hasMaterializedLegend } from "../../materialization/legends.js";
5
5
  import { findSemanticScale } from "../../selectors/scales.js";
6
- import { validateKeys } from "../../core/validation.js";
6
+ import { validateOptionObject } from "../../core/validation.js";
7
+ import {
8
+ getMarkGraphicTypes,
9
+ getPositionChannelDefinition
10
+ } from "../../core/vocabulary.js";
7
11
 
8
12
  export function validateOptions(args, supported, operation) {
9
- validateKeys(args, supported, operation);
13
+ validateOptionObject(args, supported, operation);
10
14
  }
11
15
 
12
16
  export function validateLineSeriesCompatibility(layer, channel, field) {
@@ -65,10 +69,12 @@ export function rebindPositionGuides(
65
69
  nextScale,
66
70
  target
67
71
  ) {
72
+ const definition = getPositionChannelDefinition(channel);
73
+ if (definition?.guideChannel === undefined) return program;
68
74
  if (previousScale === undefined || previousScale === nextScale) return program;
69
75
 
70
- const axis = program.semanticSpec.guides.axis?.[channel];
71
- const direction = channel === "x" ? "vertical" : "horizontal";
76
+ const axis = program.semanticSpec.guides.axis?.[definition.guideChannel];
77
+ const direction = definition.gridDirection;
72
78
  const grid = program.semanticSpec.guides.grid?.[direction];
73
79
  const ownsAxis = axis?.scale === previousScale;
74
80
  const ownsGrid = grid?.scale === previousScale;
@@ -86,13 +92,13 @@ export function rebindPositionGuides(
86
92
  let next = program;
87
93
  if (ownsAxis) {
88
94
  next = next.editSemantic({
89
- property: `guide.axis.${channel}.scale`,
95
+ property: `guide.axis.${definition.guideChannel}.scale`,
90
96
  value: nextScale
91
97
  });
92
- for (const component of ["ticks", "labels", "title"]) {
93
- const config = next.guideConfigs.axis?.[channel]?.[component];
98
+ for (const component of ["line", "ticks", "labels", "title"]) {
99
+ const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
94
100
  if (config?.scale === previousScale) {
95
- next = next._withGuideConfig(channel, component, {
101
+ next = next._withGuideConfig(definition.guideChannel, component, {
96
102
  ...config,
97
103
  scale: nextScale
98
104
  });
@@ -141,22 +147,12 @@ export function resolveTarget(
141
147
  throw new Error(`Mark "${id}" requires an existing dataset.`);
142
148
  }
143
149
 
144
- const expectedGraphic = {
145
- point: ["circle", "rect", "collection"],
146
- line: "path",
147
- bar: "rect",
148
- area: "path",
149
- rule: "line"
150
- }[layer.mark.type];
150
+ const expectedGraphic = getMarkGraphicTypes(layer.mark.type);
151
151
 
152
152
  const graphicType = program.graphicSpec.objects[id]?.type;
153
- const matches = Array.isArray(expectedGraphic)
154
- ? expectedGraphic.includes(graphicType)
155
- : graphicType === expectedGraphic;
153
+ const matches = expectedGraphic?.includes(graphicType) === true;
156
154
  if (!matches) {
157
- const label = Array.isArray(expectedGraphic)
158
- ? expectedGraphic.join(" or ")
159
- : expectedGraphic;
155
+ const label = expectedGraphic?.join(" or ") ?? "supported";
160
156
  throw new Error(`Mark "${id}" requires ${label} graphics.`);
161
157
  }
162
158
 
@@ -3,7 +3,7 @@ import {
3
3
  normalizeStrokeDashPattern,
4
4
  readNominalField,
5
5
  validateNominalFieldType
6
- } from "../../grammar/scales.js";
6
+ } from "../../grammar/scales/index.js";
7
7
  import { resolveStrokeDashScaleDefinition } from "../scales/definitions.js";
8
8
  import { applyMaterializationPlan } from "../../materialization/dependencies.js";
9
9
  import { planEncodingRematerialization } from "../../materialization/encodings.js";