ggaction 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
@@ -0,0 +1,73 @@
1
+ import { action } from "../../core/action.js";
2
+ import { formatTextValue, validateTextFormat } from "../../grammar/text.js";
3
+ import { canMaterializeText } from "../../materialization/marks/index.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/index.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
+ }
@@ -1,15 +1,8 @@
1
1
  import { action } from "../../core/action.js";
2
- import { validateKeys, validatePositiveFinite } from "../../core/validation.js";
3
- import {
4
- normalizeStrokeDashPattern,
5
- validateOpacityValue
6
- } from "../../grammar/scales.js";
7
- import {
8
- validateRuleStroke,
9
- validateRuleStrokeWidth
10
- } from "../../grammar/ruleAppearance.js";
2
+ import { validateKeys } from "../../core/validation.js";
11
3
  import { findLayer } from "../../selectors/layers.js";
12
4
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
5
+ import { resolveErrorBarAppearance } from "./options.js";
13
6
  import { resolveErrorBar } from "./resolve.js";
14
7
 
15
8
  const OPTIONS = Object.freeze([
@@ -29,24 +22,22 @@ const OPTIONS = Object.freeze([
29
22
  ]);
30
23
 
31
24
  function resolveAppearance(args) {
32
- const caps = args.caps ?? true;
33
- if (typeof caps !== "boolean") {
34
- throw new TypeError("createErrorBar caps must be a boolean.");
35
- }
36
- const capSize = args.capSize ?? 8;
37
- validatePositiveFinite(capSize, "createErrorBar capSize");
38
- const stroke = validateRuleStroke(
39
- args.stroke ?? DEFAULT_COLORS.mark,
40
- "createErrorBar stroke"
41
- );
42
- const strokeWidth = validateRuleStrokeWidth(
43
- args.strokeWidth ?? 1.5,
44
- "createErrorBar strokeWidth"
25
+ const appearanceArgs = Object.fromEntries(
26
+ ["caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"]
27
+ .filter(key => Object.hasOwn(args, key))
28
+ .map(key => [key, args[key]])
45
29
  );
46
- const strokeDash = args.strokeDash ?? "solid";
47
- normalizeStrokeDashPattern(strokeDash);
48
- const opacity = validateOpacityValue(args.opacity ?? 1, "createErrorBar opacity");
49
- return { caps, capSize, stroke, strokeWidth, strokeDash, opacity };
30
+ return resolveErrorBarAppearance(appearanceArgs, {
31
+ defaults: {
32
+ caps: true,
33
+ capSize: 8,
34
+ stroke: DEFAULT_COLORS.mark,
35
+ strokeWidth: 1.5,
36
+ strokeDash: "solid",
37
+ opacity: 1
38
+ },
39
+ operation: "createErrorBar"
40
+ });
50
41
  }
51
42
 
52
43
  function positionArgs(resolved) {
@@ -115,30 +106,46 @@ export const createErrorBar = action(
115
106
  .encodeStrokeDash({ target: resolved.id, value: appearance.strokeDash })
116
107
  .encodeOpacity({ target: resolved.id, value: appearance.opacity });
117
108
 
118
- if (!appearance.caps) return next;
119
-
120
109
  const intervalLayer = findLayer(next, resolved.id);
121
- for (const [id, field] of [
122
- [resolved.lowerCapId, resolved.fields.lower],
123
- [resolved.upperCapId, resolved.fields.upper]
124
- ]) {
125
- next = next.createErrorBarCap({
126
- id,
110
+ if (appearance.caps) {
111
+ for (const [id, field] of [
112
+ [resolved.lowerCapId, resolved.fields.lower],
113
+ [resolved.upperCapId, resolved.fields.upper]
114
+ ]) {
115
+ next = next.createErrorBarCap({
116
+ id,
117
+ data: resolved.dataId,
118
+ orientation: resolved.orientation,
119
+ positionField: resolved.position.field,
120
+ positionFieldType: resolved.position.fieldType,
121
+ intervalField: field,
122
+ coordinate: resolved.coordinate,
123
+ positionScale: intervalLayer.encoding[resolved.position.channel].scale,
124
+ intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
125
+ capSize: appearance.capSize,
126
+ stroke: appearance.stroke,
127
+ strokeWidth: appearance.strokeWidth,
128
+ strokeDash: appearance.strokeDash,
129
+ opacity: appearance.opacity
130
+ });
131
+ }
132
+ }
133
+ return next._withMarkConfig(resolved.id, {
134
+ ...next.markConfigs[resolved.id],
135
+ errorBar: {
127
136
  data: resolved.dataId,
128
137
  orientation: resolved.orientation,
129
138
  positionField: resolved.position.field,
130
139
  positionFieldType: resolved.position.fieldType,
131
- intervalField: field,
140
+ lowerField: resolved.fields.lower,
141
+ upperField: resolved.fields.upper,
132
142
  coordinate: resolved.coordinate,
133
143
  positionScale: intervalLayer.encoding[resolved.position.channel].scale,
134
144
  intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
135
- capSize: appearance.capSize,
136
- stroke: appearance.stroke,
137
- strokeWidth: appearance.strokeWidth,
138
- strokeDash: appearance.strokeDash,
139
- opacity: appearance.opacity
140
- });
141
- }
142
- return next;
145
+ lowerCapId: resolved.lowerCapId,
146
+ upperCapId: resolved.upperCapId,
147
+ ...appearance
148
+ }
149
+ });
143
150
  }
144
151
  );
@@ -0,0 +1,130 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
5
+ import {
6
+ ERROR_BAR_EDIT_OPTIONS,
7
+ resolveErrorBarAppearance
8
+ } from "./options.js";
9
+
10
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
11
+
12
+ function resolveOwner(program, requested) {
13
+ const target = requested === undefined
14
+ ? undefined
15
+ : validateUserId(requested, "Error-bar id");
16
+ return resolveEligibleLayer(program, {
17
+ target,
18
+ label: "error bar",
19
+ predicate: layer =>
20
+ layer.mark?.type === "rule" &&
21
+ program.markConfigs[layer.id]?.errorBar !== undefined
22
+ });
23
+ }
24
+
25
+ function removeCap(program, id) {
26
+ if (findLayer(program, id) === undefined) return program;
27
+ return program
28
+ .editSemantic({ property: `layer[${id}]`, remove: true })
29
+ .editGraphics({ target: id, remove: true })
30
+ ._withoutMaterializationConfig(["marks", id]);
31
+ }
32
+
33
+ function capArgs(config, id, intervalField) {
34
+ return {
35
+ id,
36
+ data: config.data,
37
+ orientation: config.orientation,
38
+ positionField: config.positionField,
39
+ positionFieldType: config.positionFieldType,
40
+ intervalField,
41
+ coordinate: config.coordinate,
42
+ positionScale: config.positionScale,
43
+ intervalScale: config.intervalScale,
44
+ capSize: config.capSize,
45
+ stroke: config.stroke,
46
+ strokeWidth: config.strokeWidth,
47
+ strokeDash: config.strokeDash,
48
+ opacity: config.opacity
49
+ };
50
+ }
51
+
52
+ function rematerializeRuleAppearance(program, id, config, fixedSpan) {
53
+ return program
54
+ .editSemantic({
55
+ property: `layer[${id}].encoding.strokeDash.datum`,
56
+ value: config.strokeDash
57
+ })
58
+ ._withMarkConfig(id, {
59
+ ...program.markConfigs[id],
60
+ stroke: config.stroke,
61
+ strokeWidth: config.strokeWidth,
62
+ strokeDash: config.strokeDash,
63
+ opacity: config.opacity,
64
+ ...(fixedSpan === undefined ? {} : { fixedSpan })
65
+ })
66
+ .rematerializeRuleMark({ id });
67
+ }
68
+
69
+ export const rematerializeErrorBar = action(
70
+ {
71
+ op: "rematerializeErrorBar",
72
+ description: "Reconcile one error bar and its owned caps."
73
+ },
74
+ function (args = {}) {
75
+ validateKeys(args, REMATERIALIZE_OPTIONS, "rematerializeErrorBar");
76
+ const id = validateUserId(args.id, "Error-bar id");
77
+ const layer = findLayer(this, id);
78
+ const config = this.markConfigs[id]?.errorBar;
79
+ if (layer?.mark?.type !== "rule" || config === undefined) {
80
+ throw new Error(`Unknown error bar "${id}".`);
81
+ }
82
+ let next = rematerializeRuleAppearance(this, id, config);
83
+ const capDefinitions = [
84
+ [config.lowerCapId, config.lowerField],
85
+ [config.upperCapId, config.upperField]
86
+ ];
87
+ if (!config.caps) {
88
+ for (const [capId] of capDefinitions) next = removeCap(next, capId);
89
+ return next;
90
+ }
91
+ const span = {
92
+ orientation: config.orientation === "vertical" ? "horizontal" : "vertical",
93
+ size: config.capSize
94
+ };
95
+ for (const [capId, field] of capDefinitions) {
96
+ if (findLayer(next, capId) === undefined) {
97
+ next = next.createErrorBarCap(capArgs(config, capId, field));
98
+ } else {
99
+ next = rematerializeRuleAppearance(next, capId, config, span);
100
+ }
101
+ }
102
+ return next;
103
+ }
104
+ );
105
+
106
+ export const editErrorBar = action(
107
+ {
108
+ op: "editErrorBar",
109
+ description: "Edit one error bar and its owned cap appearance."
110
+ },
111
+ function (args = {}) {
112
+ validateKeys(args, ERROR_BAR_EDIT_OPTIONS, "editErrorBar");
113
+ const editable = ERROR_BAR_EDIT_OPTIONS.filter(option => option !== "target");
114
+ if (!editable.some(option => Object.hasOwn(args, option))) {
115
+ throw new Error("editErrorBar requires at least one appearance change.");
116
+ }
117
+ const owner = resolveOwner(this, args.target);
118
+ const current = this.markConfigs[owner.id].errorBar;
119
+ const appearance = resolveErrorBarAppearance(args, {
120
+ defaults: current,
121
+ operation: "editErrorBar"
122
+ });
123
+ return this
124
+ ._withMarkConfig(owner.id, {
125
+ ...this.markConfigs[owner.id],
126
+ errorBar: { ...current, ...appearance }
127
+ })
128
+ .rematerializeErrorBar({ id: owner.id });
129
+ }
130
+ );