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
@@ -4,6 +4,7 @@ import { isPlainObject } from "../../core/immutable.js";
4
4
  import {
5
5
  readNominalField,
6
6
  readQuantitativeField,
7
+ readScaleField,
7
8
  readTemporalField,
8
9
  validateSemanticFieldType
9
10
  } from "../../grammar/scales.js";
@@ -85,16 +86,35 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
85
86
  }
86
87
  const fieldType = rule
87
88
  ? validateSemanticFieldType(args.fieldType)
88
- : args.fieldType ?? "quantitative";
89
- if (!rule && fieldType !== "quantitative") {
89
+ : layer.mark.type === "rect"
90
+ ? args.fieldType ?? primary.fieldType
91
+ : args.fieldType ?? "quantitative";
92
+ if (
93
+ !rule &&
94
+ layer.mark.type !== "rect" &&
95
+ fieldType !== "quantitative"
96
+ ) {
90
97
  throw new Error(`${operation} requires a quantitative field.`);
91
98
  }
99
+ if (
100
+ layer.mark.type === "rect" &&
101
+ !["quantitative", "temporal"].includes(fieldType)
102
+ ) {
103
+ throw new Error(`${operation} requires a quantitative or temporal rect field.`);
104
+ }
105
+ if (layer.mark.type === "rect" && fieldType !== primary.fieldType) {
106
+ throw new Error(
107
+ `${operation} fieldType must match the primary ${primaryChannel} fieldType.`
108
+ );
109
+ }
92
110
  if (rule && fieldType !== primary.fieldType) {
93
111
  throw new Error(
94
112
  `${operation} fieldType must match the primary ${primaryChannel} fieldType.`
95
113
  );
96
114
  }
97
- if (hasField) validateSecondaryField(dataset, args.field, fieldType);
115
+ if (hasField && layer.mark.type === "rect") {
116
+ readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true });
117
+ } else if (hasField) validateSecondaryField(dataset, args.field, fieldType);
98
118
  else normalizeRuleDatum(args.datum, fieldType, channel);
99
119
 
100
120
  const previous = layer.encoding?.[channel];
@@ -135,7 +155,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
135
155
  if (rule) return next.rematerializeRuleMark({ id: target });
136
156
  return layer.mark.type === "bar"
137
157
  ? next.rematerializeBarMark({ id: target })
138
- : next.rematerializeAreaMark({ id: target });
158
+ : layer.mark.type === "rect"
159
+ ? next.rematerializeRectMark({ id: target })
160
+ : next.rematerializeAreaMark({ id: target });
139
161
  }
140
162
 
141
163
  const encodeX2 = action(
@@ -149,7 +171,7 @@ const encodeX2 = action(
149
171
  "x2",
150
172
  args,
151
173
  "encodeX2",
152
- ["area", "rule", "bar"]
174
+ ["area", "rule", "bar", "rect"]
153
175
  );
154
176
  }
155
177
  );
@@ -165,7 +187,7 @@ const encodeY2 = action(
165
187
  "y2",
166
188
  args,
167
189
  "encodeY2",
168
- ["area", "rule", "bar"]
190
+ ["area", "rule", "bar", "rect"]
169
191
  );
170
192
  }
171
193
  );
@@ -3,10 +3,14 @@ import { findDataset } from "../../selectors/datasets.js";
3
3
  import { resolveEligibleLayer } from "../../selectors/layers.js";
4
4
  import { hasMaterializedLegend } from "../../materialization/legends.js";
5
5
  import { findSemanticScale } from "../../selectors/scales.js";
6
- import { validateKeys } from "../../core/validation.js";
6
+ import { validateOptionObject } from "../../core/validation.js";
7
+ import {
8
+ getMarkGraphicTypes,
9
+ getPositionChannelDefinition
10
+ } from "../../core/vocabulary.js";
7
11
 
8
12
  export function validateOptions(args, supported, operation) {
9
- validateKeys(args, supported, operation);
13
+ validateOptionObject(args, supported, operation);
10
14
  }
11
15
 
