ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -0,0 +1,115 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateOptionObject } from "../../core/validation.js";
3
+ import {
4
+ normalizeParallelDimensions,
5
+ validateParallelKeyField,
6
+ validateParallelMissingPolicy,
7
+ validateParallelRows
8
+ } from "../../grammar/parallelCoordinates.js";
9
+ import { resolvePositionScaleDefinition } from "../scales/definitions.js";
10
+ import { applyEncodingScale, resolveTarget } from "./shared.js";
11
+ import { resolveParallelCoordinate } from "../coordinates/parallel.js";
12
+
13
+ const OPTIONS = Object.freeze([
14
+ "target", "coordinate", "dimensions", "key", "missing"
15
+ ]);
16
+
17
+ export const encodeParallelCoordinates = action(
18
+ {
19
+ op: "encodeParallelCoordinates",
20
+ description: "Atomically encode ordered Parallel-coordinate dimensions."
21
+ },
22
+ function (args = {}) {
23
+ validateOptionObject(args, OPTIONS, "encodeParallelCoordinates");
24
+ const { id: target, dataset, layer } = resolveTarget(
25
+ this,
26
+ args.target,
27
+ ["line"],
28
+ "line mark"
29
+ );
30
+ if (
31
+ ["x", "y", "theta", "radius"].some(
32
+ channel => layer.encoding?.[channel] !== undefined
33
+ )
34
+ ) {
35
+ throw new Error(
36
+ `Parallel encoding cannot mix with existing position encodings on layer "${target}".`
37
+ );
38
+ }
39
+ const coordinate = resolveParallelCoordinate(this, {
40
+ requested: args.coordinate,
41
+ layer,
42
+ operation: "encodeParallelCoordinates"
43
+ });
44
+ const dimensions = normalizeParallelDimensions(
45
+ dataset.values,
46
+ args.dimensions,
47
+ target
48
+ );
49
+ const key = args.key === undefined
50
+ ? undefined
51
+ : validateParallelKeyField(args.key);
52
+ const missing = validateParallelMissingPolicy(args.missing ?? "break");
53
+ validateParallelRows(dataset.values, dimensions, { key, missing });
54
+
55
+ const definitions = dimensions.map(dimension =>
56
+ resolvePositionScaleDefinition(
57
+ this,
58
+ "y",
59
+ dimension.fieldType,
60
+ { ...dimension.scaleOptions, id: dimension.scale },
61
+ dimension.fieldType === "ordinal" ? { discreteType: "point" } : {}
62
+ )
63
+ );
64
+ const storedDimensions = dimensions.map(({ scaleOptions, ...dimension }) => {
65
+ void scaleOptions;
66
+ return dimension;
67
+ });
68
+
69
+ let next = this;
70
+ if (coordinate.create) {
71
+ next = next.createCoordinate({ id: coordinate.id, type: "parallel" });
72
+ }
73
+ if (layer.coordinate === undefined) {
74
+ next = next.editSemantic({
75
+ property: `layer[${target}].coordinate`,
76
+ value: coordinate.id
77
+ });
78
+ }
79
+ next = next
80
+ .editSemantic({
81
+ property: `layer[${target}].encoding.parallel.dimensions`,
82
+ value: storedDimensions
83
+ })
84
+ .editSemantic({
85
+ property: `layer[${target}].encoding.parallel.missing`,
86
+ value: missing
87
+ });
88
+ if (key === undefined && layer.encoding?.parallel?.key !== undefined) {
89
+ next = next.editSemantic({
90
+ property: `layer[${target}].encoding.parallel.key`,
91
+ remove: true
92
+ });
93
+ } else if (key !== undefined) {
94
+ next = next.editSemantic({
95
+ property: `layer[${target}].encoding.parallel.key`,
96
+ value: key
97
+ });
98
+ }
99
+ for (let index = 0; index < definitions.length; index += 1) {
100
+ next = applyEncodingScale(next, definitions[index], dimensions[index].scaleOptions);
101
+ }
102
+ for (const definition of definitions) {
103
+ next = next.rematerializeScale({
104
+ id: definition.id,
105
+ guides: false,
106
+ marks: false
107
+ });
108
+ }
109
+ return next.rematerializeLineMark({ id: target, scales: false });
110
+ }
111
+ );
112
+
113
+ export function registerParallelEncodingAction(ProgramClass) {
114
+ ProgramClass.prototype.encodeParallelCoordinates = encodeParallelCoordinates;
115
+ }
@@ -0,0 +1,131 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateOptionObject
6
+ } from "../../core/validation.js";
7
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
8
+ import { validatePathOrderDirection } from "../../grammar/pathOrder.js";
9
+ import {
10
+ assertPathOrderCompatible,
11
+ pathOrderCompatibility
12
+ } from "../../materialization/marks/pathOrder.js";
13
+ import {
14
+ canMaterializeArea,
15
+ canMaterializeLine
16
+ } from "../../materialization/marks/index.js";
17
+ import { findLayer } from "../../selectors/layers.js";
18
+
19
+ const ENCODE_OPTIONS = Object.freeze([
20
+ "target", "field", "fieldType", "order"
21
+ ]);
22
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
23
+
24
+ function resolvePathOrderLayer(program, target, { active = false } = {}) {
25
+ const requested = target === undefined
26
+ ? undefined
27
+ : validateUserId(target, "Path order target");
28
+ if (requested !== undefined) {
29
+ const layer = findLayer(program, requested);
30
+ if (layer === undefined) {
31
+ throw new Error(`Unknown path order target "${requested}".`);
32
+ }
33
+ if (active && layer.encoding?.pathOrder === undefined) {
34
+ throw new Error(`Mark "${requested}" has no path order encoding.`);
35
+ }
36
+ assertPathOrderCompatible(
37
+ program,
38
+ layer,
39
+ active ? "removePathOrder" : "encodePathOrder"
40
+ );
41
+ return layer;
42
+ }
43
+ const pathLayers = program.semanticSpec.layers.filter(layer =>
44
+ ["line", "area"].includes(layer.mark?.type)
45
+ );
46
+ const candidates = pathLayers.filter(layer =>
47
+ pathOrderCompatibility(program, layer).compatible &&
48
+ (!active || layer.encoding?.pathOrder !== undefined)
49
+ );
50
+ const current = candidates.find(
51
+ layer => layer.id === program.context.currentMark
52
+ );
53
+ if (current !== undefined) return current;
54
+ if (candidates.length === 1) return candidates[0];
55
+ if (candidates.length > 1) {
56
+ throw new Error("path order target is ambiguous; provide target.");
57
+ }
58
+ if (!active && pathLayers.length === 1) {
59
+ assertPathOrderCompatible(program, pathLayers[0], "encodePathOrder");
60
+ }
61
+ if (active && pathLayers.length === 1) {
62
+ throw new Error(`Mark "${pathLayers[0].id}" has no path order encoding.`);
63
+ }
64
+ throw new Error("path order requires an eligible layer.");
65
+ }
66
+
67
+ function rematerializePath(program, layer) {
68
+ if (layer.mark.type === "line") {
69
+ return canMaterializeLine(program, layer)
70
+ ? program.rematerializeLineMark({ id: layer.id })
71
+ : program;
72
+ }
73
+ return canMaterializeArea(program, layer)
74
+ ? program.rematerializeAreaMark({ id: layer.id })
75
+ : program;
76
+ }
77
+
78
+ const encodePathOrder = action(
79
+ {
80
+ op: "encodePathOrder",
81
+ description: "Order vertices within each Cartesian path series."
82
+ },
83
+ function (args = {}) {
84
+ validateOptionObject(args, ENCODE_OPTIONS, "encodePathOrder");
85
+ const field = validateNonEmptyString(args.field, "Path order field");
86
+ const fieldType = args.fieldType ?? "quantitative";
87
+ if (fieldType !== "quantitative") {
88
+ throw new Error("encodePathOrder requires a quantitative field.");
89
+ }
90
+ const order = validatePathOrderDirection(args.order ?? "ascending");
91
+ const layer = resolvePathOrderLayer(this, args.target);
92
+ const dataset = assertPathOrderCompatible(this, layer, "encodePathOrder");
93
+ readQuantitativeField(dataset.values, field);
94
+
95
+ const next = this
96
+ .editSemantic({
97
+ property: `layer[${layer.id}].encoding.pathOrder.field`,
98
+ value: field
99
+ })
100
+ .editSemantic({
101
+ property: `layer[${layer.id}].encoding.pathOrder.fieldType`,
102
+ value: fieldType
103
+ })
104
+ .editSemantic({
105
+ property: `layer[${layer.id}].encoding.pathOrder.order`,
106
+ value: order
107
+ });
108
+ return rematerializePath(next, findLayer(next, layer.id));
109
+ }
110
+ );
111
+
112
+ const removePathOrder = action(
113
+ {
114
+ op: "removePathOrder",
115
+ description: "Remove explicit path order and restore automatic ordering."
116
+ },
117
+ function (args = {}) {
118
+ validateOptionObject(args, REMOVE_OPTIONS, "removePathOrder");
119
+ const layer = resolvePathOrderLayer(this, args.target, { active: true });
120
+ const next = this.editSemantic({
121
+ property: `layer[${layer.id}].encoding.pathOrder`,
122
+ remove: true
123
+ });
124
+ return rematerializePath(next, findLayer(next, layer.id));
125
+ }
126
+ );
127
+
128
+ export function registerPathOrderEncodingActions(ProgramClass) {
129
+ ProgramClass.prototype.encodePathOrder = encodePathOrder;
130
+ ProgramClass.prototype.removePathOrder = removePathOrder;
131
+ }
@@ -9,6 +9,44 @@ function replaceAlternateBinding(program, target, channel, previous, usesField)
9
9
  : program;
