ggaction 0.0.4 → 0.0.5

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 (157) hide show
  1. package/CHANGELOG.md +30 -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 +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  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/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -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(
@@ -3,9 +3,22 @@ import {
3
3
  validateRuleStroke,
4
4
  validateRuleStrokeWidth
5
5
  } from "../../grammar/ruleAppearance.js";
6
- import { resolveTarget, validateOptions } from "./shared.js";
6
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
7
+ import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
8
+ import {
9
+ applyEncodingScale,
10
+ resolveReassignmentScaleOptions,
11
+ resolveTarget,
12
+ validateOptions
13
+ } from "./shared.js";
14
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
15
+ import { planEncodingRematerialization } from "../../materialization/encodings.js";
16
+ import { findLayer } from "../../selectors/layers.js";
7
17
 
8
- const OPTIONS = Object.freeze(["target", "value"]);
18
+ const STROKE_OPTIONS = Object.freeze(["target", "value"]);
19
+ const WIDTH_OPTIONS = Object.freeze([
20
+ "target", "value", "field", "fieldType", "scale"
21
+ ]);
9
22
 
10
23
  const encodeStroke = action(
11
24
  {
@@ -13,7 +26,7 @@ const encodeStroke = action(
13
26
  description: "Set a constant graphical stroke on a rule mark."
14
27
  },
15
28
  function (args = {}) {
16
- validateOptions(args, OPTIONS, "encodeStroke");
29
+ validateOptions(args, STROKE_OPTIONS, "encodeStroke");
17
30
  const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
18
31
  return this
19
32
  ._withMarkConfig(id, {
@@ -30,14 +43,79 @@ const encodeStrokeWidth = action(
30
43
  description: "Set a constant graphical stroke width on a rule mark."
31
44
  },
32
45
  function (args = {}) {
33
- validateOptions(args, OPTIONS, "encodeStrokeWidth");
34
- const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
35
- return this
36
- ._withMarkConfig(id, {
37
- ...this.markConfigs[id],
38
- strokeWidth: validateRuleStrokeWidth(args.value)
46
+ validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
47
+ const hasValue = Object.hasOwn(args, "value");
48
+ const hasField = Object.hasOwn(args, "field");
49
+ if (hasValue === hasField) {
50
+ throw new Error("encodeStrokeWidth requires exactly one of value or field.");
51
+ }
52
+ if (hasValue) {
53
+ const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
54
+ let next = this.guideConfigs.legend?.strokeWidth?.target === id
55
+ ? this.removeLegend({ target: id })
56
+ : this;
57
+ if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
58
+ next = next.editSemantic({
59
+ property: `layer[${id}].encoding.strokeWidth`,
60
+ remove: true
61
+ });
62
+ }
63
+ return next
64
+ ._withMarkConfig(id, {
65
+ ...next.markConfigs[id],
66
+ strokeWidth: validateRuleStrokeWidth(args.value)
67
+ })
68
+ .rematerializeRuleMark({ id });
69
+ }
70
+ const { id, dataset, layer } = resolveTarget(
71
+ this,
72
+ args.target,
73
+ ["rule", "line"],
74
+ "rule or line mark"
75
+ );
76
+ const fieldType = args.fieldType ?? "quantitative";
77
+ if (fieldType !== "quantitative") {
78
+ throw new Error("encodeStrokeWidth requires a quantitative field.");
79
+ }
80
+ const values = readQuantitativeField(dataset.values, args.field);
81
+ if (values.some(value => value < 0)) {
82
+ throw new RangeError(
83
+ `encodeStrokeWidth field "${args.field}" cannot contain negative values.`
84
+ );
85
+ }
86
+ const previous = layer.encoding?.strokeWidth;
87
+ const requestedScale = resolveReassignmentScaleOptions(
88
+ previous,
89
+ args.scale ?? {}
90
+ );
91
+ const scale = resolveStrokeWidthScaleDefinition(this, requestedScale);
92
+ const { strokeWidth, ...config } = this.markConfigs[id] ?? {};
93
+ void strokeWidth;
94
+ let next = this
95
+ ._withMarkConfig(id, config)
96
+ .editSemantic({
97
+ property: `layer[${id}].encoding.strokeWidth.field`,
98
+ value: args.field
39
99
  })
40
- .rematerializeRuleMark({ id });
100
+ .editSemantic({
101
+ property: `layer[${id}].encoding.strokeWidth.fieldType`,
102
+ value: fieldType
103
+ })
104
+ .editSemantic({
105
+ property: `layer[${id}].encoding.strokeWidth.scale`,
106
+ value: scale.id
107
+ });
108
+ next = applyEncodingScale(next, scale, requestedScale, {
109
+ reassignment: previous?.scale === scale.id
110
+ });
111
+ return applyMaterializationPlan(
112
+ next,
113
+ planEncodingRematerialization(next, {
114
+ target: id,
115
+ channel: "strokeWidth",
116
+ scale: scale.id
117
+ })
118
+ );
41
119
  }
42
120
  );
43
121
 
@@ -14,7 +14,7 @@ import { compositionChildDescriptor } from
14
14
  "../../materialization/composition.js";
15
15
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
16
16
  import { deriveFacetChildren } from "./derive.js";
17
- import { rebindLayerData, replayDerivedData } from "./replay.js";
17
+ import { replayDerivedData } from "./replay.js";
18
18
  import { composeFacetGuides } from "./guides.js";
19
19
 
20
20
  const FACET_OPTIONS = Object.freeze([
@@ -155,7 +155,6 @@ export const editFacetHeaders = action(
155
155
 
156
156
  export function registerFacetActions(ProgramClass) {
157
157
  ProgramClass.prototype.replayDerivedData = replayDerivedData;
158
- ProgramClass.prototype.rebindLayerData = rebindLayerData;
159
158
  ProgramClass.prototype.composeFacetGuides = composeFacetGuides;
160
159
  ProgramClass.prototype.facet = facet;
161
160
  ProgramClass.prototype.editFacetHeaders = editFacetHeaders;
@@ -12,6 +12,7 @@ import { buildMaterializationPlan } from "../../materialization/planner.js";
12
12
  import { requireDataset } from "../../selectors/datasets.js";
13
13
  import { requireLayer } from "../../selectors/layers.js";
14
14
  import { findSemanticScale } from "../../selectors/scales.js";
15
+ import { findTransformPolicy } from "../../grammar/transforms.js";
15
16
 
16
17
  function sharedHistogramBoundaries(program) {
17
18
  const boundaries = new Map();
@@ -59,6 +60,23 @@ function replayDatasetId(cell, owner) {
59
60
  return `${cell.id}-${owner}-data`;
60
61
  }
61
62
 
63
+ function rebindReplayedGradientPlots(child, base, datasets) {
64
+ let next = child;
65
+ for (const [id, markConfig] of Object.entries(base.markConfigs)) {
66
+ const gradientPlot = markConfig.gradientPlot;
67
+ if (gradientPlot?.materialized !== true) continue;
68
+ const profile = datasets.get(gradientPlot.profileId);
69
+ const source = datasets.get(gradientPlot.source);
70
+ if (profile === undefined || source === undefined) {
71
+ throw new Error(
72
+ `Facet gradient plot "${id}" is missing replayed profile dependencies.`
73
+ );
74
+ }
75
+ next = next.rebindGradientPlotProfile({ id, profile, source });
76
+ }
77
+ return next;
78
+ }
79
+
62
80
  function cellMaterializationPlan(program) {
63
81
  const scaleIds = [...new Set(program.semanticSpec.layers.flatMap(layer =>
64
82
  Object.values(layer.encoding ?? {})
@@ -72,7 +90,13 @@ function cellMaterializationPlan(program) {
72
90
  return buildMaterializationPlan({ scales });
73
91
  }
74
92
 
75
- function deriveCellProgram(base, definition, cell, histogramBoundaries) {
93
+ function deriveCellProgram(
94
+ base,
95
+ definition,
96
+ cell,
97
+ histogramBoundaries,
98
+ scales
99
+ ) {
76
100
  let child = base.filterData({
77
101
  id: cell.data,
78
102
  source: definition.data,
@@ -88,10 +112,15 @@ function deriveCellProgram(base, definition, cell, histogramBoundaries) {
88
112
  );
89
113
  }
90
114
  const id = replayDatasetId(cell, replay.id);
115
+ const policy = findTransformPolicy(replay.transform.type);
116
+ const transform = policy?.facetReplayTransform?.(
117
+ replay.transform,
118
+ { scales }
119
+ ) ?? replay.transform;
91
120
  child = child.replayDerivedData({
92
121
  id,
93
122
  source,
94
- transform: replay.transform
123
+ transform
95
124
  });
96
125
  datasets.set(replay.id, id);
97
126
  }
@@ -109,6 +138,7 @@ function deriveCellProgram(base, definition, cell, histogramBoundaries) {
109
138
  histogramBoundaries.get(layer.id)
110
139
  );
111
140
  }
141
+ child = rebindReplayedGradientPlots(child, base, datasets);
112
142
  return applyMaterializationPlan(child, cellMaterializationPlan(child));
113
143
  }
114
144
 
@@ -163,7 +193,7 @@ export function deriveFacetChildren(
163
193
  : new Map();
164
194
  const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
165
195
  cell.id,
166
- deriveCellProgram(template, definition, cell, bins)
196
+ deriveCellProgram(template, definition, cell, bins, resolutionRequest)
167
197
  ]));
168
198
  const resolution = resolveFacetScaleDomains(
169
199
  template.semanticSpec,
@@ -2,33 +2,25 @@ import { action } from "../../core/action.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateUserId } from "../../core/identifiers.js";
4
4
  import { validateKeys } from "../../core/validation.js";
5
+ import { findTransformPolicy } from "../../grammar/transforms.js";
5
6
  import { hasDataset } from "../../selectors/datasets.js";
6
- import { requireLayer } from "../../selectors/layers.js";
7
7
 
8
8
  const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
9
- const REBIND_OPTIONS = Object.freeze(["id", "data"]);
10
- const MATERIALIZERS = Object.freeze({
11
- filter: "materializeFilteredData",
12
- regression: "materializeRegressionData",
13
- density: "materializeDensityData",
14
- interval: "materializeIntervalData",
15
- boxSummary: "materializeBoxSummaryData",
16
- boxOutlier: "materializeBoxOutlierData"
17
- });
18
9
 
19
10
  function requestedTransform(transform) {
20
11
  if (!isPlainObject(transform)) {
21
12
  throw new TypeError("replayDerivedData transform must be a plain object.");
22
13
  }
23
- const materialize = MATERIALIZERS[transform.type];
24
- if (materialize === undefined) {
14
+ const policy = findTransformPolicy(transform.type);
15
+ if (policy?.facetTopology === undefined) {
25
16
  throw new Error(
26
17
  `replayDerivedData does not support transform "${transform.type ?? "unknown"}".`
27
18
  );
28
19
  }
29
- if (transform.type !== "density") return { transform, materialize };
30
- const { resolved: _resolved, ...requested } = transform;
31
- return { transform: requested, materialize };
20
+ return {
21
+ transform: policy.replayTransform?.(transform) ?? transform,
22
+ materialize: policy.materializeOp
23
+ };
32
24
  }
33
25
 
34
26
  export const replayDerivedData = action(
@@ -46,24 +38,3 @@ export const replayDerivedData = action(
46
38
  [resolved.materialize]({ id });
47
39
  }
48
40
  );
49
-
50
- export const rebindLayerData = action(
51
- {
52
- op: "rebindLayerData",
53
- description: "Rebind one facet-cell layer to its replayed dataset."
54
- },
55
- function (args = {}) {
56
- validateKeys(args, REBIND_OPTIONS, "rebindLayerData");
57
- const id = validateUserId(args.id, "Facet layer id");
58
- const data = validateUserId(args.data, "Facet layer dataset id");
59
- requireLayer(this, id);
60
- if (!hasDataset(this, data)) {
61
- throw new Error(`Facet layer dataset "${data}" does not exist.`);
62
- }
63
- return this.editSemantic({
64
- property: `layer[${id}].data`,
65
- value: data
66
- });
67
- }
68
- );
69
-