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,451 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { isNominalValue } from "../../grammar/scales/fields.js";
5
+ import {
6
+ validateNonEmptyString,
7
+ validateNonNegativeFinite,
8
+ validateOptionObject
9
+ } from "../../core/validation.js";
10
+ import { findDataset } from "../../selectors/datasets.js";
11
+ import { findSemanticScale } from "../../selectors/scales.js";
12
+ import {
13
+ applyFacadeGuides,
14
+ normalizeAppearance,
15
+ normalizeFieldEncoding,
16
+ normalizeGuides,
17
+ omitUndefinedOptions,
18
+ resolveFacadeData,
19
+ resolveFacadeId,
20
+ validateFacadeOptions
21
+ } from "./shared.js";
22
+
23
+ const COMMON_OPTIONS = Object.freeze([
24
+ "id", "data", "coordinate", "category", "orientation", "summary", "guides", "labels"
25
+ ]);
26
+ const DOT_OPTIONS = Object.freeze([...COMMON_OPTIONS, "value", "point"]);
27
+ const LOLLIPOP_OPTIONS = Object.freeze([...DOT_OPTIONS, "baseline", "stem"]);
28
+ const DUMBBELL_OPTIONS = Object.freeze([
29
+ ...COMMON_OPTIONS, "start", "end", "startPoint", "endPoint", "connector"
30
+ ]);
31
+ const CATEGORY_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
32
+ const VALUE_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
33
+ const POINT_OPTIONS = Object.freeze([
34
+ "shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
35
+ ]);
36
+ const RULE_OPTIONS = Object.freeze(["stroke", "strokeWidth", "strokeDash", "opacity"]);
37
+ const LABEL_OPTIONS = Object.freeze([
38
+ "endpoint", "field", "value", "content", "normalizeBy", "format", "layout",
39
+ "fill", "opacity", "fontSize", "fontFamily", "fontWeight", "align", "baseline",
40
+ "rotation", "dx", "dy"
41
+ ]);
42
+ const SUMMARY_OPERATIONS = Object.freeze(["mean", "median", "sum", "min", "max"]);
43
+ const EDIT_OPTIONS = Object.freeze([
44
+ "target", "data", "coordinate", "category", "value", "start", "end",
45
+ "orientation", "summary", "baseline"
46
+ ]);
47
+
48
+ function normalizeCategory(value, operation) {
49
+ const category = normalizeFieldEncoding(value, `${operation} category`);
50
+ validateOptionObject(category, CATEGORY_OPTIONS, `${operation} category`);
51
+ validateNonEmptyString(category.field, `${operation} category field`);
52
+ const fieldType = category.fieldType ?? "nominal";
53
+ if (!['nominal', 'ordinal'].includes(fieldType)) {
54
+ throw new TypeError(`${operation} category must be nominal or ordinal.`);
55
+ }
56
+ return { ...omitUndefinedOptions(category), fieldType };
57
+ }
58
+
59
+ function normalizeValue(value, operation, role) {
60
+ const result = normalizeFieldEncoding(value, `${operation} ${role}`);
61
+ validateOptionObject(result, VALUE_OPTIONS, `${operation} ${role}`);
62
+ validateNonEmptyString(result.field, `${operation} ${role} field`);
63
+ if (result.fieldType !== undefined && result.fieldType !== "quantitative") {
64
+ throw new TypeError(`${operation} ${role} must be quantitative.`);
65
+ }
66
+ return { ...omitUndefinedOptions(result), fieldType: "quantitative" };
67
+ }
68
+
69
+ function normalizeOrientation(value, operation) {
70
+ const orientation = value ?? "horizontal";
71
+ if (!['horizontal', 'vertical'].includes(orientation)) {
72
+ throw new Error(`${operation} orientation must be horizontal or vertical.`);
73
+ }
74
+ return orientation;
75
+ }
76
+
77
+ function normalizeSummary(value, operation) {
78
+ if (value === undefined || value === false) return false;
79
+ if (!SUMMARY_OPERATIONS.includes(value)) {
80
+ throw new Error(`${operation} summary must be false or ${SUMMARY_OPERATIONS.join(", ")}.`);
81
+ }
82
+ return value;
83
+ }
84
+
85
+ function sameOptionValue(left, right) {
86
+ if (Object.is(left, right)) return true;
87
+ if (Array.isArray(left) || Array.isArray(right)) {
88
+ return Array.isArray(left) && Array.isArray(right)
89
+ && left.length === right.length
90
+ && left.every((value, index) => sameOptionValue(value, right[index]));
91
+ }
92
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
93
+ const leftKeys = Object.keys(left).sort();
94
+ const rightKeys = Object.keys(right).sort();
95
+ return leftKeys.length === rightKeys.length
96
+ && leftKeys.every((key, index) => key === rightKeys[index]
97
+ && sameOptionValue(left[key], right[key]));
98
+ }
99
+
100
+ function validateRows(program, data, category, measures, operation) {
101
+ const dataset = findDataset(program, data);
102
+ if (dataset?.values === undefined || dataset.values.length === 0) {
103
+ throw new Error(`${operation} requires at least one source row.`);
104
+ }
105
+ dataset.values.forEach((row, index) => {
106
+ if (!isNominalValue(row?.[category])) {
107
+ throw new TypeError(`${operation} category field "${category}" has an invalid value at row ${index}.`);
108
+ }
109
+ for (const field of measures) {
110
+ if (!Number.isFinite(row?.[field])) {
111
+ throw new TypeError(`${operation} ${field} must be finite at row ${index}.`);
112
+ }
113
+ }
114
+ });
115
+ }
116
+
117
+ function outputField(id, role) {
118
+ return `__${id}_${role}`;
119
+ }
120
+
121
+ function prepareData(program, { id, data, category, measures, summary, operation }) {
122
+ validateRows(program, data, category.field, measures.map(item => item.field), operation);
123
+ if (summary === false) return { program, data, measures };
124
+ const derived = `${id}SummaryData`;
125
+ const outputs = measures.map((item, index) => ({
126
+ ...item,
127
+ field: outputField(id, item.role ?? `value${index + 1}`)
128
+ }));
129
+ return {
130
+ program: program.createSummaryData({
131
+ id: derived,
132
+ source: data,
133
+ groupBy: category.field,
134
+ aggregates: measures.map((item, index) => ({
135
+ op: summary,
136
+ field: item.field,
137
+ as: outputs[index].field
138
+ }))
139
+ }),
140
+ data: derived,
141
+ measures: outputs
142
+ };
143
+ }
144
+
145
+ function positionBindings(id, category, value, orientation, coordinate) {
146
+ const categoryScale = { ...(category.scale ?? {}), id: category.scale?.id ?? `${id}Category` };
147
+ const valueScale = { ...(value.scale ?? {}), id: value.scale?.id ?? `${id}Value` };
148
+ const categoryBinding = {
149
+ field: category.field,
150
+ fieldType: category.fieldType,
151
+ scale: categoryScale,
152
+ ...(coordinate === undefined ? {} : { coordinate })
153
+ };
154
+ const valueBinding = {
155
+ field: value.field,
156
+ fieldType: "quantitative",
157
+ scale: valueScale,
158
+ ...(coordinate === undefined ? {} : { coordinate })
159
+ };
160
+ return orientation === "horizontal"
161
+ ? { x: valueBinding, y: categoryBinding }
162
+ : { x: categoryBinding, y: valueBinding };
163
+ }
164
+
165
+ function createPoint(program, { id, data, bindings, appearance }) {
166
+ const { radius, ...style } = appearance;
167
+ let next = program
168
+ .createPointMark({ id, data, ...style })
169
+ .encodeX({ target: id, ...bindings.x })
170
+ .encodeY({ target: id, ...bindings.y });
171
+ return radius === undefined
172
+ ? next
173
+ : next.encodePointRadius({ target: id, value: radius });
174
+ }
175
+
176
+ function normalizePoint(value, operation, role = "point") {
177
+ const point = normalizeAppearance(value, POINT_OPTIONS, `${operation} ${role}`);
178
+ if (point.radius !== undefined) {
179
+ validateNonNegativeFinite(point.radius, `${operation} ${role} radius`);
180
+ }
181
+ return point;
182
+ }
183
+
184
+ function normalizeRule(value, operation, role) {
185
+ return normalizeAppearance(value, RULE_OPTIONS, `${operation} ${role}`);
186
+ }
187
+
188
+ function createConnector(program, { id, data, bindings, end, appearance }) {
189
+ let next = program
190
+ .createRuleMark({ id, data, ...appearance })
191
+ .encodeX({ target: id, ...bindings.x })
192
+ .encodeY({ target: id, ...bindings.y });
193
+ const valueChannel = bindings.x.fieldType === "quantitative" ? "x" : "y";
194
+ return next[valueChannel === "x" ? "encodeX2" : "encodeY2"]({
195
+ target: id,
196
+ ...end,
197
+ fieldType: "quantitative"
198
+ });
199
+ }
200
+
201
+ function normalizeLabels(value, operation, allowEndpoint) {
202
+ if (value === undefined || value === false) return false;
203
+ if (!isPlainObject(value)) {
204
+ throw new TypeError(`${operation} labels must be false or a plain object.`);
205
+ }
206
+ validateOptionObject(value, LABEL_OPTIONS, `${operation} labels`);
207
+ if (!allowEndpoint && Object.hasOwn(value, "endpoint")) {
208
+ throw new Error(`${operation} labels.endpoint is only available for Dumbbell endpoints.`);
209
+ }
210
+ return { ...value };
211
+ }
212
+
213
+ function labelOptions(labels, defaultField) {
214
+ const { endpoint: _endpoint, ...options } = labels;
215
+ const hasContent = ["field", "value", "content"].some(key => Object.hasOwn(options, key));
216
+ return { ...options, ...(hasContent ? {} : { field: defaultField }) };
217
+ }
218
+
219
+ function attachLabels(program, id, labels, defaultField) {
220
+ if (labels === false) return program;
221
+ return program.createMarkLabels({
222
+ id: `${id}Label`,
223
+ source: id,
224
+ ...labelOptions(labels, defaultField)
225
+ });
226
+ }
227
+
228
+ function finish(program, { id, source, data, kind, roles, summary, orientation, options }) {
229
+ return program._withMarkConfig(id, {
230
+ ...program.markConfigs[id],
231
+ endpointPlot: { kind, source, data, roles, summary, orientation, options }
232
+ });
233
+ }
234
+
235
+ export const createDotPlot = action(
236
+ { op: "createDotPlot", description: "Create raw or explicitly summarized categorical dots." },
237
+ function (args = {}) {
238
+ const operation = "createDotPlot";
239
+ validateFacadeOptions(args, DOT_OPTIONS, operation);
240
+ const id = resolveFacadeId(this, args.id, { defaultId: "dotPlot", operation });
241
+ const source = resolveFacadeData(this, args.data, operation);
242
+ const category = normalizeCategory(args.category, operation);
243
+ const value = normalizeValue(args.value, operation, "value");
244
+ const orientation = normalizeOrientation(args.orientation, operation);
245
+ const summary = normalizeSummary(args.summary, operation);
246
+ const point = normalizePoint(args.point, operation);
247
+ const guides = normalizeGuides(args.guides, operation);
248
+ const labels = normalizeLabels(args.labels, operation, false);
249
+ const prepared = prepareData(this, {
250
+ id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation
251
+ });
252
+ const finalValue = prepared.measures[0];
253
+ const bindings = positionBindings(id, category, finalValue, orientation, args.coordinate);
254
+ let next = createPoint(prepared.program, { id, data: prepared.data, bindings, appearance: point });
255
+ next = attachLabels(next, id, labels, finalValue.field);
256
+ next = applyFacadeGuides(next, guides, id, guides);
257
+ return finish(next, {
258
+ id, source, data: prepared.data, kind: "dot", roles: { category, value }, summary, orientation,
259
+ options: { id, data: source, coordinate: args.coordinate, category, value, orientation,
260
+ summary, point, guides, labels }
261
+ });
262
+ }
263
+ );
264
+
265
+ export const createLollipopPlot = action(
266
+ { op: "createLollipopPlot", description: "Create categorical value points with baseline stems." },
267
+ function (args = {}) {
268
+ const operation = "createLollipopPlot";
269
+ validateFacadeOptions(args, LOLLIPOP_OPTIONS, operation);
270
+ const id = resolveFacadeId(this, args.id, { defaultId: "lollipopPlot", operation });
271
+ const source = resolveFacadeData(this, args.data, operation);
272
+ const category = normalizeCategory(args.category, operation);
273
+ const value = normalizeValue(args.value, operation, "value");
274
+ const orientation = normalizeOrientation(args.orientation, operation);
275
+ const summary = normalizeSummary(args.summary, operation);
276
+ const baseline = args.baseline ?? 0;
277
+ if (!Number.isFinite(baseline)) throw new TypeError(`${operation} baseline must be finite.`);
278
+ const point = normalizePoint(args.point, operation);
279
+ const stem = normalizeRule(args.stem, operation, "stem");
280
+ const guides = normalizeGuides(args.guides, operation);
281
+ const labels = normalizeLabels(args.labels, operation, false);
282
+ const prepared = prepareData(this, {
283
+ id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation
284
+ });
285
+ const finalValue = prepared.measures[0];
286
+ const bindings = positionBindings(id, category, finalValue, orientation, args.coordinate);
287
+ const stemId = `${id}Stem`;
288
+ let next = prepared.program;
289
+ next = createConnector(next, {
290
+ id: stemId,
291
+ data: prepared.data,
292
+ bindings,
293
+ end: { datum: baseline },
294
+ appearance: stem
295
+ });
296
+ next = createPoint(next, { id, data: prepared.data, bindings, appearance: point });
297
+ next = attachLabels(next, id, labels, finalValue.field);
298
+ next = applyFacadeGuides(next, guides, id, guides);
299
+ return finish(next, {
300
+ id, source, data: prepared.data, kind: "lollipop",
301
+ roles: { category, value, baseline, stemId }, summary, orientation,
302
+ options: { id, data: source, coordinate: args.coordinate, category, value, orientation,
303
+ summary, baseline, point, stem, guides, labels }
304
+ });
305
+ }
306
+ );
307
+
308
+ export const createDumbbellPlot = action(
309
+ { op: "createDumbbellPlot", description: "Create two categorical endpoints joined by a connector." },
310
+ function (args = {}) {
311
+ const operation = "createDumbbellPlot";
312
+ validateFacadeOptions(args, DUMBBELL_OPTIONS, operation);
313
+ const id = resolveFacadeId(this, args.id, { defaultId: "dumbbellPlot", operation });
314
+ const source = resolveFacadeData(this, args.data, operation);
315
+ const category = normalizeCategory(args.category, operation);
316
+ const start = normalizeValue(args.start, operation, "start");
317
+ const end = normalizeValue(args.end, operation, "end");
318
+ if (start.field === end.field) throw new Error(`${operation} start and end fields must be distinct.`);
319
+ if (start.scale !== undefined && end.scale !== undefined
320
+ && !sameOptionValue(start.scale, end.scale)) {
321
+ throw new Error(`${operation} start and end must use the same quantitative scale.`);
322
+ }
323
+ const orientation = normalizeOrientation(args.orientation, operation);
324
+ const summary = normalizeSummary(args.summary, operation);
325
+ const startPoint = normalizePoint(args.startPoint, operation, "startPoint");
326
+ const endPoint = normalizePoint(args.endPoint, operation, "endPoint");
327
+ const connector = normalizeRule(args.connector, operation, "connector");
328
+ const guides = normalizeGuides(args.guides, operation);
329
+ const labels = normalizeLabels(args.labels, operation, true);
330
+ const sharedScale = start.scale ?? end.scale;
331
+ const prepared = prepareData(this, {
332
+ id, data: source, category,
333
+ measures: [
334
+ { ...start, scale: sharedScale, role: "start" },
335
+ { ...end, scale: sharedScale, role: "end" }
336
+ ],
337
+ summary, operation
338
+ });
339
+ const [finalStart, finalEnd] = prepared.measures;
340
+ const startBindings = positionBindings(id, category, finalStart, orientation, args.coordinate);
341
+ const endBindings = positionBindings(id, category, finalEnd, orientation, args.coordinate);
342
+ const connectorId = `${id}Connector`;
343
+ const startId = `${id}Start`;
344
+ let next = createConnector(prepared.program, {
345
+ id: connectorId,
346
+ data: prepared.data,
347
+ bindings: startBindings,
348
+ end: { field: finalEnd.field },
349
+ appearance: connector
350
+ });
351
+ next = createPoint(next, {
352
+ id: startId, data: prepared.data, bindings: startBindings, appearance: startPoint
353
+ });
354
+ next = createPoint(next, {
355
+ id, data: prepared.data, bindings: endBindings, appearance: endPoint
356
+ });
357
+ if (labels !== false) {
358
+ const endpoint = labels.endpoint ?? "end";
359
+ if (!['start', 'end', 'both'].includes(endpoint)) {
360
+ throw new Error(`${operation} labels.endpoint must be start, end, or both.`);
361
+ }
362
+ for (const [role, target] of [["start", startId], ["end", id]]) {
363
+ if (endpoint !== "both" && endpoint !== role) continue;
364
+ next = next.createMarkLabels({
365
+ id: endpoint === "both" ? `${target}Label` : `${id}Label`,
366
+ source: target,
367
+ ...labelOptions(labels, role === "start" ? finalStart.field : finalEnd.field)
368
+ });
369
+ }
370
+ }
371
+ next = applyFacadeGuides(next, guides, id, guides);
372
+ return finish(next, {
373
+ id, source, data: prepared.data, kind: "dumbbell",
374
+ roles: { category, start, end, startId, connectorId }, summary, orientation,
375
+ options: { id, data: source, coordinate: args.coordinate, category, start, end, orientation,
376
+ summary, startPoint, endPoint, connector, guides, labels }
377
+ });
378
+ }
379
+ );
380
+
381
+ function resolveEndpointOwner(program, requested) {
382
+ const eligible = program.semanticSpec.layers.filter(
383
+ layer => program.markConfigs[layer.id]?.endpointPlot !== undefined
384
+ );
385
+ if (requested !== undefined) {
386
+ const id = validateUserId(requested, "Endpoint-plot owner id");
387
+ const owner = eligible.find(layer => layer.id === id);
388
+ if (owner === undefined) throw new Error(`Unknown endpoint-plot owner "${id}".`);
389
+ return owner;
390
+ }
391
+ const current = eligible.find(layer => layer.id === program.context.currentMark);
392
+ if (current !== undefined) return current;
393
+ if (eligible.length === 1) return eligible[0];
394
+ if (eligible.length === 0) throw new Error("editEndpointPlot requires an endpoint plot.");
395
+ throw new Error("editEndpointPlot target is ambiguous; provide target.");
396
+ }
397
+
398
+ const KIND_METHOD = Object.freeze({
399
+ dot: "createDotPlot",
400
+ lollipop: "createLollipopPlot",
401
+ dumbbell: "createDumbbellPlot"
402
+ });
403
+ const KIND_ROLES = Object.freeze({
404
+ dot: new Set(["data", "coordinate", "category", "value", "orientation", "summary"]),
405
+ lollipop: new Set([
406
+ "data", "coordinate", "category", "value", "orientation", "summary", "baseline"
407
+ ]),
408
+ dumbbell: new Set([
409
+ "data", "coordinate", "category", "start", "end", "orientation", "summary"
410
+ ])
411
+ });
412
+
413
+ function reviseRequestedScales(program, ownerId, revised, changes) {
414
+ let next = program;
415
+ for (const role of ["category", "value", "start", "end"]) {
416
+ if (!changes.includes(role) || !isPlainObject(revised[role]?.scale)) continue;
417
+ const requested = revised[role].scale;
418
+ const fallback = role === "category" ? `${ownerId}Category` : `${ownerId}Value`;
419
+ const id = requested.id ?? fallback;
420
+ if (findSemanticScale(next, id) === undefined) continue;
421
+ const { id: _id, ...definition } = requested;
422
+ void _id;
423
+ if (Object.keys(definition).length > 0) {
424
+ next = next.editScale({ id, ...definition });
425
+ }
426
+ }
427
+ return next;
428
+ }
429
+
430
+ export const editEndpointPlot = action(
431
+ { op: "editEndpointPlot", description: "Atomically revise an endpoint plot's semantic roles." },
432
+ function (args = {}) {
433
+ validateFacadeOptions(args, EDIT_OPTIONS, "editEndpointPlot");
434
+ const changes = Object.keys(args).filter(key => key !== "target");
435
+ if (changes.length === 0) {
436
+ throw new Error("editEndpointPlot requires at least one endpoint-plot option.");
437
+ }
438
+ const owner = resolveEndpointOwner(this, args.target);
439
+ const current = this.markConfigs[owner.id].endpointPlot;
440
+ const allowed = KIND_ROLES[current.kind];
441
+ const invalid = changes.find(key => !allowed.has(key));
442
+ if (invalid !== undefined) {
443
+ throw new Error(`editEndpointPlot ${invalid} is not available for ${current.kind} plots.`);
444
+ }
445
+ const revised = { ...current.options };
446
+ for (const key of changes) revised[key] = args[key];
447
+ let next = this.removeMark({ target: owner.id });
448
+ next = reviseRequestedScales(next, owner.id, revised, changes);
449
+ return next[KIND_METHOD[current.kind]](revised);
450
+ }
451
+ );
@@ -158,7 +158,7 @@ export const createHeatmap = action(
158
158
  .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
159
159
  .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }))
