ggaction 0.0.12 → 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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -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 +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  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 +67 -22
  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 +21 -10
  83. package/src/actions/errorBars/edit.js +355 -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 +106 -2
  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 +34 -3
  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/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -0,0 +1,169 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateNonNegativeFinite } from "../../core/validation.js";
3
+ import { findDataset } from "../../selectors/datasets.js";
4
+ import {
5
+ applyFacadeGuides,
6
+ normalizeAppearance,
7
+ normalizeEncoding,
8
+ normalizeFieldEncoding,
9
+ normalizeGuides,
10
+ resolveFacadeData,
11
+ resolveFacadeId,
12
+ targetArgs,
13
+ validateFacadeOptions
14
+ } from "./shared.js";
15
+
16
+ const INTERVAL_OPTIONS = Object.freeze([
17
+ "id", "data", "coordinate", "x", "y", "xOffset", "yOffset", "groupBy",
18
+ "color", "point", "errorBar", "guides"
19
+ ]);
20
+ const POINT_OPTIONS = Object.freeze([
21
+ "shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
22
+ ]);
23
+ const ERROR_BAR_OPTIONS = Object.freeze([
24
+ "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
25
+ ]);
26
+
27
+ function intervalChannel(value, operation, channel, id) {
28
+ const encoding = normalizeFieldEncoding(value, `${operation} ${channel}`);
29
+ return {
30
+ ...encoding,
31
+ scale: {
32
+ ...(encoding.scale ?? {}),
33
+ id: encoding.scale?.id ?? `${id}${channel.toUpperCase()}`
34
+ }
35
+ };
36
+ }
37
+
38
+ function centerField(program, config) {
39
+ if (config.intervalMode === "explicit") return config.centerField;
40
+ const dataset = findDataset(program, config.data);
41
+ const transform = dataset?.transform?.[0];
42
+ if (transform?.type !== "interval") {
43
+ throw new Error(`Interval plot requires interval provenance for dataset "${config.data}".`);
44
+ }
45
+ return transform.as.center;
46
+ }
47
+
48
+ function applyIntervalPointPositions(program, id, config) {
49
+ const vertical = config.orientation === "vertical";
50
+ const positionChannel = vertical ? "x" : "y";
51
+ const intervalChannelName = vertical ? "y" : "x";
52
+ let next = program[positionChannel === "x" ? "encodeX" : "encodeY"]({
53
+ target: id,
54
+ field: config.positionField,
55
+ fieldType: config.positionFieldType,
56
+ ...(config.positionTemporalUnit === undefined
57
+ ? {}
58
+ : { temporalUnit: config.positionTemporalUnit }),
59
+ coordinate: config.coordinate,
60
+ scale: { id: config.positionScale }
61
+ });
62
+ next = next[intervalChannelName === "x" ? "encodeX" : "encodeY"]({
63
+ target: id,
64
+ field: centerField(next, config),
65
+ fieldType: "quantitative",
66
+ coordinate: config.coordinate,
67
+ scale: { id: config.intervalScale }
68
+ });
69
+ if (config.offset !== undefined) {
70
+ next = next[config.offset.channel === "xOffset" ? "encodeXOffset" : "encodeYOffset"]({
71
+ target: id,
72
+ field: config.offset.field,
73
+ fieldType: config.offset.fieldType,
74
+ scale: { id: config.offset.scale },
75
+ paddingInner: config.offset.paddingInner,
76
+ paddingOuter: config.offset.paddingOuter
77
+ });
78
+ }
79
+ return next;
80
+ }
81
+
82
+ export const createIntervalPlot = action(
83
+ {
84
+ op: "createIntervalPlot",
85
+ description: "Create a center point and matching statistical or explicit interval."
86
+ },
87
+ function (args = {}) {
88
+ const operation = "createIntervalPlot";
89
+ validateFacadeOptions(args, INTERVAL_OPTIONS, operation);
90
+ const id = resolveFacadeId(this, args.id, { defaultId: "intervalPlot", operation });
91
+ const data = resolveFacadeData(this, args.data, operation);
92
+ const x = intervalChannel(args.x, operation, "x", id);
93
+ const y = intervalChannel(args.y, operation, "y", id);
94
+ const point = normalizeAppearance(args.point, POINT_OPTIONS, `${operation} point`);
95
+ const { radius, ...pointAppearance } = point;
96
+ if (radius !== undefined) validateNonNegativeFinite(radius, `${operation} point radius`);
97
+ const errorBar = normalizeAppearance(
98
+ args.errorBar,
99
+ ERROR_BAR_OPTIONS,
100
+ `${operation} errorBar`
101
+ );
102
+ const color = normalizeEncoding(args.color, `${operation} color`);
103
+ const guides = normalizeGuides(args.guides, operation);
104
+ const intervalId = `${id}Interval`;
105
+
106
+ let next = this.createErrorBar({
107
+ id: intervalId,
108
+ data,
109
+ x,
110
+ y,
111
+ ...(args.xOffset === undefined ? {} : { xOffset: args.xOffset }),
112
+ ...(args.yOffset === undefined ? {} : { yOffset: args.yOffset }),
113
+ ...(Object.hasOwn(args, "groupBy") ? { groupBy: args.groupBy } : {}),
114
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
115
+ ...errorBar
116
+ });
117
+ const config = next.markConfigs[intervalId]?.errorBar;
118
+ if (config === undefined) {
119
+ throw new Error(`Interval plot child "${intervalId}" has no interval owner configuration.`);
120
+ }
121
+ next = next.createPointMark({ id, data: config.data, ...pointAppearance });
122
+ next = applyIntervalPointPositions(next, id, config);
123
+ if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
124
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
125
+ next = next._withMarkConfig(id, {
126
+ ...next.markConfigs[id],
127
+ intervalPlot: { intervalId, materialized: true }
128
+ });
129
+ return applyFacadeGuides(next, guides, id, guides);
130
+ }
131
+ );
132
+
133
+ const REGRESSION_OPTIONS = Object.freeze([
134
+ "id", "data", "coordinate", "x", "y", "color", "size", "shape", "point",
135
+ "groupBy", "method", "degree", "span", "confidenceMethod", "level", "confidence",
136
+ "interval", "band", "line", "guides"
137
+ ]);
138
+
139
+ export const createRegressionPlot = action(
140
+ {
141
+ op: "createRegressionPlot",
142
+ description: "Create a scatter plot with a fitted regression line and optional interval band."
143
+ },
144
+ function (args = {}) {
145
+ const operation = "createRegressionPlot";
146
+ validateFacadeOptions(args, REGRESSION_OPTIONS, operation);
147
+ const id = resolveFacadeId(this, args.id, { defaultId: "regressionPlot", operation });
148
+ const data = resolveFacadeData(this, args.data, operation);
149
+ const guides = normalizeGuides(args.guides, operation);
150
+ let next = this.createScatterPlot({
151
+ id,
152
+ data,
153
+ x: args.x,
154
+ y: args.y,
155
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
156
+ ...(args.color === undefined ? {} : { color: args.color }),
157
+ ...(args.size === undefined ? {} : { size: args.size }),
158
+ ...(args.shape === undefined ? {} : { shape: args.shape }),
159
+ ...(args.point === undefined ? {} : { point: args.point }),
160
+ guides: false
161
+ });
162
+ const regression = Object.fromEntries([
163
+ "groupBy", "method", "degree", "span", "confidenceMethod", "level",
164
+ "confidence", "interval", "band", "line"
165
+ ].filter(key => Object.hasOwn(args, key)).map(key => [key, args[key]]));
166
+ next = next.createRegression({ target: id, ...regression });
167
+ return applyFacadeGuides(next, guides, id, guides);
168
+ }
169
+ );
@@ -1,5 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
- import { validateNonEmptyString } from "../../core/validation.js";
2
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
3
3
  import {
4
4
  applyFacadeGuides,
5
5
  normalizeAppearance,
@@ -50,20 +50,20 @@ export const createLinePlot = action(
50
50
  const strokeDash = normalizeStrokeDashEncoding(args.strokeDash);
51
51
  const groupBy = args.groupBy === undefined
52
52
  ? undefined
53
- : validateNonEmptyString(args.groupBy, "createLinePlot groupBy");
53
+ : normalizeGroupFields(args.groupBy);
54
54
  const guides = normalizeGuides(args.guides, "createLinePlot");
55
55
 
56
56
  let next = this
57
57
  .createLineMark({ id, data, ...line })
58
58
  .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
59
59
  .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
60
- if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
61
60
  if (groupBy !== undefined) {
62
- next = next.encodeGroup({ field: groupBy, target: id });
61
+ next = next.encodeGroup({ fields: groupBy, target: id });
63
62
  }
63
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
64
64
  if (strokeDash !== undefined) {
65
65
  next = next.encodeStrokeDash(targetArgs(strokeDash, id));
66
66
  }
67
- return applyFacadeGuides(next, guides);
67
+ return applyFacadeGuides(next, guides, id);
68
68
  }
69
69
  );
