ggaction 0.0.11 → 0.0.13

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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +79 -24
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +153 -11
  83. package/src/actions/errorBars/edit.js +365 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +138 -14
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +60 -4
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -1,3 +1,5 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
2
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
3
  import { action } from "../../../core/action.js";
2
4
  import { validateUserId } from "../../../core/identifiers.js";
3
5
  import {
@@ -40,7 +42,7 @@ function resolveLineRange(resolvedScale, bounds, channel) {
40
42
 
41
43
  function resolveGeometry(program, channel, scaleId, position) {
42
44
  const hasConsumer = program.semanticSpec.layers.some(
43
- layer => layer.encoding?.[channel]?.scale === scaleId
45
+ layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
44
46
  );
45
47
 
46
48
  if (!hasConsumer) {
@@ -84,7 +86,7 @@ function createEditAxisLine(channel) {
84
86
 
85
87
  return action(
86
88
  { op: operation, description: `Edit the concrete ${channel}-axis line.` },
87
- function (args = {}) {
89
+ withGuideLayoutValidation(function (args = {}) {
88
90
  validateKeys(args, EDIT_OPTIONS, operation);
89
91
  const { graphic } = axisIds(channel);
90
92
  const line = this.graphicSpec.objects[graphic];
@@ -116,7 +118,7 @@ function createEditAxisLine(channel) {
116
118
  return next
117
119
  .editGraphics({ target: graphic, property: "stroke", value: color })
118
120
  .editGraphics({ target: graphic, property: "strokeWidth", value: lineWidth });
119
- }
121
+ })
120
122
  );
121
123
  }
122
124
 
@@ -129,7 +131,7 @@ function createAxisLine(channel) {
129
131
 
130
132
  return action(
131
133
  { op: operation, description: `Create the concrete ${channel}-axis line.` },
132
- function (args = {}) {
134
+ withGuideLayoutValidation(function (args = {}) {
133
135
  validateKeys(args, CREATE_OPTIONS, operation);
134
136
  const scale = validateUserId(args.scale ?? channel, "Scale id");
135
137
  const position = validateAxisPosition(
@@ -154,7 +156,7 @@ function createAxisLine(channel) {
154
156
  ...resolvePlotGraphicPlacement(this)
155
157
  })
156
158
  [editOperation]({ position, color, lineWidth });
157
- }
159
+ })
158
160
  );
159
161
  }
160
162
 
@@ -0,0 +1,117 @@
1
+ import { action } from "../../../../core/action.js";
2
+ import { validateNonEmptyString, validateOptionObject } from "../../../../core/validation.js";
3
+ import { requireParallelAxisLayer, resolveParallelAxisTarget } from "./resolve.js";
4
+ import {
5
+ defaultParallelAxis, hasParallelAxisParts, PARALLEL_AXIS_GRAPHICS,
6
+ PARALLEL_AXIS_PARTS, patchParallelAxis, resolveParallelAxisConfigs, validateParallelAxisOptions
7
+ } from "./policy.js";
8
+
9
+ function owner(program, args, operation, existing = false) {
10
+ const stored = program.semanticSpec.guides.axis?.parallel;
11
+ if (existing && stored === undefined) throw new Error(`${operation} requires existing Parallel axes.`);
12
+ const target = resolveParallelAxisTarget(program, args.target === undefined ? stored?.target : args.target);
13
+ if (stored !== undefined && stored.target !== target) throw new Error("Parallel axes belong to another target.");
14
+ const resolved = requireParallelAxisLayer(program, target);
15
+ if (args.coordinate !== undefined && args.coordinate !== resolved.coordinate.id) {
16
+ throw new Error(`Parallel layer "${target}" uses coordinate "${resolved.coordinate.id}".`);
17
+ }
18
+ if (Object.hasOwn(args, "field") && !resolved.dimensions.some(dimension => dimension.field === args.field)) {
19
+ throw new Error(`${operation} requires an encoded dimension field "${args.field}".`);
20
+ }
21
+ return { ...resolved, target, stored };
22
+ }
23
+
24
+ function withOwner(program, resolved, config) {
25
+ let next = program;
26
+ if (resolved.stored === undefined) {
27
+ next = next.editSemantic({ property: "guide.axis.parallel.target", value: resolved.target })
28
+ .editSemantic({ property: "guide.axis.parallel.coordinate", value: resolved.coordinate.id })
29
+ .editSemantic({ property: "guide.axis.parallel.scales", value: resolved.dimensions.map(dimension => dimension.scale) });
30
+ }
31
+ return next._withGuideConfig("parallel", "axes", {
32
+ target: resolved.target, scales: resolved.dimensions.map(dimension => dimension.scale), ...config
33
+ });
34
+ }
35
+
36
+ function withTitle(program, args, create) {
37
+ const remove = !create && args.title === false;
38
+ const text = args.title?.text;
39
+ if (!remove && text === undefined) return program;
40
+ const previous = program.semanticSpec.guides.axis?.parallel?.titles ?? [];
41
+ const titles = previous.filter(title => title.field !== args.field);
42
+ if (!remove) titles.push({ field: args.field, text });
43
+ if (titles.length === 0 && previous.length === 0) return program;
44
+ return program.editSemantic({ property: "guide.axis.parallel.titles",
45
+ ...(titles.length === 0 ? { remove: true } : { value: titles }) });
46
+ }
47
+
48
+ const createParallelAxes = action({
49
+ op: "createParallelAxes", description: "Create axes for every encoded Parallel dimension."
50
+ }, function (args = {}) {
51
+ validateOptionObject(args, ["target", "coordinate"], "createParallelAxes");
52
+ const resolved = owner(this, args, "createParallelAxes");
53
+ if (resolved.stored !== undefined || this.guideConfigs.axis?.parallel !== undefined ||
54
+ Object.values(PARALLEL_AXIS_GRAPHICS).some(id => this.graphicSpec.objects[id] !== undefined)) {
55
+ throw new Error("createParallelAxes requires missing Parallel axes.");
56
+ }
57
+ return withOwner(this, resolved, {
58
+ mode: "all", dimensions: resolved.dimensions.map(dimension => defaultParallelAxis(dimension.field))
59
+ }).rematerializeParallelAxes({ target: resolved.target });
60
+ });
61
+
62
+ function makeAxisAction(create) {
63
+ const operation = create ? "createParallelAxis" : "editParallelAxis";
64
+ return action({ op: operation, description: `${create ? "Create" : "Edit"} selected components of a Parallel field axis.` }, function (args = {}) {
65
+ validateParallelAxisOptions(args, operation, create);
66
+ const resolved = owner(this, args, operation, !create);
67
+ const config = resolved.stored === undefined
68
+ ? { mode: "selected", dimensions: resolved.dimensions.map(dimension => ({ field: dimension.field })) }
69
+ : resolveParallelAxisConfigs(this, resolved.dimensions);
70
+ const previous = config.dimensions.find(dimension => dimension.field === args.field);
71
+ const updated = patchParallelAxis(previous, args, create, operation);
72
+ const next = withTitle(withOwner(this, resolved, {
73
+ ...config, dimensions: config.dimensions.map(dimension => dimension.field === args.field ? updated : dimension)
74
+ }), args, create);
75
+ return next.rematerializeParallelAxes({ target: resolved.target });
76
+ });
77
+ }
78
+
79
+ const createParallelAxis = makeAxisAction(true);
80
+ const editParallelAxis = makeAxisAction(false);
81
+
82
+ const removeParallelAxis = action({
83
+ op: "removeParallelAxis", description: "Remove every component of one Parallel field axis."
84
+ }, function (args = {}) {
85
+ validateOptionObject(args, ["field", "target"], "removeParallelAxis");
86
+ validateNonEmptyString(args.field, "Parallel axis field");
87
+ const resolved = owner(this, args, "removeParallelAxis", true);
88
+ const config = resolveParallelAxisConfigs(this, resolved.dimensions).dimensions.find(dimension => dimension.field === args.field);
89
+ if (!hasParallelAxisParts(config)) throw new Error("removeParallelAxis requires an existing field axis.");
90
+ return this.editParallelAxis({ field: args.field, target: resolved.target,
91
+ ...Object.fromEntries(PARALLEL_AXIS_PARTS.filter(part => config[part] !== undefined).map(part => [part, false])) });
92
+ });
93
+
94
+ const removeParallelAxes = action({
95
+ op: "removeParallelAxes", description: "Remove all Parallel axis semantics, recipes and graphics."
96
+ }, function (args = {}) {
97
+ validateOptionObject(args, ["target", "coordinate"], "removeParallelAxes");
98
+ const stored = this.semanticSpec.guides.axis?.parallel;
99
+ if (stored === undefined) throw new Error("removeParallelAxes requires existing Parallel axes.");
100
+ if (args.target !== undefined && args.target !== stored.target ||
101
+ args.coordinate !== undefined && args.coordinate !== stored.coordinate) {
102
+ throw new Error("removeParallelAxes selectors must match the stored owner.");
103
+ }
104
+ let next = this.editSemantic({ property: "guide.axis.parallel", remove: true });
105
+ for (const id of Object.values(PARALLEL_AXIS_GRAPHICS)) {
106
+ if (next.graphicSpec.objects[id] !== undefined) next = next.editGraphics({ target: id, remove: true });
107
+ }
108
+ return next._withoutMaterializationConfig(["guides", "axis", "parallel"]);
109
+ });
110
+
111
+ export function registerParallelAxisLifecycleActions(ProgramClass) {
112
+ ProgramClass.prototype.createParallelAxes = createParallelAxes;
113
+ ProgramClass.prototype.createParallelAxis = createParallelAxis;
114
+ ProgramClass.prototype.editParallelAxis = editParallelAxis;
115
+ ProgramClass.prototype.removeParallelAxis = removeParallelAxis;
116
+ ProgramClass.prototype.removeParallelAxes = removeParallelAxes;
117
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ validateGeneratedItemLimit,
3
+ validateNonEmptyString,
4
+ validateNonNegativeFinite,
5
+ validateOptionObject
6
+ } from "../../../../core/validation.js";
7
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
8
+ import { validateAxisTextStyle } from "../labels.js";
9
+ import { validateAxisFormat } from "../policy.js";
10
+
11
+ export const PARALLEL_AXIS_PARTS = Object.freeze(["line", "ticks", "labels", "title"]);
12
+ export const PARALLEL_AXIS_GRAPHICS = Object.freeze({
13
+ line: "parallelAxisLines", ticks: "parallelAxisTicks",
14
+ labels: "parallelAxisLabels", title: "parallelAxisTitles"
15
+ });
16
+ const STYLE_OPTIONS = Object.freeze({
17
+ line: ["color", "lineWidth"],
18
+ ticks: ["count", "values", "length", "color", "lineWidth"],
19
+ labels: ["count", "values", "offset", "format", "color", "fontSize", "fontFamily", "fontWeight"],
20
+ title: ["text", "offset", "color", "fontSize", "fontFamily", "fontWeight"]
21
+ });
22
+
23
+ export function defaultParallelAxis(field) {
24
+ return {
25
+ field,
26
+ line: { color: DEFAULT_COLORS.axis, lineWidth: 1.25 },
27
+ ticks: { mode: "auto", length: 8, color: DEFAULT_COLORS.mutedText, lineWidth: 1 },
28
+ labels: { mode: "auto", offset: 9, format: "auto", color: DEFAULT_COLORS.axis,
29
+ fontSize: 11, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: "normal" },
30
+ title: { offset: 20, color: DEFAULT_COLORS.axisTitle,
31
+ fontSize: 13, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 600 }
32
+ };
33
+ }
34
+
35
+ function validateMode(options, operation) {
36
+ if (Object.hasOwn(options, "count") && Object.hasOwn(options, "values")) {
37
+ throw new Error(`${operation} cannot use count and values together.`);
38
+ }
39
+ }
40
+
41
+ export function validateParallelAxisOptions(args, operation, create) {
42
+ validateOptionObject(args, ["target", "field", "line", "ticksAndLabels", "title", "ticks", "labels"], operation);
43
+ validateNonEmptyString(args.field, "Parallel axis field");
44
+ if (create && args.line === false && args.title === false &&
45
+ (args.ticksAndLabels === false || args.ticks === false && args.labels === false)) {
46
+ throw new Error(`${operation} requires at least one enabled axis component.`);
47
+ }
48
+ if (!create && ![...PARALLEL_AXIS_PARTS, "ticksAndLabels"].some(key => Object.hasOwn(args, key))) {
49
+ throw new Error(`${operation} requires at least one axis change.`);
50
+ }
51
+ if (args.ticksAndLabels !== undefined && (args.ticks !== undefined || args.labels !== undefined)) {
52
+ throw new Error(`${operation} cannot combine ticksAndLabels with ticks or labels.`);
53
+ }
54
+ for (const part of PARALLEL_AXIS_PARTS) {
55
+ if (!Object.hasOwn(args, part) || args[part] === false) continue;
56
+ validateOptionObject(args[part], STYLE_OPTIONS[part], `${operation}.${part}`);
57
+ validateMode(args[part], `${operation}.${part}`);
58
+ }
59
+ if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
60
+ const group = args.ticksAndLabels;
61
+ validateOptionObject(group, ["count", "values", "ticks", "labels"], `${operation}.ticksAndLabels`);
62
+ validateMode(group, `${operation}.ticksAndLabels`);
63
+ for (const part of ["ticks", "labels"]) {
64
+ if (Object.hasOwn(group, part)) {
65
+ validateOptionObject(group[part], STYLE_OPTIONS[part].filter(key => !["count", "values"].includes(key)),
66
+ `${operation}.ticksAndLabels.${part}`);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ function patchComponent(previous, defaults, options, part, create, operation) {
73
+ if (create && previous !== undefined) throw new Error(`${operation} requires missing ${part}.`);
74
+ if (!create && previous === undefined) throw new Error(`${operation} requires existing ${part}.`);
75
+ if (options === false) return undefined;
76
+ const next = { ...(create ? defaults : previous), ...options };
77
+ delete next.text;
78
+ if (Object.hasOwn(options, "count")) { next.mode = "count"; delete next.values; }
79
+ if (Object.hasOwn(options, "values")) { next.mode = "values"; delete next.count; }
80
+ validateNonEmptyString(next.color, "Parallel axis color");
81
+ if (part === "line" || part === "ticks") validateNonNegativeFinite(next.lineWidth, "Parallel axis lineWidth");
82
+ if (part === "ticks") validateNonNegativeFinite(next.length, "Parallel tick length");
83
+ if (part === "labels" || part === "title") {
84
+ validateAxisTextStyle(next, "Parallel axis text");
85
+ validateNonNegativeFinite(next.offset, "Parallel text offset");
86
+ }
87
+ if (part === "labels") validateAxisFormat(next.format);
88
+ if (Object.hasOwn(options, "text")) validateNonEmptyString(options.text, "Parallel axis title");
89
+ if (next.mode === "count") {
90
+ if (!Number.isInteger(next.count) || next.count <= 0) throw new RangeError("Parallel tick count must be a positive integer.");
91
+ validateGeneratedItemLimit(next.count, "Parallel tick count");
92
+ }
93
+ if (next.mode === "values") {
94
+ if (!Array.isArray(next.values)) throw new TypeError("Parallel tick values must be an array.");
95
+ validateGeneratedItemLimit(next.values.length, "Parallel tick values");
96
+ next.values = [...next.values];
97
+ }
98
+ return next;
99
+ }
100
+
101
+ export function patchParallelAxis(previous, args, create, operation) {
102
+ const defaults = defaultParallelAxis(args.field);
103
+ const next = { ...previous, field: args.field };
104
+ const updates = {};
105
+ for (const part of ["line", "title"]) {
106
+ if (create ? args[part] !== false : Object.hasOwn(args, part)) updates[part] = args[part] ?? {};
107
+ }
108
+ if (args.ticksAndLabels === false) {
109
+ if (!create) { updates.ticks = false; updates.labels = false; }
110
+ } else if (args.ticksAndLabels !== undefined) {
111
+ const group = args.ticksAndLabels ?? {};
112
+ const mode = Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(group, key)).map(key => [key, group[key]]));
113
+ updates.ticks = { ...mode, ...(group.ticks ?? {}) };
114
+ updates.labels = { ...mode, ...(group.labels ?? {}) };
115
+ } else {
116
+ for (const part of ["ticks", "labels"]) {
117
+ if (create ? args[part] !== false : Object.hasOwn(args, part)) updates[part] = args[part] ?? {};
118
+ }
119
+ }
120
+ for (const [part, options] of Object.entries(updates)) {
121
+ const value = patchComponent(previous?.[part], defaults[part], options, part, create, operation);
122
+ if (value === undefined) delete next[part]; else next[part] = value;
123
+ }
124
+ return next;
125
+ }
126
+
127
+ export function hasParallelAxisParts(config) {
128
+ return PARALLEL_AXIS_PARTS.some(part => config?.[part] !== undefined);
129
+ }
130
+
131
+ export function resolveParallelAxisConfigs(program, dimensions) {
132
+ const previous = program.guideConfigs.axis?.parallel?.axes;
133
+ const mode = previous?.mode ?? "all";
134
+ const configs = dimensions.map(dimension => previous?.dimensions?.find(config => config.field === dimension.field) ??
135
+ (mode === "all" ? defaultParallelAxis(dimension.field) : { field: dimension.field }));
136
+ return { mode, dimensions: configs };
137
+ }
@@ -9,6 +9,8 @@ import { niceTicks } from "../../../../grammar/ticks.js";
9
9
  import { resolveGraphicBounds } from "../../../../layout/canvas.js";
10
10
  import { findCoordinate } from "../../../../selectors/coordinates.js";
11
11
  import { findLayer } from "../../../../selectors/layers.js";
12
+ import { formatAxisValue } from "../policy.js";
13
+ import { resolveParallelAxisConfigs } from "./policy.js";
12
14
 
13
15
  export function requireParallelAxisLayer(program, target) {
14
16
  const layer = findLayer(program, target);
@@ -18,7 +20,8 @@ export function requireParallelAxisLayer(program, target) {
18
20
  if (
19
21
  layer?.mark?.type !== "line" ||
20
22
  coordinate?.type !== "parallel" ||
21
- layer.encoding?.parallel?.dimensions?.length < 2
23
+ !Array.isArray(layer.encoding?.parallel?.dimensions) ||
24
+ layer.encoding.parallel.dimensions.length < 2
22
25
  ) {
23
26
  throw new Error(`Parallel axes require an encoded Parallel line "${target}".`);
24
27
  }
@@ -43,16 +46,16 @@ export function resolveParallelAxisTarget(program, requested) {
43
46
  return candidates[0].id;
44
47
  }
45
48
 
46
- function ticksForScale(scale) {
49
+ function ticksForScale(scale, count = 5) {
47
50
  if (["ordinal", "band", "point"].includes(scale.type)) return scale.domain;
48
51
  if (isTransformedScaleType(scale.type)) {
49
- return transformedTicks(scale.type, scale.domain, 5, {
52
+ return transformedTicks(scale.type, scale.domain, count, {
50
53
  ...(scale.base === undefined ? {} : { base: scale.base }),
51
54
  ...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
52
55
  ...(scale.constant === undefined ? {} : { constant: scale.constant })
53
56
  });
54
57
  }
55
- return niceTicks(scale.domain, 5);
58
+ return niceTicks(scale.domain, count);
56
59
  }
57
60
 
58
61
  function formatValue(value) {
@@ -62,25 +65,43 @@ function formatValue(value) {
62
65
  return String(value);
63
66
  }
64
67
 
65
- export function resolveParallelAxisValues(program, dimensions) {
68
+ function componentValues(program, dimension, config) {
69
+ const scale = program.resolvedScales[dimension.scale];
70
+ if (scale === undefined) throw new Error(`Parallel axis requires resolved scale "${dimension.scale}".`);
71
+ const discrete = ["ordinal", "band", "point"].includes(scale.type);
72
+ if (discrete && config.mode === "count") throw new Error("Ordinal Parallel axes use domain values, not count.");
73
+ const values = config.mode === "values" ? config.values : ticksForScale(scale, config.count ?? 5);
74
+ if (new Set(values).size !== values.length || values.some(value => discrete
75
+ ? !scale.domain.includes(value)
76
+ : !Number.isFinite(value) || value < Math.min(...scale.domain) || value > Math.max(...scale.domain))) {
77
+ throw new Error(`Parallel axis values must be distinct values inside the scale domain for "${dimension.field}".`);
78
+ }
79
+ return {
80
+ values,
81
+ y: discrete ? mapOrdinalPositionValues(values, scale) : mapContinuousScaleValues(values, scale),
82
+ text: config.format === undefined ? undefined : values.map(value =>
83
+ formatAxisValue(value, scale.type, config.format, formatValue))
84
+ };
85
+ }
86
+
87
+ export function resolveStyledParallelAxes(program, dimensions) {
66
88
  const bounds = resolveGraphicBounds(program);
67
89
  const step = bounds.width / (dimensions.length - 1);
68
- const axes = dimensions.map((dimension, index) => {
69
- const scale = program.resolvedScales[dimension.scale];
70
- if (scale === undefined) {
71
- throw new Error(`Parallel axis requires resolved scale "${dimension.scale}".`);
72
- }
73
- const values = ticksForScale(scale);
74
- const y = ["ordinal", "band", "point"].includes(scale.type)
75
- ? mapOrdinalPositionValues(values, scale)
76
- : mapContinuousScaleValues(values, scale);
77
- return {
78
- ...dimension,
79
- x: bounds.x + step * index,
80
- values,
81
- y,
82
- labels: values.map(formatValue)
83
- };
84
- });
85
- return { axes, bounds };
90
+ const configs = resolveParallelAxisConfigs(program, dimensions);
91
+ const titles = program.semanticSpec.guides.axis?.parallel?.titles ?? [];
92
+ return {
93
+ bounds,
94
+ configs,
95
+ axes: dimensions.map((dimension, index) => {
96
+ const config = configs.dimensions[index];
97
+ return {
98
+ ...dimension,
99
+ x: bounds.x + step * index,
100
+ config,
101
+ title: titles.find(title => title.field === dimension.field)?.text ?? dimension.title,
102
+ ticks: config.ticks === undefined ? undefined : componentValues(program, dimension, config.ticks),
103
+ labels: config.labels === undefined ? undefined : componentValues(program, dimension, config.labels)
104
+ };
105
+ })
106
+ };
86
107
  }