160
160
  .encodeColor(targetArgs(color, id));
161
- return applyFacadeGuides(next, guides);
161
+ return applyFacadeGuides(next, guides, id);
162
162
  }
163
163
 
164
164
  const x = normalizePosition(args.x, "x");
@@ -190,6 +190,6 @@ export const createHeatmap = action(
190
190
  fieldType: "quantitative",
191
191
  ...color
192
192
  });
193
- return applyFacadeGuides(next, resolvedGuides);
193
+ return applyFacadeGuides(next, resolvedGuides, id, guides);
194
194
  }
195
195
  );
@@ -53,6 +53,6 @@ export const createHistogram = action(
53
53
  ...histogram
54
54
  });
55
55
  if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
56
- return applyFacadeGuides(next, guides);
56
+ return applyFacadeGuides(next, guides, id);
57
57
  }
58
58
  );
@@ -0,0 +1,71 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateNonEmptyString, validateOptionObject, validateUnitInterval } from "../../core/validation.js";
3
+ import {
4
+ applyFacadeGuides, normalizeAppearance, normalizeFieldEncoding, normalizeGuides,
5
+ omitUndefinedOptions, resolveFacadeData, resolveFacadeId, validateFacadeOptions
6
+ } from "./shared.js";
7
+
8
+ const OPERATION = "createHorizonPlot";
9
+ const OPTIONS = ["id", "data", "coordinate", "x", "y", "groupBy", "bands", "baseline", "extent",
10
+ "resolve", "missing", "overflow", "palette", "area", "guides"];
11
+
12
+ function position(value, role) {
13
+ const label = `${OPERATION} ${role}`;
14
+ const field = normalizeFieldEncoding(value, label);
15
+ validateOptionObject(field, ["field", "fieldType", "scale", ...(role === "x" ? ["temporalUnit"] : [])], label);
16
+ validateNonEmptyString(field.field, `${label} field`);
17
+ if (field.fieldType === null || field.temporalUnit === null) throw new TypeError(`${label} type/unit cannot be null.`);
18
+ if (field.scale !== undefined) validateOptionObject(field.scale, undefined, `${label}.scale`);
19
+ return { ...omitUndefinedOptions(field),
20
+ ...(field.scale === undefined ? {} : { scale: omitUndefinedOptions(field.scale) }) };
21
+ }
22
+
23
+ function guideOptions(value) {
24
+ const guides = normalizeGuides(value, OPERATION);
25
+ if (guides === false) return false;
26
+ validateOptionObject(guides, ["axes", "grid", "legend"], `${OPERATION} guides`);
27
+ for (const [family, forbidden, supported] of [
28
+ ["axes", "y", ["coordinate", "x", "y"]], ["grid", "horizontal", ["horizontal", "vertical"]]
29
+ ]) {
30
+ const options = guides[family];
31
+ if (options === undefined || options === false) continue;
32
+ validateOptionObject(options, supported, `${OPERATION} guides.${family}`);
33
+ if (options[forbidden] !== undefined && options[forbidden] !== false) {
34
+ throw new Error(`${OPERATION} guides.${family}.${forbidden} only accepts false.`);
35
+ }
36
+ }
37
+ if (guides.legend !== undefined && guides.legend !== false) {
38
+ throw new Error(`${OPERATION} guides.legend only accepts false.`);
39
+ }
40
+ return guides;
41
+ }
42
+
43
+ export const createHorizonPlot = action({
44
+ op: OPERATION, description: "Create a signed, folded horizon area with original-x guides."
45
+ }, function (args = {}) {
46
+ validateFacadeOptions(args, OPTIONS, OPERATION);
47
+ const id = resolveFacadeId(this, args.id, { defaultId: "horizonPlot", operation: OPERATION });
48
+ const data = resolveFacadeData(this, args.data, OPERATION);
49
+ const x = position(args.x, "x");
50
+ const y = position(args.y, "y");
51
+ const { id: requestedId, data: requestedData, coordinate, x: requestedX, y: requestedY,
52
+ area: requestedArea, guides: requestedGuides, ...horizon } = omitUndefinedOptions(args);
53
+ const { opacity, ...area } = omitUndefinedOptions(normalizeAppearance(requestedArea,
54
+ ["opacity", "stroke", "strokeWidth", "curve"], `${OPERATION} area`));
55
+ for (const [key, value] of Object.entries({ ...horizon, ...area })) {
56
+ if (value === null) throw new TypeError(`${OPERATION} ${key} cannot be null.`);
57
+ }
58
+ if (horizon.palette !== undefined) {
59
+ validateOptionObject(horizon.palette, ["positive", "negative"], `${OPERATION} palette`);
60
+ if (Object.values(horizon.palette).some(value => value === null)) {
61
+ throw new TypeError(`${OPERATION} palette entries cannot be null.`);
62
+ }
63
+ }
64
+ if (opacity !== undefined) validateUnitInterval(opacity, `${OPERATION} area.opacity`);
65
+ const guides = guideOptions(requestedGuides);
66
+ let next = this.createAreaMark({ id, data, ...area });
67
+ if (coordinate !== undefined) next = next.createCoordinate({ id: coordinate, type: "cartesian", layers: [id] });
68
+ next = next.encodeHorizon({ ...horizon, target: id, x, y });
69
+ if (opacity !== undefined) next = next.editAreaMark({ target: id, opacity });
70
+ return applyFacadeGuides(next, guides, id, requestedGuides ?? {});
71
+ });
@@ -1,15 +1,52 @@
1
+ import { createRosePlot, createRadialBarPlot } from "./radial.js";
2
+ import { createAreaPlot } from "./area.js";
1
3
  import { createHeatmap } from "./heatmap.js";
