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
@@ -1,7 +1,7 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import {
3
3
  readNominalField,
4
- validateNominalFieldType
4
+ validateCategoricalFieldType
5
5
  } from "../../grammar/scales.js";
6
6
  import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
7
7
  import {
@@ -12,6 +12,7 @@ import {
12
12
  } from "./shared.js";
13
13
  import {
14
14
  resolveBarColorLayout,
15
+ resolveBarChannels,
15
16
  resolveBarGrain
16
17
  } from "../../grammar/bars/policy.js";
17
18
  import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
@@ -21,84 +22,101 @@ import { planEncodingRematerialization } from "../../materialization/encodings.j
21
22
  const ENCODING_OPTIONS = Object.freeze([
22
23
  "field", "target", "fieldType", "scale", "paddingInner", "paddingOuter"
23
24
  ]);
24
- const encodeXOffset = action(
25
- {
26
- op: "encodeXOffset",
27
- description: "Encode a nominal field within each ordinal x band."
28
- },
29
- function (args = {}) {
30
- validateOptions(args, ENCODING_OPTIONS, "encodeXOffset");
31
- const fieldType = validateNominalFieldType(args.fieldType ?? "nominal");
32
- const { id: target, dataset, layer } = resolveTarget(
33
- this,
34
- args.target,
35
- ["bar"],
36
- "bar mark"
37
- );
25
+ function createOffsetEncoding(channel) {
26
+ const operation = channel === "xOffset" ? "encodeXOffset" : "encodeYOffset";
27
+ const parentChannel = channel === "xOffset" ? "x" : "y";
28
+ return action(
29
+ {
30
+ op: operation,
31
+ description: `Encode a categorical field within each ${parentChannel} category band.`
32
+ },
33
+ function (args = {}) {
34
+ validateOptions(args, ENCODING_OPTIONS, operation);
35
+ const fieldType = validateCategoricalFieldType(args.fieldType ?? "nominal");
36
+ const { id: target, dataset, layer } = resolveTarget(
37
+ this,
38
+ args.target,
39
+ ["bar"],
40
+ "bar mark"
41
+ );
42
+
43
+ const channels = resolveBarChannels(layer);
44
+ if (
45
+ resolveBarGrain(layer) === undefined ||
46
+ channels?.category !== parentChannel
47
+ ) {
48
+ throw new Error(
49
+ `${operation} requires a complete bar with a ${parentChannel} category encoding.`
50
+ );
51
+ }
52
+ if (
53
+ layer.encoding?.color !== undefined &&
54
+ (resolveBarColorLayout(layer) !== "group" ||
55
+ layer.encoding.color.field !== args.field)
56
+ ) {
57
+ throw new Error(
58
+ `${operation} field must match a grouped bar color field.`
59
+ );
60
+ }
38
61
 
39
- if (
40
- resolveBarGrain(layer) === undefined ||
41
- layer.encoding.x.scale === undefined
42
- ) {
43
- throw new Error(
44
- "encodeXOffset requires a complete bar x/y encoding."
62
+ readNominalField(dataset.values, args.field);
63
+ const requestedScale = resolveReassignmentScaleOptions(
64
+ layer.encoding?.[channel],
65
+ args.scale ?? {}
45
66
  );
46
- }
47
- if (
48
- layer.encoding?.color !== undefined &&
49
- (resolveBarColorLayout(layer) !== "group" ||
50
- layer.encoding.color.field !== args.field)
51
- ) {
52
- throw new Error(
53
- "encodeXOffset field must match a grouped bar color field."
67
+ const scale = resolveOffsetScaleDefinition(this, requestedScale, channel);
68
+ if (Object.hasOwn(scale, "unknown")) {
69
+ throw new Error(
70
+ `${channel} scale unknown is not supported for grouped bars.`
71
+ );
72
+ }
73
+ const padding = normalizeOffsetPadding(
74
+ args,
75
+ this.markConfigs[target]?.[channel],
76
+ channel
54
77
  );
55
- }
56
78
 
57
- readNominalField(dataset.values, args.field);
58
- const requestedScale = resolveReassignmentScaleOptions(
59
- layer.encoding?.xOffset,
60
- args.scale ?? {}
61
- );
62
- const scale = resolveOffsetScaleDefinition(this, requestedScale);
63
- if (Object.hasOwn(scale, "unknown")) {
64
- throw new Error("xOffset scale unknown is not supported for grouped bars.");
79
+ let next = this
80
+ .editSemantic({
81
+ property: `layer[${target}].encoding.${channel}.field`,
82
+ value: args.field
83
+ })
84
+ .editSemantic({
85
+ property: `layer[${target}].encoding.${channel}.fieldType`,
86
+ value: fieldType
87
+ })
88
+ .editSemantic({
89
+ property: `layer[${target}].encoding.${channel}.scale`,
90
+ value: scale.id
91
+ })
92
+ ._withMarkConfig(target, {
93
+ ...this.markConfigs[target],
94
+ [channel]: padding
95
+ });
96
+ next = applyEncodingScale(next, scale, requestedScale, {
97
+ reassignment: layer.encoding?.[channel]?.scale === scale.id
98
+ });
99
+ if (layer.encoding?.color === undefined) {
100
+ return next
101
+ .rematerializeScale({ id: scale.id })
102
+ .editGraphics({ target, property: "length", value: 0 });
103
+ }
104
+ return applyMaterializationPlan(
105
+ next,
106
+ planEncodingRematerialization(next, {
107
+ target,
108
+ channel,
109
+ scale: scale.id
110
+ })
111
+ );
65
112
  }
66
- const padding = normalizeOffsetPadding(
67
- args,
68
- this.markConfigs[target]?.xOffset
69
- );
113
+ );
114
+ }
70
115
 
71
- let next = this
72
- .editSemantic({
73
- property: `layer[${target}].encoding.xOffset.field`,
74
- value: args.field
75
- })
76
- .editSemantic({
77
- property: `layer[${target}].encoding.xOffset.fieldType`,
78
- value: fieldType
79
- })
80
- .editSemantic({
81
- property: `layer[${target}].encoding.xOffset.scale`,
82
- value: scale.id
83
- })
84
- ._withMarkConfig(target, {
85
- ...this.markConfigs[target],
86
- xOffset: padding
87
- });
88
- next = applyEncodingScale(next, scale, requestedScale, {
89
- reassignment: layer.encoding?.xOffset?.scale === scale.id
90
- });
91
- return applyMaterializationPlan(
92
- next,
93
- planEncodingRematerialization(next, {
94
- target,
95
- channel: "xOffset",
96
- scale: scale.id
97
- })
98
- );
99
- }
100
- );
116
+ const encodeXOffset = createOffsetEncoding("xOffset");
117
+ const encodeYOffset = createOffsetEncoding("yOffset");
101
118
 
102
119
  export function registerOffsetEncodingAction(ProgramClass) {
103
120
  ProgramClass.prototype.encodeXOffset = encodeXOffset;
121
+ ProgramClass.prototype.encodeYOffset = encodeYOffset;
104
122
  }
@@ -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,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."
@@ -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,
@@ -1,11 +1,10 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import {
3
- canMaterializeArea,
4
- canMaterializeLine,
3
+ getPositionEncodingMaterializationSteps
5
4
  } from "../../materialization/marks.js";
6
- import { resolveBarGrain } from "../../grammar/bars/policy.js";
7
5
  import { resolvePositionEncoding } from "./position/resolve.js";
8
6
  import { findLayer } from "../../selectors/layers.js";
7
+ import { applyPositionSemantics } from "./position/apply.js";
9
8
  import {
10
9
  applyEncodingScale,
11
10
  rebindPositionGuides
@@ -34,69 +33,18 @@ function encodePosition(program, channel, args, operation) {
34
33
  type: coordinate.type,
35
34
  layers: [target]
36
35
  });
37
- if (previous !== undefined) {
38
- const alternate = hasField ? "datum" : "field";
39
- if (Object.hasOwn(previous, alternate)) {
40
- next = next.editSemantic({
41
- property: `layer[${target}].encoding.${channel}.${alternate}`,
42
- remove: true
43
- });
44
- }
45
- }
46
- next = next
47
- .editSemantic({
48
- property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
49
- value: hasField ? field : datum
50
- })
51
- .editSemantic({
52
- property: `layer[${target}].encoding.${channel}.fieldType`,
53
- value: fieldType
54
- });
55
-
56
- if (layer.mark.type === "line" && channel === "y" && args.aggregate !== undefined) {
57
- next = next.editSemantic({
58
- property: `layer[${target}].encoding.y.aggregate`,
59
- value: aggregate
60
- });
61
- }
62
-
63
- if (layer.mark.type === "bar" && channel === "x" && bin !== undefined) {
64
- const [mode] = Object.keys(bin);
65
- const previousModes = Object.keys(layer.encoding?.x?.bin ?? {});
66
- for (const previousMode of previousModes) {
67
- if (previousMode === mode) continue;
68
- next = next.editSemantic({
69
- property: `layer[${target}].encoding.x.bin.${previousMode}`,
70
- remove: true
71
- });
72
- }
73
- next = next.editSemantic({
74
- property: `layer[${target}].encoding.x.bin.${mode}`,
75
- value: bin[mode]
76
- });
77
- }
78
-
79
- if (layer.mark.type === "bar") {
80
- if (aggregate !== undefined) {
81
- next = next.editSemantic({
82
- property: `layer[${target}].encoding.${channel}.aggregate`,
83
- value: aggregate
84
- });
85
- }
86
- if (stack !== undefined) {
87
- next = next.editSemantic({
88
- property: `layer[${target}].encoding.${channel}.stack`,
89
- value: stack
90
- });
91
- }
92
- }
93
-
94
- if (layer.mark.type === "area" && channel === "y" && stack !== undefined) {
95
- next = next.editSemantic({
96
- property: `layer[${target}].encoding.y.stack`,
97
- value: stack
98
- });
99
- }
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
+ });
100
48
 
101
49
  next = next.editSemantic({
102
50
  property: `layer[${target}].encoding.${channel}.scale`,
@@ -113,17 +61,6 @@ function encodePosition(program, channel, args, operation) {
113
61
  target
114
62
  );
115
63
 
116
- if (layer.mark.type === "line") {
117
- const updated = findLayer(next, target);
118
- return canMaterializeLine(next, updated)
119
- ? next.rematerializeLineMark({ id: target })
120
- : next.rematerializeScale({ id: scale.id });
121
- }
122
-
123
- if (layer.mark.type === "rule") {
124
- return next.rematerializeRuleMark({ id: target });
125
- }
126
-
127
64
  if (layer.mark.type === "bar") {
128
65
  const updated = findLayer(next, target);
129
66
  const pendingBox = next.markConfigs[target]?.boxPlot;
@@ -132,22 +69,17 @@ function encodePosition(program, channel, args, operation) {
132
69
  ? next.materializeBoxPlot({ id: target })
133
70
  : next;
134
71
  }
135
- const materialized = resolveBarGrain(updated) !== undefined
136
- ? next.rematerializeBarMark({ id: target })
137
- : next.rematerializeScale({ id: scale.id });
138
- return materialized;
139
72
  }
140
73
 
141
- next = next.rematerializeScale({ id: scale.id });
142
- if (layer.mark.type === "area") {
143
- const updated = findLayer(next, target);
144
- return canMaterializeArea(next, updated)
145
- ? next.rematerializeAreaMark({ id: target })
146
- : next;
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);
147
81
  }
148
- return layer.mark.type === "point"
149
- ? next.rematerializePointMark({ id: target })
150
- : next;
82
+ return next;
151
83
  }
152
84
 
153
85
  const encodeX = action(
@@ -170,7 +102,29 @@ const encodeY = action(
170
102
  }
171
103
  );
172
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
+
173
125
  export function registerPositionEncodingActions(ProgramClass) {
174
126
  ProgramClass.prototype.encodeX = encodeX;
175
127
  ProgramClass.prototype.encodeY = encodeY;
128
+ ProgramClass.prototype.encodeTheta = encodeTheta;
129
+ ProgramClass.prototype.encodeR = encodeR;
176
130
  }