@@ -105,6 +105,6 @@ export const createParallelCoordinates = action(
105
105
  if (strokeDash !== undefined) {
106
106
  next = next.encodeStrokeDash(targetArgs(strokeDash, id));
107
107
  }
108
- return applyFacadeGuides(next, guides);
108
+ return applyFacadeGuides(next, guides, id);
109
109
  }
110
110
  );
@@ -0,0 +1,44 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
4
+ normalizeArcCategory, normalizeCategoryAggregate, omitUndefinedOptions,
5
+ resolveFacadeData, resolveFacadeId, validateFacadeOptions
6
+ } from "./shared.js";
7
+
8
+ const OPERATION = "createPiePlot";
9
+ const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "color", "arc", "guides"];
10
+ const ARC_OPTIONS = ["innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth"];
11
+ function guideOptions(value, color) {
12
+ const guides = normalizeCategoricalGuides(value, OPERATION, color);
13
+ if (guides === false) return false;
14
+ for (const key of ["axes", "grid"]) {
15
+ if (guides[key] !== undefined && guides[key] !== false) {
16
+ throw new TypeError(`${OPERATION} guides.${key} only accepts false.`);
17
+ }
18
+ }
19
+ return { ...guides, axes: false, grid: false };
20
+ }
21
+
22
+ export const createPiePlot = action({
23
+ op: OPERATION, description: "Create a categorical count or weighted-sum pie or donut plot."
24
+ }, function (args = {}) {
25
+ validateFacadeOptions(args, OPTIONS, OPERATION);
26
+ const id = resolveFacadeId(this, args.id, { defaultId: "piePlot", operation: OPERATION });
27
+ const data = resolveFacadeData(this, args.data, OPERATION);
28
+ const category = normalizeArcCategory(args.category, OPERATION);
29
+ const aggregate = normalizeCategoryAggregate(args, OPERATION);
30
+ const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${OPERATION} arc`));
31
+ const color = args.color === false ? undefined
32
+ : normalizeCategoricalColor(args.color === undefined ? category.field : args.color, `${OPERATION} color`);
33
+ if (color !== undefined && arc.fill !== undefined) {
34
+ throw new Error(`${OPERATION} arc.fill cannot be combined with color; use color:false.`);
35
+ }
36
+ const guides = guideOptions(args.guides, color);
37
+ let next = this.createArcMark({ id, data, ...arc }).encodeTheta({
38
+ ...category, target: id, aggregate,
39
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
40
+ ...(aggregate === "sum" ? { weight: args.value } : {})
41
+ });
42
+ if (color !== undefined) next = next.encodeColor({ ...color, target: id });
43
+ return applyFacadeGuides(next, guides, id, args.guides ?? {});
44
+ });
@@ -0,0 +1,123 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateNonNegativeFinite, validateOptionObject } from
3
+ "../../core/validation.js";
4
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
5
+ import {
6
+ applyFacadeGuides,
7
+ normalizeAppearance,
8
+ normalizeEncoding,
9
+ normalizeFieldEncoding,
10
+ normalizeGuides,
11
+ normalizeStrokeDashEncoding,
12
+ positionArgs,
13
+ resolveFacadeData,
14
+ resolveFacadeId,
15
+ targetArgs,
16
+ validateFacadeOptions
17
+ } from "./shared.js";
18
+
19
+ const POSITION_OPTIONS = Object.freeze([
20
+ "field", "fieldType", "temporalUnit", "scale"
21
+ ]);
22
+ const POINT_OPTIONS = Object.freeze([
23
+ "shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
24
+ ]);
25
+ const LINE_OPTIONS = Object.freeze([
26
+ "strokeWidth", "curve", "stroke", "opacity", "closed"
27
+ ]);
28
+
29
+ function position(value, operation, channel) {
30
+ const resolved = normalizeFieldEncoding(value, `${operation} ${channel}`);
31
+ validateOptionObject(
32
+ resolved,
33
+ channel === "theta" ? POSITION_OPTIONS : ["field", "fieldType", "scale"],
34
+ `${operation} ${channel}`
35
+ );
36
+ return resolved;
37
+ }
38
+
39
+ export const createPolarScatterPlot = action(
40
+ {
41
+ op: "createPolarScatterPlot",
42
+ description: "Create a Polar scatter plot with distinct radial position and glyph size."
43
+ },
44
+ function (args = {}) {
45
+ const operation = "createPolarScatterPlot";
46
+ validateFacadeOptions(args, [
47
+ "id", "data", "coordinate", "theta", "radius", "color", "size",
48
+ "shape", "point", "guides"
49
+ ], operation);
50
+ const id = resolveFacadeId(this, args.id, {
51
+ defaultId: "polarScatterPlot",
52
+ operation
53
+ });
54
+ const data = resolveFacadeData(this, args.data, operation);
55
+ const theta = position(args.theta, operation, "theta");
56
+ const radiusPosition = position(args.radius, operation, "radius");
57
+ const color = normalizeEncoding(args.color, `${operation} color`);
58
+ const size = normalizeEncoding(args.size, `${operation} size`);
59
+ const shape = normalizeEncoding(args.shape, `${operation} shape`);
60
+ const { radius, ...point } = normalizeAppearance(
61
+ args.point,
62
+ POINT_OPTIONS,
63
+ `${operation} point`
64
+ );
65
+ const guides = normalizeGuides(args.guides, operation);
66
+ if (radius !== undefined) {
67
+ validateNonNegativeFinite(radius, `${operation} point radius`);
68
+ if (size !== undefined) {
69
+ throw new Error(`${operation} point radius conflicts with size.`);
70
+ }
71
+ }
72
+
73
+ let next = this
74
+ .createPointMark({ id, data, ...point })
75
+ .encodeTheta(positionArgs(theta, { target: id, coordinate: args.coordinate }))
76
+ .encodeR(positionArgs(radiusPosition, { target: id, coordinate: args.coordinate }));
77
+ if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
78
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
79
+ if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
80
+ if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
81
+ return applyFacadeGuides(next, guides, id);
82
+ }
83
+ );
84
+
85
+ export const createPolarLinePlot = action(
86
+ {
87
+ op: "createPolarLinePlot",
88
+ description: "Create an open or explicitly closed Polar line plot."
89
+ },
90
+ function (args = {}) {
91
+ const operation = "createPolarLinePlot";
92
+ validateFacadeOptions(args, [
93
+ "id", "data", "coordinate", "theta", "radius", "color", "groupBy",
94
+ "strokeDash", "line", "guides"
95
+ ], operation);
96
+ const id = resolveFacadeId(this, args.id, {
97
+ defaultId: "polarLinePlot",
98
+ operation
99
+ });
100
+ const data = resolveFacadeData(this, args.data, operation);
101
+ const theta = position(args.theta, operation, "theta");
102
+ const radius = position(args.radius, operation, "radius");
103
+ const color = normalizeEncoding(args.color, `${operation} color`);
104
+ const strokeDash = normalizeStrokeDashEncoding(
105
+ args.strokeDash,
106
+ `${operation} strokeDash`
107
+ );
108
+ const groupBy = args.groupBy === undefined
109
+ ? undefined
110
+ : normalizeGroupFields(args.groupBy);
111
+ const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
112
+ const guides = normalizeGuides(args.guides, operation);
113
+
114
+ let next = this
115
+ .createLineMark({ id, data, ...line })
116
+ .encodeTheta(positionArgs(theta, { target: id, coordinate: args.coordinate }))
117
+ .encodeR(positionArgs(radius, { target: id, coordinate: args.coordinate }));
118
+ if (groupBy !== undefined) next = next.encodeGroup({ target: id, fields: groupBy });
119
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
120
+ if (strokeDash !== undefined) next = next.encodeStrokeDash(targetArgs(strokeDash, id));
121
+ return applyFacadeGuides(next, guides, id);
122
+ }
123
+ );
@@ -0,0 +1,299 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateNonEmptyString, validateOptionObject } from
4
+ "../../core/validation.js";
5
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
6
+ import { readNominalField, readQuantitativeField } from
7
+ "../../grammar/scales/fields.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import {
10
+ normalizeAppearance,
11
+ normalizeEncoding,
12
+ normalizeFieldEncoding,
13
+ normalizeGuides,
14
+ normalizeStrokeDashEncoding,
15
+ omitUndefinedOptions,
16
+ resolveFacadeData,
17
+ resolveFacadeId,
18
+ validateFacadeOptions
19
+ } from "./shared.js";
20
+
21
+ const OPTIONS = Object.freeze([
22
+ "id", "data", "coordinate", "category", "value", "wide", "groupBy",
23
+ "order", "color", "strokeDash", "line", "guides"
24
+ ]);
25
+ const CATEGORY_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
26
+ const VALUE_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
27
+ const CATEGORY_SCALE_OPTIONS = Object.freeze([
28
+ "id", "type", "domain", "range", "reverse", "paddingInner", "paddingOuter",
29
+ "padding", "align"
30
+ ]);
31
+ const WIDE_OPTIONS = Object.freeze(["fields", "as"]);
32
+ const WIDE_AS_OPTIONS = Object.freeze(["key", "value"]);
33
+ const LINE_OPTIONS = Object.freeze([
34
+ "strokeWidth", "curve", "stroke", "opacity", "closed"
35
+ ]);
36
+
37
+ function normalizeCategory(value, operation) {
38
+ const category = normalizeFieldEncoding(value, `${operation} category`);
39
+ validateOptionObject(category, CATEGORY_OPTIONS, `${operation} category`);
40
+ validateNonEmptyString(category.field, `${operation} category field`);
41
+ const fieldType = category.fieldType ?? "nominal";
42
+ if (!["nominal", "ordinal"].includes(fieldType)) {
43
+ throw new TypeError(`${operation} category must be nominal or ordinal.`);
44
+ }
45
+ if (category.scale !== undefined) {
46
+ validateOptionObject(
47
+ category.scale,
48
+ CATEGORY_SCALE_OPTIONS,
49
+ `${operation} category.scale`
50
+ );
51
+ if (
52
+ category.scale.type !== undefined &&
53
+ !["band", "point"].includes(category.scale.type)
54
+ ) {
55
+ throw new TypeError(`${operation} category.scale.type must be band or point.`);
56
+ }
57
+ }
58
+ return {
59
+ ...omitUndefinedOptions(category),
60
+ fieldType,
61
+ ...(category.scale === undefined
62
+ ? {}
63
+ : { scale: omitUndefinedOptions(category.scale) })
64
+ };
65
+ }
66
+
67
+ function normalizeValue(value, operation) {
68
+ const result = normalizeFieldEncoding(value, `${operation} value`);
69
+ validateOptionObject(result, VALUE_OPTIONS, `${operation} value`);
70
+ validateNonEmptyString(result.field, `${operation} value field`);
71
+ if (result.fieldType !== undefined && result.fieldType !== "quantitative") {
72
+ throw new TypeError(`${operation} value must be quantitative.`);
73
+ }
74
+ return { ...omitUndefinedOptions(result), fieldType: "quantitative" };
75
+ }
76
+
77
+ function normalizeWide(value, id, operation) {
78
+ if (!isPlainObject(value)) {
79
+ throw new TypeError(`${operation} wide must be a plain object.`);
80
+ }
81
+ validateOptionObject(value, WIDE_OPTIONS, `${operation} wide`);
82
+ if (!Array.isArray(value.fields) || value.fields.length < 3) {
83
+ throw new Error(`${operation} wide.fields must contain at least three fields.`);
84
+ }
85
+ const fields = [...value.fields];
86
+ for (const field of fields) validateNonEmptyString(field, `${operation} wide field`);
87
+ if (new Set(fields).size !== fields.length) {
88
+ throw new Error(`${operation} wide.fields must be unique.`);
89
+ }
90
+ const aliases = value.as === undefined ? {} : value.as;
91
+ if (!isPlainObject(aliases)) {
92
+ throw new TypeError(`${operation} wide.as must be a plain object.`);
93
+ }
94
+ validateOptionObject(aliases, WIDE_AS_OPTIONS, `${operation} wide.as`);
95
+ const key = aliases.key ?? `${id}Dimension`;
96
+ const foldedValue = aliases.value ?? `${id}Value`;
97
+ validateNonEmptyString(key, `${operation} wide.as.key`);
98
+ validateNonEmptyString(foldedValue, `${operation} wide.as.value`);
99
+ if (key === foldedValue || fields.includes(key) || fields.includes(foldedValue)) {
100
+ throw new Error(`${operation} wide aliases must be distinct from selected fields.`);
101
+ }
102
+ return { fields, as: { key, value: foldedValue } };
103
+ }
104
+
105
+ function orderedCategories(rows, field, requested, operation) {
106
+ const observed = [...new Set(readNominalField(rows, field))];
107
+ if (!Array.isArray(requested) && requested !== undefined) {
108
+ throw new TypeError(`${operation} order must be an array.`);
109
+ }
110
+ const order = requested === undefined ? observed : [...requested];
111
+ if (order.length < 3 || new Set(order).size !== order.length) {
112
+ throw new Error(`${operation} order must contain at least three distinct categories.`);
113
+ }
114
+ const observedSet = new Set(observed);
115
+ if (
116
+ order.length !== observed.length ||
117
+ order.some(value => !observedSet.has(value))
118
+ ) {
119
+ throw new Error(`${operation} order must contain every category exactly once.`);
120
+ }
121
+ return order;
122
+ }
123
+
124
+ function seriesKey(row, fields) {
125
+ return JSON.stringify(fields.map(field => row[field]));
126
+ }
127
+
128
+ function radarSeriesFields(groupBy, color, strokeDash, operation) {
129
+ if (groupBy.length > 0) return groupBy;
130
+ const colorField = color?.field;
131
+ const dashField = strokeDash?.field;
132
+ if (
133
+ colorField !== undefined && dashField !== undefined &&
134
+ colorField !== dashField
135
+ ) {
136
+ throw new Error(
137
+ `${operation} color and strokeDash fields must match unless groupBy defines the series.`
138
+ );
139
+ }
140
+ const field = colorField ?? dashField;
141
+ return field === undefined ? [] : [field];
142
+ }
143
+
144
+ function validateRadarRows(rows, { category, value, groupBy, order }, operation) {
145
+ readQuantitativeField(rows, value);
146
+ for (const field of groupBy) readNominalField(rows, field);
147
+ const expected = new Set(order);
148
+ const series = new Map();
149
+ for (const row of rows) {
150
+ const key = seriesKey(row, groupBy);
151
+ const categories = series.get(key) ?? new Set();
152
+ const current = row[category];
153
+ if (!expected.has(current)) {
154
+ throw new Error(`${operation} found a category outside order.`);
155
+ }
156
+ if (categories.has(current)) {
157
+ throw new Error(`${operation} requires one row per series and category.`);
158
+ }
159
+ categories.add(current);
160
+ series.set(key, categories);
161
+ }
162
+ if (series.size === 0) throw new Error(`${operation} requires radar rows.`);
163
+ for (const categories of series.values()) {
164
+ if (
165
+ categories.size !== order.length ||
166
+ order.some(value => !categories.has(value))
167
+ ) {
168
+ throw new Error(`${operation} requires every category in every series.`);
169
+ }
170
+ }
171
+ }
172
+
173
+ function scaleWithOrder(category, order, operation) {
174
+ const scale = category.scale ?? {};
175
+ if (
176
+ scale.domain !== undefined && scale.domain !== "auto" &&
177
+ (!Array.isArray(scale.domain) ||
178
+ scale.domain.length !== order.length ||
179
+ scale.domain.some((value, index) => value !== order[index]))
180
+ ) {
181
+ throw new Error(`${operation} category.scale.domain conflicts with order.`);
182
+ }
183
+ return { ...category, scale: { ...scale, domain: order } };
184
+ }
185
+
186
+ export const createRadarPlot = action(
187
+ {
188
+ op: "createRadarPlot",
189
+ description: "Create a closed Radar plot from long rows or an explicit wide fold."
190
+ },
191
+ function (args = {}) {
192
+ const operation = "createRadarPlot";
193
+ validateFacadeOptions(args, OPTIONS, operation);
194
+ const hasWide = args.wide !== undefined;
195
+ const hasCategory = args.category !== undefined;
196
+ const hasValue = args.value !== undefined;
197
+ if (
198
+ (hasWide && (hasCategory || hasValue)) ||
199
+ (!hasWide && (!hasCategory || !hasValue))
200
+ ) {
201
+ throw new Error(
202
+ `${operation} requires category and value, or wide, but not both.`
203
+ );
204
+ }
205
+ const longForm = !hasWide;
206
+ const id = resolveFacadeId(this, args.id, {
207
+ defaultId: "radarPlot",
208
+ operation
209
+ });
210
+ const source = resolveFacadeData(this, args.data, operation);
211
+ const sourceRows = findDataset(this, source).values;
212
+ const groupBy = args.groupBy === undefined
213
+ ? []
214
+ : normalizeGroupFields(args.groupBy);
215
+ const color = normalizeEncoding(args.color, `${operation} color`);
216
+ const strokeDash = normalizeStrokeDashEncoding(
217
+ args.strokeDash,
218
+ `${operation} strokeDash`
219
+ );
220
+ const seriesFields = radarSeriesFields(
221
+ groupBy,
222
+ color,
223
+ strokeDash,
224
+ operation
225
+ );
226
+ const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
227
+ if (line.closed === false) {
228
+ throw new Error(`${operation} requires a closed line.`);
229
+ }
230
+ const guides = normalizeGuides(args.guides, operation);
231
+
232
+ let next = this;
233
+ let data = source;
234
+ let category;
235
+ let radarValue;
236
+ let order;
237
+ if (longForm) {
238
+ category = normalizeCategory(args.category, operation);
239
+ radarValue = normalizeValue(args.value, operation);
240
+ order = orderedCategories(sourceRows, category.field, args.order, operation);
241
+ validateRadarRows(sourceRows, {
242
+ category: category.field,
243
+ value: radarValue.field,
244
+ groupBy: seriesFields,
245
+ order
246
+ }, operation);
247
+ } else {
248
+ const wide = normalizeWide(args.wide, id, operation);
249
+ if (sourceRows.length > 1 && groupBy.length === 0) {
250
+ throw new Error(`${operation} wide rows require groupBy as series identity.`);
251
+ }
252
+ for (const field of wide.fields) readQuantitativeField(sourceRows, field);
253
+ for (const field of groupBy) readNominalField(sourceRows, field);
254
+ if (!Array.isArray(args.order) && args.order !== undefined) {
255
+ throw new TypeError(`${operation} order must be an array.`);
256
+ }
257
+ order = args.order === undefined ? wide.fields : [...args.order];
258
+ if (
259
+ order.length !== wide.fields.length ||
260
+ new Set(order).size !== order.length ||
261
+ order.some(field => !wide.fields.includes(field))
262
+ ) {
263
+ throw new Error(
264
+ `${operation} order must contain every wide field exactly once.`
265
+ );
266
+ }
267
+ data = `${id}FoldData`;
268
+ next = next.createFoldData({
269
+ id: data,
270
+ source,
271
+ fields: wide.fields,
272
+ as: wide.as
273
+ });
274
+ const foldedRows = findDataset(next, data).values;
275
+ category = { field: wide.as.key, fieldType: "nominal" };
276
+ radarValue = { field: wide.as.value, fieldType: "quantitative" };
277
+ validateRadarRows(foldedRows, {
278
+ category: category.field,
279
+ value: radarValue.field,
280
+ groupBy: seriesFields,
281
+ order
282
+ }, operation);
283
+ }
284
+
285
+ category = scaleWithOrder(category, order, operation);
286
+ return next.createPolarLinePlot({
287
+ id,
288
+ data,
289
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
290
+ theta: category,
291
+ radius: radarValue,
292
+ ...(groupBy.length === 0 ? {} : { groupBy }),
293
+ ...(color === undefined ? {} : { color }),
294
+ ...(strokeDash === undefined ? {} : { strokeDash }),
295
+ line: { ...line, closed: true },
296
+ guides
297
+ });
298
+ }
299
+ );