10
10
  }
11
11
 
12
+ function reconcileInheritedRulePosition(program, {
13
+ target,
14
+ channel,
15
+ layer,
16
+ hasField
17
+ }) {
18
+ const config = program.markConfigs[target] ?? {};
19
+ const inherited = config.inheritedPosition;
20
+ if (layer.mark.type !== "rule" || inherited === undefined) return program;
21
+
22
+ let channels = inherited.channels.filter(candidate => candidate !== channel);
23
+ let next = program;
24
+ if (
25
+ !hasField &&
26
+ ["x", "y"].includes(channel) &&
27
+ layer.encoding?.x2 === undefined &&
28
+ layer.encoding?.y2 === undefined
29
+ ) {
30
+ const opposite = channel === "x" ? "y" : "x";
31
+ if (channels.includes(opposite) && layer.encoding?.[opposite] !== undefined) {
32
+ next = next.editSemantic({
33
+ property: `layer[${target}].encoding.${opposite}`,
34
+ remove: true
35
+ });
36
+ channels = channels.filter(candidate => candidate !== opposite);
37
+ }
38
+ }
39
+
40
+ const { inheritedPosition, ...rest } = config;
41
+ void inheritedPosition;
42
+ return next._withMarkConfig(target, channels.length === 0
43
+ ? rest
44
+ : {
45
+ ...rest,
46
+ inheritedPosition: { ...inherited, channels }
47
+ });
48
+ }
49
+
12
50
  function applyBin(program, target, channel, previous, bin) {
13
51
  if (bin === undefined) return program;
14
52
  const [mode] = Object.keys(bin);
@@ -26,9 +64,26 @@ function applyBin(program, target, channel, previous, bin) {
26
64
  });