12
16
  export function validateLineSeriesCompatibility(layer, channel, field) {
@@ -65,10 +69,12 @@ export function rebindPositionGuides(
65
69
  nextScale,
66
70
  target
67
71
  ) {
72
+ const definition = getPositionChannelDefinition(channel);
73
+ if (definition?.guideChannel === undefined) return program;
68
74
  if (previousScale === undefined || previousScale === nextScale) return program;
69
75
 
70
- const axis = program.semanticSpec.guides.axis?.[channel];
71
- const direction = channel === "x" ? "vertical" : "horizontal";
76
+ const axis = program.semanticSpec.guides.axis?.[definition.guideChannel];
77
+ const direction = definition.gridDirection;
72
78
  const grid = program.semanticSpec.guides.grid?.[direction];
73
79
  const ownsAxis = axis?.scale === previousScale;
74
80
  const ownsGrid = grid?.scale === previousScale;
@@ -86,13 +92,13 @@ export function rebindPositionGuides(
86
92
  let next = program;
87
93
  if (ownsAxis) {
88
94
  next = next.editSemantic({
89
- property: `guide.axis.${channel}.scale`,
95
+ property: `guide.axis.${definition.guideChannel}.scale`,
90
96
  value: nextScale
91
97
  });
92
- for (const component of ["ticks", "labels", "title"]) {
93
- const config = next.guideConfigs.axis?.[channel]?.[component];
98
+ for (const component of ["line", "ticks", "labels", "title"]) {
99
+ const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
94
100
  if (config?.scale === previousScale) {
95
- next = next._withGuideConfig(channel, component, {
101
+ next = next._withGuideConfig(definition.guideChannel, component, {
96
102
  ...config,
97
103
  scale: nextScale
98
104
  });
@@ -141,22 +147,12 @@ export function resolveTarget(
141
147
  throw new Error(`Mark "${id}" requires an existing dataset.`);
142
148
  }
143
149
 
144
- const expectedGraphic = {
145
- point: ["circle", "rect", "collection"],
146
- line: "path",
147
- bar: "rect",
148
- area: "path",
149
- rule: "line"
150
- }[layer.mark.type];
150
+ const expectedGraphic = getMarkGraphicTypes(layer.mark.type);
151
151
 
152
152
  const graphicType = program.graphicSpec.objects[id]?.type;
153
- const matches = Array.isArray(expectedGraphic)
154
- ? expectedGraphic.includes(graphicType)
155
- : graphicType === expectedGraphic;
153
+ const matches = expectedGraphic?.includes(graphicType) === true;
156
154
  if (!matches) {
157
- const label = Array.isArray(expectedGraphic)
158
- ? expectedGraphic.join(" or ")
159
- : expectedGraphic;
155
+ const label = expectedGraphic?.join(" or ") ?? "supported";
160
156
  throw new Error(`Mark "${id}" requires ${label} graphics.`);
161
157
  }
162
158
 
@@ -0,0 +1,73 @@
1
+ import { action } from "../../core/action.js";
2
+ import { formatTextValue, validateTextFormat } from "../../grammar/text.js";
3
+ import { canMaterializeText } from "../../materialization/marks.js";
4
+ import { findLayer } from "../../selectors/layers.js";
5
+ import { resolveTarget, validateOptions } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze(["target", "field", "value", "format"]);
8
+
9
+ function validateTextField(rows, field, format) {
10
+ if (typeof field !== "string" || field.length === 0) {
11
+ throw new TypeError("encodeText field must be a non-empty string.");
12
+ }
13
+ let found = false;
14
+ for (const row of rows) {
15
+ if (!Object.hasOwn(row, field)) continue;
16
+ found = true;
17
+ formatTextValue(row[field], format);
18
+ }
19
+ if (!found) throw new Error(`Unknown text field "${field}".`);
20
+ }
21
+
22
+ export const encodeText = action(
23
+ {
24
+ op: "encodeText",
25
+ description: "Assign field-driven or constant text content."
26
+ },
27
+ function (args = {}) {
28
+ validateOptions(args, OPTIONS, "encodeText");
29
+ const hasField = Object.hasOwn(args, "field");
30
+ const hasValue = Object.hasOwn(args, "value");
31
+ if (hasField === hasValue) {
32
+ throw new Error("encodeText requires exactly one of field or value.");
33
+ }
34
+ const { id: target, dataset, layer } = resolveTarget(
35
+ this,
36
+ args.target,
37
+ ["text"],
38
+ "text mark"
39
+ );
40
+ const previous = layer.encoding?.text;
41
+ const format = validateTextFormat(args.format ?? previous?.format ?? "auto");
42
+ if (hasField) validateTextField(dataset.values, args.field, format);
43
+ else if (formatTextValue(args.value, format) === undefined) {
44
+ throw new Error("encodeText value must produce non-empty text.");
45
+ }
46
+
47
+ let next = this;
48
+ const alternate = hasField ? "datum" : "field";
49
+ if (Object.hasOwn(previous ?? {}, alternate)) {
50
+ next = next.editSemantic({
51
+ property: `layer[${target}].encoding.text.${alternate}`,
52
+ remove: true
53
+ });
54
+ }
55
+ next = next
56
+ .editSemantic({
57
+ property: `layer[${target}].encoding.text.${hasField ? "field" : "datum"}`,
58
+ value: hasField ? args.field : args.value
59
+ })
60
+ .editSemantic({
61
+ property: `layer[${target}].encoding.text.format`,
62
+ value: format
63
+ });
64
+ const updated = findLayer(next, target);
65
+ return canMaterializeText(next, updated)
66
+ ? next.rematerializeTextMark({ id: target })
67
+ : next;
68
+ }
69
+ );
70
+
71
+ export function registerTextEncodingAction(ProgramClass) {
72
+ ProgramClass.prototype.encodeText = encodeText;
73
+ }
@@ -2,13 +2,10 @@ import { action } from "../../core/action.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
3
  import {
4
4
  validateKeys,
5
- validateNonEmptyString,
6
- validateNonNegativeFinite,
7
- validateUnitInterval
8
5
  } from "../../core/validation.js";
9
6
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
10
7
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
11
- import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
8
+ import { resolveBoundaryAppearance } from "./options.js";
12
9
  import { resolveErrorBand } from "./resolve.js";
13
10
 
14
11
  const OPTIONS = Object.freeze([
@@ -24,35 +21,21 @@ const OPTIONS = Object.freeze([
24
21
  "curve",
25
22
  "boundaries"
26
23
  ]);
27
- const BOUNDARY_OPTIONS = Object.freeze([
28
- "stroke", "strokeWidth", "strokeDash", "opacity", "curve"
29
- ]);
30
-
31
24
  function resolveBoundaries(value, areaCurve) {
32
25
  if (value === undefined || value === false) return undefined;
33
26
  if (!isPlainObject(value)) {
34
27
  throw new TypeError("createErrorBand boundaries must be false or a plain object.");
35
28
  }
36
- validateKeys(value, BOUNDARY_OPTIONS, "createErrorBand boundaries");
37
- const stroke = value.stroke ?? DEFAULT_COLORS.mark;
38
- const strokeWidth = value.strokeWidth ?? 1;
39
- const strokeDash = value.strokeDash ?? "solid";
40
- const opacity = value.opacity ?? 1;
41
- const curve = validateCurveInterpolation(value.curve ?? areaCurve);
42
- validateNonEmptyString(stroke, "Error-band boundary stroke");
43
- validateNonNegativeFinite(
44
- strokeWidth,
45
- "Error-band boundary strokeWidth"
46
- );
47
- const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
48
- validateUnitInterval(opacity, "Error-band boundary opacity");
49
- return {
50
- stroke,
51
- strokeWidth,
52
- strokeDash: resolvedStrokeDash,
53
- opacity,
54
- curve
55
- };
29
+ return resolveBoundaryAppearance(value, {
30
+ defaults: {
31
+ stroke: DEFAULT_COLORS.mark,
32
+ strokeWidth: 1,
33
+ strokeDash: "solid",
34
+ opacity: 1,
35
+ curve: areaCurve
36
+ },
37
+ operation: "createErrorBand boundaries"
38
+ });
56
39
  }
57
40
 
58
41
  function positionArgs(resolved) {
@@ -153,7 +136,32 @@ export const createErrorBand = action(
153
136
  id: `${resolved.id}UpperBoundary`,
154
137
  bound: resolved.fields.upper
155
138
  });
139
+ for (const [id, bound] of [
140
+ [`${resolved.id}LowerBoundary`, "lower"],
141
+ [`${resolved.id}UpperBoundary`, "upper"]
142
+ ]) {
143
+ next = next._withMarkConfig(id, {
144
+ ...next.markConfigs[id],
145
+ ...boundaries,
146
+ errorBandBoundary: { owner: resolved.id, bound }
147
+ });
148
+ }
156
149
  }
157
- return next;
150
+ return next._withMarkConfig(resolved.id, {
151
+ ...next.markConfigs[resolved.id],
152
+ errorBand: {
153
+ data: resolved.dataId,
154
+ orientation: resolved.orientation,
155
+ position: resolved.position,
156
+ coordinate: resolved.coordinate,
157
+ intervalScale: resolved.interval.scale.id ?? resolved.interval.channel,
158
+ positionScale: resolved.position.scale.id ?? resolved.position.channel,
159
+ groupBy: resolved.groupField,
160
+ lowerField: resolved.fields.lower,
161
+ upperField: resolved.fields.upper,
162
+ lowerBoundaryId: `${resolved.id}LowerBoundary`,
163
+ upperBoundaryId: `${resolved.id}UpperBoundary`
164
+ }
165
+ });
158
166
  }
159
167
  );
@@ -0,0 +1,206 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ validateOptionObject,
5
+ validateNonEmptyString,
6
+ validateUnitInterval
7
+ } from "../../core/validation.js";
8
+ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
9
+ import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
10
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
11
+ import {
12
+ ERROR_BAND_BOUNDARY_OPTIONS,
13
+ resolveBoundaryAppearance
14
+ } from "./options.js";
15
+
16
+ const EDIT_OPTIONS = Object.freeze(["target", "fill", "opacity", "curve"]);
17
+ const BOUNDARY_EDIT_OPTIONS = Object.freeze([
18
+ "target", "boundary", ...ERROR_BAND_BOUNDARY_OPTIONS
19
+ ]);
20
+ const REMATERIALIZE_OPTIONS = Object.freeze([
21
+ "id", ...ERROR_BAND_BOUNDARY_OPTIONS
22
+ ]);
23
+
24
+ function resolveOwner(program, requested) {
25
+ const target = requested === undefined
26
+ ? undefined
27
+ : validateUserId(requested, "Error-band id");
28
+ return resolveEligibleLayer(program, {
29
+ target,
30
+ label: "error band",
31
+ predicate: layer =>
32
+ layer.mark?.type === "area" &&
33
+ program.markConfigs[layer.id]?.errorBand !== undefined
34
+ });
35
+ }
36
+
37
+ function currentBoundaryAppearance(program, id) {
38
+ const config = program.markConfigs[id];
39
+ if (findLayer(program, id)?.mark?.type !== "line" || config === undefined) {
40
+ throw new Error(`Unknown error-band boundary "${id}".`);
41
+ }
42
+ return {
43
+ stroke: config.stroke,
44
+ strokeWidth: config.strokeWidth,
45
+ strokeDash: config.strokeDash,
46
+ opacity: config.opacity,
47
+ curve: config.curve ?? "linear"
48
+ };
49
+ }
50
+
51
+ function defaultBoundaryAppearance(program, owner) {
52
+ return {
53
+ stroke: DEFAULT_COLORS.mark,
54
+ strokeWidth: 1,
55
+ strokeDash: "solid",
56
+ opacity: 1,
57
+ curve: program.markConfigs[owner]?.curve ?? "linear"
58
+ };
59
+ }
60
+
61
+ function createBoundary(program, owner, id, bound, appearance) {
62
+ const config = program.markConfigs[owner].errorBand;
63
+ const next = program.createErrorBandBoundary({
64
+ id,
65
+ data: config.data,
66
+ orientation: config.orientation,
67
+ bound,
68
+ position: config.position,
69
+ coordinate: config.coordinate,
70
+ intervalScale: config.intervalScale,
71
+ positionScale: config.positionScale,
72
+ groupBy: config.groupBy,
73
+ ...appearance
74
+ });
75
+ return next._withMarkConfig(id, {
76
+ ...next.markConfigs[id],
77
+ ...appearance,
78
+ errorBandBoundary: {
79
+ owner,
80
+ bound: id === config.lowerBoundaryId ? "lower" : "upper"
81
+ }
82
+ });
83
+ }
84
+
85
+ export const rematerializeErrorBandBoundary = action(
86
+ {
87
+ op: "rematerializeErrorBandBoundary",
88
+ description: "Rematerialize one owned error-band boundary."
89
+ },
90
+ function (args = {}) {
91
+ validateOptionObject(
92
+ args,
93
+ REMATERIALIZE_OPTIONS,
94
+ "rematerializeErrorBandBoundary"
95
+ );
96
+ const id = validateUserId(args.id, "Error-band boundary id");
97
+ currentBoundaryAppearance(this, id);
98
+ const graphic = this.graphicSpec.objects[id];
99
+ const next = this
100
+ ._withMarkConfig(id, {
101
+ ...this.markConfigs[id],
102
+ stroke: args.stroke,
103
+ strokeWidth: args.strokeWidth,
104
+ strokeDash: args.strokeDash,
105
+ opacity: args.opacity,
106
+ curve: args.curve
107
+ })
108
+ .editGraphics({
109
+ target: id,
110
+ property: "strokeDash",
111
+ value: graphic.items.map(() => args.strokeDash)
112
+ });
113
+ return next.rematerializeLineMark({ id });
114
+ }
115
+ );
116
+
117
+ export const editErrorBand = action(
118
+ {
119
+ op: "editErrorBand",
120
+ description: "Edit one error-band body appearance."
121
+ },
122
+ function (args = {}) {
123
+ validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
124
+ if (!["fill", "opacity", "curve"].some(key => Object.hasOwn(args, key))) {
125
+ throw new Error("editErrorBand requires fill, opacity, or curve.");
126
+ }
127
+ const owner = resolveOwner(this, args.target);
128
+ const config = { ...this.markConfigs[owner.id] };
129
+ const errorBand = { ...config.errorBand };
130
+ if (Object.hasOwn(args, "fill")) {
131
+ errorBand.fill = validateNonEmptyString(args.fill, "Error-band fill");
132
+ }
133
+ if (Object.hasOwn(args, "opacity")) {
134
+ config.opacity = validateUnitInterval(args.opacity, "Error-band opacity");
135
+ }
136
+ if (Object.hasOwn(args, "curve")) {
137
+ config.curve = validateCurveInterpolation(args.curve);
138
+ }
139
+ return this
140
+ ._withMarkConfig(owner.id, { ...config, errorBand })
141
+ .rematerializeAreaMark({ id: owner.id });
142
+ }
143
+ );
144
+
145
+ export const editErrorBandBoundary = action(
146
+ {
147
+ op: "editErrorBandBoundary",
148
+ description: "Edit one or both owned error-band boundaries."
149
+ },
150
+ function (args = {}) {
151
+ validateOptionObject(
152
+ args,
153
+ BOUNDARY_EDIT_OPTIONS,
154
+ "editErrorBandBoundary"
155
+ );
156
+ if (!ERROR_BAND_BOUNDARY_OPTIONS.some(key => Object.hasOwn(args, key))) {
157
+ throw new Error("editErrorBandBoundary requires an appearance change.");
158
+ }
159
+ const boundary = args.boundary ?? "both";
160
+ if (!["both", "lower", "upper"].includes(boundary)) {
161
+ throw new Error(`Unsupported error-band boundary "${boundary}".`);
162
+ }
163
+ const owner = resolveOwner(this, args.target);
164
+ const config = this.markConfigs[owner.id].errorBand;
165
+ const patch = Object.fromEntries(
166
+ ERROR_BAND_BOUNDARY_OPTIONS
167
+ .filter(key => Object.hasOwn(args, key))
168
+ .map(key => [key, args[key]])
169
+ );
170
+ const ids = boundary === "both"
171
+ ? [config.lowerBoundaryId, config.upperBoundaryId]
172
+ : [boundary === "lower" ? config.lowerBoundaryId : config.upperBoundaryId];
173
+ const plans = ids.map(id => {
174
+ const existing = findLayer(this, id) !== undefined;
175
+ return {
176
+ id,
177
+ existing,
178
+ bound: id === config.lowerBoundaryId
179
+ ? config.lowerField
180
+ : config.upperField,
181
+ appearance: resolveBoundaryAppearance(patch, {
182
+ defaults: existing
183
+ ? currentBoundaryAppearance(this, id)
184
+ : defaultBoundaryAppearance(this, owner.id),
185
+ operation: "editErrorBandBoundary"
186
+ })
187
+ };
188
+ });
189
+ let next = this;
190
+ for (const plan of plans) {
191
+ next = plan.existing
192
+ ? next.rematerializeErrorBandBoundary({
193
+ id: plan.id,
194
+ ...plan.appearance
195
+ })
196
+ : createBoundary(
197
+ next,
198
+ owner.id,
199
+ plan.id,
200
+ plan.bound,
201
+ plan.appearance
202
+ );
203
+ }
204
+ return next;
205
+ }
206
+ );
@@ -1,7 +1,16 @@
1
1
  import { createErrorBand } from "./create.js";
2
2
  import { createErrorBandBoundary } from "./components.js";
3
+ import {
4
+ editErrorBand,
5
+ editErrorBandBoundary,
6
+ rematerializeErrorBandBoundary
7
+ } from "./edit.js";
3
8
 
4
9
  export function registerErrorBandActions(ProgramClass) {
5
10
  ProgramClass.prototype.createErrorBand = createErrorBand;
6
11
  ProgramClass.prototype.createErrorBandBoundary = createErrorBandBoundary;
12
+ ProgramClass.prototype.editErrorBand = editErrorBand;
13
+ ProgramClass.prototype.editErrorBandBoundary = editErrorBandBoundary;
14
+ ProgramClass.prototype.rematerializeErrorBandBoundary =
15
+ rematerializeErrorBandBoundary;
7
16
  }
@@ -0,0 +1,39 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import {
3
+ validateKeys,
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateUnitInterval
7
+ } from "../../core/validation.js";
8
+ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
9
+ import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
10
+
11
+ export const ERROR_BAND_BOUNDARY_OPTIONS = Object.freeze([
12
+ "stroke", "strokeWidth", "strokeDash", "opacity", "curve"
13
+ ]);
14
+
15
+ export function resolveBoundaryAppearance(value, {
16
+ defaults,
17
+ operation
18
+ }) {
19
+ if (!isPlainObject(value)) {
20
+ throw new TypeError(`${operation} must be a plain object.`);
21
+ }
22
+ validateKeys(value, ERROR_BAND_BOUNDARY_OPTIONS, operation);
23
+ const stroke = value.stroke ?? defaults.stroke;
24
+ const strokeWidth = value.strokeWidth ?? defaults.strokeWidth;
25
+ const strokeDash = value.strokeDash ?? defaults.strokeDash;
26
+ const opacity = value.opacity ?? defaults.opacity;
27
+ const curve = validateCurveInterpolation(value.curve ?? defaults.curve);
28
+ validateNonEmptyString(stroke, `${operation} stroke`);
29
+ validateNonNegativeFinite(strokeWidth, `${operation} strokeWidth`);
30
+ const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
31
+ validateUnitInterval(opacity, `${operation} opacity`);
32
+ return {
33
+ stroke,
34
+ strokeWidth,
35
+ strokeDash: resolvedStrokeDash,
36
+ opacity,
37
+ curve
38
+ };
39
+ }