2
4
  import { createBarPlot } from "./bar.js";
3
5
  import { createHistogram } from "./histogram.js";
4
6
  import { createLinePlot } from "./line.js";
5
7
  import { createScatterPlot } from "./scatter.js";
6
8
  import { createParallelCoordinates } from "./parallel.js";
9
+ import { createPiePlot } from "./pie.js";
10
+ import { createDensityPlot } from "./density.js";
11
+ import { createHorizonPlot } from "./horizon.js";
12
+ import { createPolarLinePlot, createPolarScatterPlot } from "./polar.js";
13
+ import { createRadarPlot } from "./radar.js";
14
+ import { createRugPlot, createStripPlot } from "./rug-strip.js";
15
+ import { createIntervalPlot, createRegressionPlot } from "./interval-regression.js";
16
+ import {
17
+ createDotPlot, createLollipopPlot, createDumbbellPlot, editEndpointPlot
18
+ } from "./endpoints.js";
19
+ import { createECDFPlot, editECDFPlot } from "./ecdf.js";
20
+ import { createBeeswarmPlot } from "./beeswarm.js";
21
+ import { createRaincloudPlot, editRaincloudPlot } from "./raincloud.js";
7
22
 
8
23
  export function registerChartActions(ProgramClass) {
24
+ ProgramClass.prototype.createRosePlot = createRosePlot;
25
+ ProgramClass.prototype.createRadialBarPlot = createRadialBarPlot;
26
+ ProgramClass.prototype.createAreaPlot = createAreaPlot;
9
27
  ProgramClass.prototype.createBarPlot = createBarPlot;
10
28
  ProgramClass.prototype.createHeatmap = createHeatmap;
11
29
  ProgramClass.prototype.createHistogram = createHistogram;
12
30
  ProgramClass.prototype.createScatterPlot = createScatterPlot;
13
31
  ProgramClass.prototype.createLinePlot = createLinePlot;
14
32
  ProgramClass.prototype.createParallelCoordinates = createParallelCoordinates;
33
+ ProgramClass.prototype.createPiePlot = createPiePlot;
34
+ ProgramClass.prototype.createDensityPlot = createDensityPlot;
35
+ ProgramClass.prototype.createHorizonPlot = createHorizonPlot;
36
+ ProgramClass.prototype.createPolarScatterPlot = createPolarScatterPlot;
37
+ ProgramClass.prototype.createPolarLinePlot = createPolarLinePlot;
38
+ ProgramClass.prototype.createRadarPlot = createRadarPlot;
39
+ ProgramClass.prototype.createRugPlot = createRugPlot;
40
+ ProgramClass.prototype.createStripPlot = createStripPlot;
41
+ ProgramClass.prototype.createIntervalPlot = createIntervalPlot;
42
+ ProgramClass.prototype.createRegressionPlot = createRegressionPlot;
43
+ ProgramClass.prototype.createDotPlot = createDotPlot;
44
+ ProgramClass.prototype.createLollipopPlot = createLollipopPlot;
45
+ ProgramClass.prototype.createDumbbellPlot = createDumbbellPlot;
46
+ ProgramClass.prototype.editEndpointPlot = editEndpointPlot;
47
+ ProgramClass.prototype.createECDFPlot = createECDFPlot;
48
+ ProgramClass.prototype.editECDFPlot = editECDFPlot;
49
+ ProgramClass.prototype.createBeeswarmPlot = createBeeswarmPlot;
50
+ ProgramClass.prototype.createRaincloudPlot = createRaincloudPlot;
51
+ ProgramClass.prototype.editRaincloudPlot = editRaincloudPlot;
15
52
  }