27
65
  }
28
66
 
67
+ function applyArcThetaWeight(program, target, channel, previous, aggregate, weight) {
68
+ if (channel !== "theta") return program;
69
+ if (aggregate !== "sum") {
70
+ return previous?.weight === undefined
71
+ ? program
72
+ : program.editSemantic({
73
+ property: `layer[${target}].encoding.theta.weight`,
74
+ remove: true
75
+ });
76
+ }
77
+ return program.editSemantic({
78
+ property: `layer[${target}].encoding.theta.weight`,
79
+ value: weight
80
+ });
81
+ }
82
+
29
83
  export function applyPositionSemantics(program, {
30
84
  target,
31
85
  channel,
86
+ layer,
32
87
  previous,
33
88
  field,
34
89
  datum,
@@ -36,10 +91,17 @@ export function applyPositionSemantics(program, {
36
91
  fieldType,
37
92
  bin,
38
93
  aggregate,
39
- stack
94
+ stack,
95
+ weight
40
96
  }) {
41
- let next = replaceAlternateBinding(
42
- program,
97
+ let next = reconcileInheritedRulePosition(program, {
98
+ target,
99
+ channel,
100
+ layer,
101
+ hasField
102
+ });
103
+ next = replaceAlternateBinding(
104
+ next,
43
105
  target,
44
106
  channel,
45
107
  previous,
@@ -62,5 +124,12 @@ export function applyPositionSemantics(program, {
62
124
  value
63
125
  });
64
126
  }
65
- return next;
127
+ return applyArcThetaWeight(
128
+ next,
129
+ target,
130
+ channel,
131
+ previous,
132
+ aggregate,
133
+ weight
134
+ );
66
135
  }
@@ -24,7 +24,8 @@ function encodePosition(program, channel, args, operation) {
24
24
  coordinate,
25
25
  bin,
26
26
  aggregate,
27
- stack
27
+ stack,
28
+ weight
28
29
  } = resolvePositionEncoding(program, channel, args, operation);
29
30
 
30
31
  let next = program
@@ -36,6 +37,7 @@ function encodePosition(program, channel, args, operation) {
36
37
  next = applyPositionSemantics(next, {
37
38
  target,
38
39
  channel,
40
+ layer,
39
41
  previous,
40
42
  field,
41
43
  datum,
@@ -43,7 +45,8 @@ function encodePosition(program, channel, args, operation) {
43
45
  fieldType,
44
46
  bin,
45
47
  aggregate,
46
- stack
48
+ stack,
49
+ weight
47
50
  });
48
51
 
49
52
  next = next.editSemantic({
@@ -71,6 +74,16 @@ function encodePosition(program, channel, args, operation) {
71
74
  }
72
75
  }
73
76
 
77
+ if (layer.mark.type === "rect") {
78
+ const updated = findLayer(next, target);
79
+ const pendingGradient = next.markConfigs[target]?.gradientPlot;
80
+ if (pendingGradient !== undefined && !pendingGradient.materialized) {
81
+ return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
82
+ ? next.materializeGradientPlot({ id: target })
83
+ : next;
84
+ }
85
+ }
86
+
74
87
  const updated = findLayer(next, target);
75
88
  for (const step of getPositionEncodingMaterializationSteps(
76
89
  next,
@@ -1,4 +1,5 @@
1
1
  import { validateAggregate } from "../../../../grammar/aggregate.js";
2
+ import { validateNonEmptyString } from "../../../../core/validation.js";
2
3
  import { emptyPositionPolicy } from "./common.js";
3
4
 
4
5
  export function resolveArcPositionPolicy({ channel, args, fieldType }) {
@@ -12,15 +13,37 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
12
13
  if (args.aggregate !== undefined) {
13
14
  throw new Error("Arc radius encoding does not support aggregate.");
14
15
  }
16
+ if (args.weight !== undefined) {
17
+ throw new Error("Arc radius encoding does not support weight.");
18
+ }
15
19
  return emptyPositionPolicy();
16
20
  }
17
21
  if (!["ordinal", "nominal"].includes(fieldType)) {
18
22
  throw new Error("Arc theta encoding requires an ordinal or nominal field.");
19
23
  }
20
- if (args.aggregate === undefined) return emptyPositionPolicy();
24
+ if (args.aggregate === undefined) {
25
+ if (args.weight !== undefined) {
26
+ throw new Error('Arc theta weight requires aggregate: "sum".');
27
+ }
28
+ return emptyPositionPolicy();
29
+ }
21
30
  const aggregate = validateAggregate(args.aggregate);
22
- if (aggregate !== "count") {
23
- throw new Error('Arc theta aggregate currently supports only "count".');
31
+ if (!["count", "sum"].includes(aggregate)) {
32
+ throw new Error('Arc theta aggregate supports only "count" or "sum".');
33
+ }
34
+ if (aggregate === "count") {
35
+ if (args.weight !== undefined) {
36
+ throw new Error('Arc theta weight requires aggregate: "sum".');
37
+ }
38
+ return { bin: undefined, aggregate, stack: undefined, weight: undefined };
39
+ }
40
+ if (args.weight === undefined) {
41
+ throw new Error('Arc theta aggregate "sum" requires weight.');
24
42
  }
25
- return { bin: undefined, aggregate, stack: undefined };
43
+ return {
44
+ bin: undefined,
45
+ aggregate,
46
+ stack: undefined,
47
+ weight: validateNonEmptyString(args.weight, "Arc theta weight")
48
+ };
26
49
  }
@@ -1,6 +1,37 @@
1
1
  import { validateStack, emptyPositionPolicy } from "./common.js";
2
2
 
3
- export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType }) {
3
+ export function isCategoricalDensityPosition({
4
+ layer,
5
+ dataset,
6
+ channel,
7
+ fieldType,
8
+ field
9
+ }) {
10
+ const density = dataset.transform?.find(transform => transform.type === "density");
11
+ return layer.mark?.type === "area" &&
12
+ ["nominal", "ordinal"].includes(fieldType) &&
13
+ density?.placement?.type === "category" &&
14
+ density.placement.channel === channel &&
15
+ density.placement.categoryField === field;
16
+ }
17
+
18
+ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, field }) {
19
+ const density = dataset.transform?.find(transform => transform.type === "density");
20
+ if (["nominal", "ordinal"].includes(fieldType)) {
21
+ if (
22
+ density?.placement?.type !== "category" ||
23
+ density.placement.channel !== channel ||
24
+ density.placement.categoryField !== field
25
+ ) {
26
+ throw new Error(
27
+ "Categorical area position requires a matching category density placement."
28
+ );
29
+ }
30
+ if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
31
+ throw new Error("Categorical density position does not support aggregate, bin, or stack.");
32
+ }
33
+ return emptyPositionPolicy();
34
+ }
4
35
  if (!["quantitative", "temporal"].includes(fieldType)) {
5
36
  throw new Error(
6
37
  "Area position encoding requires quantitative or temporal fields."
@@ -12,7 +43,7 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType })
12
43
  if (args.stack === undefined) return emptyPositionPolicy();
13
44
  if (
14
45
  channel !== "y" ||
15
- !dataset.transform?.some(transform => transform.type === "density")
46
+ density === undefined
16
47
  ) {
17
48
  throw new Error("Area stack currently requires a density y encoding.");
18
49
  }
@@ -1,6 +1,9 @@
1
1
  import { validatePositionFieldCompatibility } from
2
2
  "../../../../grammar/positionCompatibility.js";
3
- import { resolveAreaPositionPolicy } from "./area.js";
3
+ import {
4
+ isCategoricalDensityPosition,
5
+ resolveAreaPositionPolicy
6
+ } from "./area.js";
4
7
  import { resolveArcPositionPolicy } from "./arc.js";
5
8
  import { resolveBarPositionPolicy } from "./bar.js";
6
9
  import { resolveLinePositionPolicy } from "./line.js";
@@ -20,7 +23,9 @@ const POSITION_POLICIES = Object.freeze({
20
23
 
21
24
  export function resolveMarkPositionPolicy(context) {
22
25
  const { layer, channel, fieldType } = context;
23
- validatePositionFieldCompatibility(layer.mark.type, channel, fieldType);
26
+ if (!isCategoricalDensityPosition(context)) {
27
+ validatePositionFieldCompatibility(layer.mark.type, channel, fieldType);
28
+ }
24
29
  const policy = POSITION_POLICIES[layer.mark.type];
25
30
  if (policy === undefined) {
26
31
  throw new Error(`Unsupported position policy for mark "${layer.mark.type}".`);
@@ -44,9 +44,14 @@ export function resolveLinePositionPolicy({
44
44
  layer.encoding?.y?.aggregate === undefined &&
45
45
  ["quantitative", "temporal"].includes(layer.encoding?.y?.fieldType) &&
46
46
  fieldType === "quantitative";
47
+ const pendingQuantitativePair =
48
+ layer.encoding?.y === undefined && fieldType === "quantitative";
47
49
  if (
48
50
  fieldType !== "temporal" &&
49
- !((regression || interval || directPair) && fieldType === "quantitative")
51
+ !(
52
+ (regression || interval || directPair || pendingQuantitativePair) &&
53
+ fieldType === "quantitative"
54
+ )
50
55
  ) {
51
56
  throw new Error(
52
57
  "Line x encoding requires a temporal field or a compatible derived quantitative field."
@@ -60,10 +65,14 @@ export function resolveLinePositionPolicy({
60
65
  }
61
66
 
62
67
  rejectSharedOptions(args, "y");
68
+ const quantitativePair =
69
+ fieldType === "quantitative" &&
70
+ layer.encoding?.x?.fieldType === "quantitative";
63
71
  const prospectiveDirect =
64
72
  args.aggregate === undefined &&
65
73
  (fieldType === "temporal" ||
66
- (fieldType === "quantitative" && layer.encoding?.x === undefined));
74
+ (fieldType === "quantitative" &&
75
+ (layer.encoding?.x === undefined || quantitativePair)));
67
76
  if (interval || prospectiveDirect) {
68
77
  if (!["quantitative", "temporal"].includes(fieldType)) {
69
78
  throw new Error(
@@ -85,6 +94,13 @@ export function resolveLinePositionPolicy({
85
94
  return emptyPositionPolicy();
86
95
  }
87
96
 
97
+ if (
98
+ layer.encoding?.x !== undefined &&
99
+ layer.encoding.x.fieldType !== "temporal"
100
+ ) {
101
+ throw new Error("Aggregate line y encoding requires a temporal x encoding.");
102
+ }
103
+
88
104
  const aggregate = validateAggregate(args.aggregate);
89
105
  validateAggregateFieldType(aggregate, fieldType);
90
106
  return { bin: undefined, aggregate, stack: undefined };
@@ -21,6 +21,7 @@ import {
21
21
  validateAggregateFieldValues,
22
22
  } from "../../../grammar/aggregate.js";
23
23
  import { normalizeRuleDatum } from "../../../grammar/rules.js";
24
+ import { readArcThetaWeights } from "../../../grammar/arcs.js";
24
25
  import { resolveMarkPositionPolicy } from "./policies/index.js";
25
26
  import {
26
27
  getPositionChannelDefinition,
@@ -29,7 +30,7 @@ import {
29
30
 
30
31
  const POSITION_ENCODING_OPTIONS = Object.freeze([
31
32
  "field", "datum", "target", "fieldType", "scale", "coordinate",
32
- "aggregate", "bin", "stack"
33
+ "aggregate", "bin", "stack", "weight"
33
34
  ]);
34
35
 
35
36
  function validateCoordinateFamily(layer, channel, operation) {
@@ -82,6 +83,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
82
83
  args.target,
83
84
  getPositionChannelDefinition(channel).markTypes
84
85
  );
86
+ if (
87
+ Object.hasOwn(args, "weight") &&
88
+ !(layer.mark.type === "arc" && channel === "theta")
89
+ ) {
90
+ throw new Error(`${operation} weight is supported only for arc theta encoding.`);
91
+ }
85
92
  validateCoordinateFamily(layer, channel, operation);
86
93
  const hasField = Object.hasOwn(args, "field");
87
94
  const hasDatum = Object.hasOwn(args, "datum");
@@ -97,7 +104,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
97
104
  }
98
105
  const previous = layer.encoding?.[channel];
99
106
  const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
100
- layer.mark.type === "arc" && channel === "theta" && args.aggregate === "count"
107
+ layer.mark.type === "arc" && channel === "theta" &&
108
+ ["count", "sum"].includes(args.aggregate)
101
109
  ? "nominal"
102
110
  : "quantitative"
103
111
  );
@@ -117,6 +125,14 @@ export function resolvePositionEncoding(program, channel, args, operation) {
117
125
  effectiveArgs[property] = previous[property];
118
126
  }
119
127
  }
128
+ if (
129
+ !Object.hasOwn(effectiveArgs, "weight") &&
130
+ effectiveArgs.aggregate === "sum" &&
131
+ previous !== undefined &&
132
+ Object.hasOwn(previous, "weight")
133
+ ) {
134
+ effectiveArgs.weight = previous.weight;
135
+ }
120
136
  const policy = resolveMarkPositionPolicy({
121
137
  program,
122
138
  layer,
@@ -179,6 +195,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
179
195
  } else if (aggregateOutput) {
180
196
  validateAggregateFieldType(policy.aggregate, fieldType);
181
197
  validateAggregateFieldValues(dataset.values, field, fieldType);
198
+ } else if (
199
+ layer.mark.type === "arc" &&
200
+ channel === "theta" &&
201
+ policy.aggregate === "sum"
202
+ ) {
203
+ readArcThetaWeights(dataset.values, policy.weight, layer.id);
182
204
  } else if (program.markConfigs[target]?.boxPlot !== undefined) {
183
205
  for (const [index, row] of dataset.values.entries()) {
184
206
  const value = row[field];
@@ -157,7 +157,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
157
157
  ? next.rematerializeBarMark({ id: target })
158
158
  : layer.mark.type === "rect"
159
159
  ? next.rematerializeRectMark({ id: target })
160
- : next.rematerializeAreaMark({ id: target });
160
+ : canMaterializeArea(next, findLayer(next, target))
161
+ ? next.rematerializeAreaMark({ id: target })
162
+ : next;
161
163
  }
162
164
 
163
165
  const encodeX2 = action(