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,12 +1,16 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { freezeOwned, isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
3
4
  import {
4
5
  validateNonEmptyString,
5
6
  validateNonNegativeFinite,
6
7
  validateOptionObject,
7
8
  validatePositiveFinite
8
9
  } from "../../core/validation.js";
9
- import { resolveFacetDefinition } from "../../grammar/facets/index.js";
10
+ import {
11
+ resolveFacetDefinition,
12
+ resolveFacetGridDefinition
13
+ } from "../../grammar/facets/index.js";
10
14
  import {
11
15
  FACET_SCALE_CHANNELS,
12
16
  normalizeFacetScalePolicies
@@ -16,14 +20,26 @@ import { compositionChildDescriptor } from
16
20
  "../../materialization/composition.js";
17
21
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
18
22
  import { deriveFacetChildren } from "./derive.js";
23
+ import { resolveFacetChildrenScales } from "./derive.js";
19
24
  import { replayDerivedData } from "./replay.js";
20
25
  import { composeFacetGuides } from "./guides.js";
21
26
  import { applyCompositionState } from "../composition/actions.js";
27
+ import { findDataset } from "../../selectors/datasets.js";
28
+ import { findLayer } from "../../selectors/layers.js";
22
29
 
23
30
  const FACET_OPTIONS = Object.freeze([
24
- "id", "field", "data", "columns", "gap", "align", "padding", "scales",
31
+ "id", "field", "data", "values", "columns", "gap", "align", "padding", "scales",
25
32
  "guides"
26
33
  ]);
34
+ const FACET_GRID_OPTIONS = Object.freeze([
35
+ "id", "data", "rows", "columns", "combinations", "gap", "align",
36
+ "padding", "scales", "guides"
37
+ ]);
38
+ const REPEAT_OPTIONS = Object.freeze([
39
+ "id", "target", "channel", "fields", "columns", "gap", "align",
40
+ "padding", "scales", "guides"
41
+ ]);
42
+ const SOURCE_EDIT_OPTIONS = Object.freeze(["program"]);
27
43
  const GUIDE_OPTIONS = Object.freeze(["axes", "legend"]);
28
44
  const HEADER_OPTIONS = Object.freeze([
29
45
  "fontSize", "fontFamily", "fontWeight", "color", "offset"
@@ -57,18 +73,32 @@ function requireFacetProgram(program, operation) {
57
73
  }
58
74
  }
59
75
 
76
+ function usedFacetScalePolicies(program, policies) {
77
+ return Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
78
+ program.semanticSpec.layers.some(
79
+ layer => layer.encoding?.[channel]?.scale !== undefined
80
+ ) ? [[channel, policies[channel]]] : []
81
+ ));
82
+ }
83
+
60
84
  function facetUnitTemplate(program) {
61
- const seed = program.children[program.compositionSpec.children[0]];
85
+ const seedId = program.compositionSpec.children.find(id =>
86
+ program.children[id]?.semanticSpec.layers.length > 0
87
+ ) ?? program.compositionSpec.children[0];
88
+ const seed = program.children[seedId];
62
89
  if (seed === undefined) {
63
90
  throw new Error(`Facet "${program.compositionSpec.id}" requires a retained child.`);
64
91
  }
65
- const { facets: _facets, ...unitConfigs } = program.materializationConfigs;
92
+ const { facets: _facets, guides: _guides, ...unitConfigs } = program.materializationConfigs;
66
93
  return new program.constructor({
67
94
  semanticSpec: program.semanticSpec,
68
95
  graphicSpec: seed.graphicSpec,
69
96
  resolvedScales: program.resolvedScales,
70
97
  materializationConfigs: freezeOwned({
71
98
  ...unitConfigs,
99
+ ...(seed.materializationConfigs.guides === undefined
100
+ ? {}
101
+ : { guides: seed.materializationConfigs.guides }),
72
102
  canvas: seed.materializationConfigs.canvas
73
103
  }),
74
104
  children: {},
@@ -81,19 +111,48 @@ function facetUnitTemplate(program) {
81
111
 
82
112
  function rederiveFacet(program, { scales, guides }) {
83
113
  const current = program.compositionSpec;
84
- const definition = resolveFacetDefinition(program.semanticSpec, {
85
- id: current.id,
86
- data: current.facet.data,
87
- field: current.facet.field,
88
- values: current.facet.values
89
- });
114
+ if (current.facet.repeat !== undefined) {
115
+ const template = facetUnitTemplate(program);
116
+ const definition = resolveRepeatDefinition(template, {
117
+ id: current.id,
118
+ ...current.facet.repeat
119
+ });
120
+ const derived = deriveRepeatChildren(
121
+ template,
122
+ definition,
123
+ usedFacetScalePolicies(template, scales),
124
+ true
125
+ );
126
+ return applyCompositionState(program, {
127
+ children: derived.children,
128
+ compositionSpec: {
129
+ ...current,
130
+ facet: { ...current.facet, scales, guides }
131
+ }
132
+ }, current.children);
133
+ }
134
+ const definition = current.facet.grid === undefined
135
+ ? resolveFacetDefinition(program.semanticSpec, {
136
+ id: current.id,
137
+ data: current.facet.data,
138
+ field: current.facet.field,
139
+ values: current.facet.values
140
+ })
141
+ : resolveFacetGridDefinition(program.semanticSpec, {
142
+ id: current.id,
143
+ data: current.facet.data,
144
+ rows: current.facet.grid.rows,
145
+ columns: current.facet.grid.columns,
146
+ combinations: current.facet.grid.combinations
147
+ });
90
148
  const request = Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
91
149
  program.semanticSpec.layers.some(
92
150
  layer => layer.encoding?.[channel]?.scale !== undefined
93
151
  ) ? [[channel, scales[channel]]] : []
94
152
  ));
95
153
  const normalized = normalizeFacetScalePolicies(program.semanticSpec, request);
96
- const derived = deriveFacetChildren(facetUnitTemplate(program), definition, {
154
+ const template = facetUnitTemplate(program);
155
+ const derived = deriveFacetChildren(template, definition, {
97
156
  closeInheritedAction: true,
98
157
  stripTitle: true,
99
158
  scales: request
@@ -106,7 +165,11 @@ function rederiveFacet(program, { scales, guides }) {
106
165
  guides
107
166
  }
108
167
  };
109
- return applyCompositionState(program, {
168
+ let parent = program._withoutMaterializationConfig(["guides", "legend"]);
169
+ for (const [kind, config] of Object.entries(template.guideConfigs.legend ?? {})) {
170
+ parent = parent._withLegendConfig(kind, config);
171
+ }
172
+ return applyCompositionState(parent, {
110
173
  children: derived.children,
111
174
  compositionSpec
112
175
  }, compositionSpec.children);
@@ -170,6 +233,242 @@ export const facet = action(
170
233
  }
171
234
  );
172
235
 
236
+ function resolveRepeatDefinition(program, args) {
237
+ if (!isPlainObject(args)) {
238
+ throw new TypeError("repeatCharts options must be a plain object.");
239
+ }
240
+ const id = validateUserId(args.id ?? "repeat", "Repeat id");
241
+ if (!["x", "y"].includes(args.channel)) {
242
+ throw new Error('repeatCharts channel must be "x" or "y".');
243
+ }
244
+ if (!Array.isArray(args.fields) || args.fields.length === 0 ||
245
+ args.fields.some(field => typeof field !== "string" || field.length === 0)) {
246
+ throw new TypeError("repeatCharts fields must be a non-empty array of field names.");
247
+ }
248
+ if (new Set(args.fields).size !== args.fields.length) {
249
+ throw new Error("repeatCharts fields must be unique.");
250
+ }
251
+ const eligible = program.semanticSpec.layers.filter(layer =>
252
+ layer.encoding?.x?.scale !== undefined &&
253
+ layer.encoding?.y?.scale !== undefined &&
254
+ layer.encoding?.[args.channel]?.field !== undefined &&
255
+ ["point", "line", "area", "bar", "rule", "tick", "rect"].includes(layer.mark?.type)
256
+ );
257
+ let target;
258
+ if (args.target !== undefined) {
259
+ target = validateUserId(args.target, "Repeat target");
260
+ if (!eligible.some(layer => layer.id === target)) {
261
+ throw new Error(`repeatCharts target "${target}" is not an eligible Cartesian mark.`);
262
+ }
263
+ } else if (eligible.length === 1) {
264
+ target = eligible[0].id;
265
+ } else {
266
+ throw new Error(
267
+ eligible.length === 0
268
+ ? "repeatCharts requires one complete Cartesian mark."
269
+ : "repeatCharts target is ambiguous; provide target."
270
+ );
271
+ }
272
+ if (program.semanticSpec.layers.length !== 1) {
273
+ throw new Error("repeatCharts currently supports one direct Cartesian mark only.");
274
+ }
275
+ const config = program.markConfigs[target] ?? {};
276
+ const composite = [
277
+ "boxPlot", "gradientPlot", "violinPlot", "regressionPlot", "endpointPlot",
278
+ "ecdfPlot", "raincloudPlot", "intervalPlot"
279
+ ].find(key => config[key] !== undefined);
280
+ if (composite !== undefined) {
281
+ throw new Error(`repeatCharts does not replace the ${composite} composite role.`);
282
+ }
283
+ const layer = findLayer(program, target);
284
+ const dataset = findDataset(program, layer.data);
285
+ if (dataset?.transform?.length > 0) {
286
+ throw new Error("repeatCharts does not rewrite a derived dataset dependency.");
287
+ }
288
+ const encoding = layer.encoding[args.channel];
289
+ return {
290
+ id,
291
+ target,
292
+ channel: args.channel,
293
+ fields: [...args.fields],
294
+ data: layer.data,
295
+ encoding,
296
+ cells: args.fields.map((field, index) => ({
297
+ id: `${id}-field-${index + 1}`,
298
+ field,
299
+ value: field
300
+ }))
301
+ };
302
+ }
303
+
304
+ function repeatEncodingArgs(definition, field) {
305
+ const encoding = definition.encoding;
306
+ return {
307
+ target: definition.target,
308
+ field,
309
+ fieldType: encoding.fieldType,
310
+ ...(encoding.temporalUnit === undefined ? {} : { temporalUnit: encoding.temporalUnit }),
311
+ ...(encoding.aggregate === undefined ? {} : { aggregate: encoding.aggregate }),
312
+ ...(encoding.bin === undefined ? {} : { bin: encoding.bin }),
313
+ ...(encoding.stack === undefined ? {} : { stack: encoding.stack }),
314
+ ...(encoding.weight === undefined ? {} : { weight: encoding.weight }),
315
+ scale: { id: encoding.scale }
316
+ };
317
+ }
318
+
319
+ function deriveRepeatChildren(base, definition, scales, closeInheritedAction) {
320
+ const template = base.semanticSpec.title.text === undefined
321
+ ? base
322
+ : base.removeTitle();
323
+ const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
324
+ cell.id,
325
+ template[definition.channel === "x" ? "encodeX" : "encodeY"](
326
+ repeatEncodingArgs(definition, cell.field)
327
+ )
328
+ ]));
329
+ return resolveFacetChildrenScales(
330
+ template,
331
+ definition.cells.map(cell => cell.id),
332
+ independentlyResolved,
333
+ scales,
334
+ closeInheritedAction
335
+ );
336
+ }
337
+
338
+ export const repeatCharts = action(
339
+ {
340
+ op: "repeatCharts",
341
+ description: "Repeat one direct Cartesian chart across an ordered field list."
342
+ },
343
+ function (args = {}) {
344
+ validateOptionObject(args, REPEAT_OPTIONS, "repeatCharts");
345
+ const guides = normalizeGuides(args.guides);
346
+ if (guides.axes === "outer") {
347
+ throw new Error("repeatCharts does not promote axes across different repeated fields.");
348
+ }
349
+ const definition = resolveRepeatDefinition(this, args);
350
+ const requestedScales = {
351
+ ...(args.scales ?? {}),
352
+ [definition.channel]: args.scales?.[definition.channel] ?? "independent"
353
+ };
354
+ const scalePolicies = normalizeFacetScalePolicies(
355
+ this.semanticSpec,
356
+ requestedScales
357
+ );
358
+ const derived = deriveRepeatChildren(
359
+ this,
360
+ definition,
361
+ requestedScales,
362
+ true
363
+ );
364
+ const preflight = resolveFacetLayout({
365
+ children: definition.cells.map(cell => ({
366
+ ...compositionChildDescriptor(cell.id, derived.children[cell.id]),
367
+ value: cell.value
368
+ })),
369
+ ...(Object.hasOwn(args, "columns") ? { columns: args.columns } : {}),
370
+ ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
371
+ ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
372
+ ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {}),
373
+ sharedLegend: guides.legend === "shared"
374
+ });
375
+ const compositionSpec = {
376
+ id: definition.id,
377
+ type: "facet",
378
+ children: definition.cells.map(cell => cell.id),
379
+ columns: preflight.columns,
380
+ gap: preflight.gap,
381
+ align: preflight.align,
382
+ padding: preflight.padding,
383
+ facet: {
384
+ data: definition.data,
385
+ values: definition.fields,
386
+ repeat: {
387
+ target: definition.target,
388
+ channel: definition.channel,
389
+ fields: definition.fields
390
+ },
391
+ scales: scalePolicies.channels,
392
+ guides
393
+ }
394
+ };
395
+ return applyCompositionState(
396
+ this._withMaterializationConfig(["facets", definition.id], {
397
+ headers: DEFAULT_HEADERS
398
+ }),
399
+ { children: derived.children, compositionSpec },
400
+ compositionSpec.children
401
+ );
402
+ }
403
+ );
404
+
405
+ export const facetGrid = action(
406
+ {
407
+ op: "facetGrid",
408
+ description: "Repeat one direct-source Cartesian chart across a row and column field grid."
409
+ },
410
+ function (args = {}) {
411
+ validateOptionObject(args, FACET_GRID_OPTIONS, "facetGrid");
412
+ const guides = normalizeGuides(args.guides);
413
+ const definition = resolveFacetGridDefinition(this.semanticSpec, args);
414
+ const scalePolicies = normalizeFacetScalePolicies(
415
+ this.semanticSpec,
416
+ args.scales ?? {}
417
+ );
418
+ const derived = deriveFacetChildren(this, definition, {
419
+ closeInheritedAction: true,
420
+ stripTitle: true,
421
+ scales: args.scales ?? {}
422
+ });
423
+ const preflight = resolveFacetLayout({
424
+ children: definition.cells.map(cell => ({
425
+ ...compositionChildDescriptor(cell.id, derived.children[cell.id]),
426
+ value: cell.value,
427
+ row: cell.row,
428
+ column: cell.column
429
+ })),
430
+ columns: definition.grid.columns.values.length,
431
+ ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
432
+ ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
433
+ ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {}),
434
+ sharedLegend: guides.legend === "shared"
435
+ });
436
+ const compositionSpec = {
437
+ id: definition.id,
438
+ type: "facet",
439
+ children: definition.cells.map(cell => cell.id),
440
+ columns: preflight.columns,
441
+ gap: preflight.gap,
442
+ align: preflight.align,
443
+ padding: preflight.padding,
444
+ facet: {
445
+ data: definition.data,
446
+ values: definition.cells.map(cell => cell.value),
447
+ grid: {
448
+ ...definition.grid,
449
+ cells: definition.cells.map(cell => ({
450
+ id: cell.id,
451
+ row: cell.row,
452
+ column: cell.column,
453
+ rowValue: cell.rowValue,
454
+ columnValue: cell.columnValue,
455
+ empty: cell.empty
456
+ }))
457
+ },
458
+ scales: scalePolicies.channels,
459
+ guides
460
+ }
461
+ };
462
+ return applyCompositionState(
463
+ this._withMaterializationConfig(["facets", definition.id], {
464
+ headers: DEFAULT_HEADERS
465
+ }),
466
+ { children: derived.children, compositionSpec },
467
+ compositionSpec.children
468
+ );
469
+ }
470
+ );
471
+
173
472
  export const editFacetHeaders = action(
174
473
  {
175
474
  op: "editFacetHeaders",
@@ -255,6 +554,9 @@ export const editFacetGuides = action(
255
554
  });
256
555
  const current = this.compositionSpec.facet;
257
556
  const guides = normalizeGuides({ ...current.guides, ...args });
557
+ if (current.repeat !== undefined && guides.axes === "outer") {
558
+ throw new Error("repeatCharts does not promote axes across different repeated fields.");
559
+ }
258
560
  return rederiveFacet(this, {
259
561
  scales: current.scales,
260
562
  guides
@@ -262,11 +564,107 @@ export const editFacetGuides = action(
262
564
  }
263
565
  );
264
566
 
567
+ function adoptUnitState(program, actionOwner) {
568
+ if (!(program instanceof actionOwner.constructor)) {
569
+ throw new TypeError("editFacetSource program must be a ChartProgram.");
570
+ }
571
+ if (program.compositionSpec !== undefined) {
572
+ throw new Error("editFacetSource program must be a complete unit ChartProgram.");
573
+ }
574
+ if (program.actionStack.length !== 0) {
575
+ throw new Error("editFacetSource program has an unfinished action stack.");
576
+ }
577
+ return new actionOwner.constructor({
578
+ semanticSpec: program.semanticSpec,
579
+ graphicSpec: program.graphicSpec,
580
+ resolvedScales: program.resolvedScales,
581
+ materializationConfigs: program.materializationConfigs,
582
+ children: {},
583
+ context: program.context,
584
+ trace: actionOwner.trace,
585
+ actionStack: actionOwner.actionStack,
586
+ actionSequence: actionOwner._actionSequence
587
+ });
588
+ }
589
+
590
+ export const editFacetSource = action(
591
+ {
592
+ op: "editFacetSource",
593
+ description: "Reapply one facet, grid, or repeat recipe to a revised complete unit program.",
594
+ scope: "composition"
595
+ },
596
+ function (args = {}) {
597
+ validateOptionObject(args, SOURCE_EDIT_OPTIONS, "editFacetSource");
598
+ requireFacetProgram(this, "editFacetSource");
599
+ const current = this.compositionSpec;
600
+ const facetConfig = this.materializationConfigs.facets?.[current.id];
601
+ const base = adoptUnitState(args.program, this);
602
+ let revised;
603
+ const scales = usedFacetScalePolicies(base, current.facet.scales);
604
+ if (current.facet.grid !== undefined) {
605
+ revised = base.facetGrid({
606
+ id: current.id,
607
+ data: current.facet.data,
608
+ rows: current.facet.grid.rows,
609
+ columns: current.facet.grid.columns,
610
+ combinations: current.facet.grid.combinations,
611
+ gap: current.gap,
612
+ align: current.align,
613
+ padding: current.padding,
614
+ scales,
615
+ guides: current.facet.guides
616
+ });
617
+ } else if (current.facet.repeat !== undefined) {
618
+ revised = base.repeatCharts({
619
+ id: current.id,
620
+ ...current.facet.repeat,
621
+ columns: current.columns,
622
+ gap: current.gap,
623
+ align: current.align,
624
+ padding: current.padding,
625
+ scales,
626
+ guides: current.facet.guides
627
+ });
628
+ } else {
629
+ revised = base.facet({
630
+ id: current.id,
631
+ data: current.facet.data,
632
+ field: current.facet.field,
633
+ values: current.facet.values,
634
+ columns: current.columns,
635
+ gap: current.gap,
636
+ align: current.align,
637
+ padding: current.padding,
638
+ scales,
639
+ guides: current.facet.guides
640
+ });
641
+ }
642
+ if (facetConfig !== undefined) {
643
+ revised = revised
644
+ ._withMaterializationConfig(["facets", current.id], facetConfig)
645
+ .materializeComposition();
646
+ }
647
+ if (revised.semanticSpec.title.text !== undefined) {
648
+ revised = revised.removeTitle();
649
+ }
650
+ if (this.semanticSpec.title.text !== undefined) {
651
+ revised = revised.createTitle({
652
+ ...this.titleConfig,
653
+ ...this.semanticSpec.title
654
+ });
655
+ }
656
+ return revised;
657
+ }
658
+ );
659
+
265
660
  export function registerFacetActions(ProgramClass) {
266
661
  ProgramClass.prototype.replayDerivedData = replayDerivedData;
267
662
  ProgramClass.prototype.composeFacetGuides = composeFacetGuides;
268
663
  ProgramClass.prototype.facet = facet;
664
+ ProgramClass.prototype.facetGrid = facetGrid;
665
+ ProgramClass.prototype.repeatCharts = repeatCharts;
269
666
  ProgramClass.prototype.editFacetHeaders = editFacetHeaders;
270
667
  ProgramClass.prototype.editFacetScales = editFacetScales;
271
668
  ProgramClass.prototype.editFacetGuides = editFacetGuides;
669
+ ProgramClass.prototype.editFacetSource = editFacetSource;
272
670
  }
@@ -63,12 +63,30 @@ function deriveCellProgram(
63
63
  histogramBoundaries,
64
64
  scales
65
65
  ) {
66
- let child = base.filterData({
67
- id: cell.data,
68
- source: definition.data,
66
+ const filters = cell.filters ?? [{
69
67
  field: definition.field,
70
- oneOf: [cell.value]
71
- });
68
+ value: cell.value
69
+ }];
70
+ let child = base;
71
+ let source = definition.data;
72
+ for (const [index, filter] of filters.entries()) {
73
+ const id = index === filters.length - 1
74
+ ? cell.data
75
+ : `${cell.id}-partition-${index + 1}-data`;
76
+ child = child.filterData({
77
+ id,
78
+ source,
79
+ field: filter.field,
80
+ oneOf: [filter.value]
81
+ });
82
+ source = id;
83
+ }
84
+ if (requireDataset(child, cell.data).values.length === 0) {
85
+ while (child.semanticSpec.layers.length > 0) {
86
+ child = child.removeMark({ target: child.semanticSpec.layers[0].id });
87
+ }
88
+ return child;
89
+ }
72
90
  const datasets = new Map([[definition.data, cell.data]]);
73
91
  for (const replay of definition.dependencies.replay) {
74
92
  const source = datasets.get(replay.source);
@@ -182,9 +200,35 @@ export function deriveFacetChildren(
182
200
  cell.id,
183
201
  deriveCellProgram(template, definition, cell, bins, scales)
184
202
  ]));
203
+ return resolveFacetChildrenScales(
204
+ template,
205
+ definition.cells.map(cell => cell.id),
206
+ independentlyResolved,
207
+ scales,
208
+ closeInheritedAction
209
+ );
210
+ }
211
+
212
+ export function resolveFacetChildrenScales(
213
+ template,
214
+ cellIds,
215
+ independentlyResolved,
216
+ scales = {},
217
+ closeInheritedAction = false
218
+ ) {
219
+ const populated = Object.fromEntries(cellIds.flatMap(id => {
220
+ const child = independentlyResolved[id];
221
+ if (child === undefined) {
222
+ throw new Error(`Facet derivation is missing child "${id}".`);
223
+ }
224
+ return child.semanticSpec.layers.length > 0 ? [[id, child]] : [];
225
+ }));
226
+ if (Object.keys(populated).length === 0) {
227
+ throw new Error("Facet derivation requires at least one populated cell.");
228
+ }
185
229
  const resolution = resolveFacetScaleDomains(
186
230
  template.semanticSpec,
187
- Object.fromEntries(Object.entries(independentlyResolved).map(([id, child]) => [
231
+ Object.fromEntries(Object.entries(populated).map(([id, child]) => [
188
232
  id,
189
233
  child.resolvedScales
190
234
  ])),
@@ -192,7 +236,11 @@ export function deriveFacetChildren(
192
236
  template.resolvedScales
193
237
  );
194
238
  const resolvedChildren = Object.fromEntries(
195
- Object.entries(independentlyResolved).map(([id, child]) => {
239
+ cellIds.map(id => {
240
+ const child = independentlyResolved[id];
241
+ if (child.semanticSpec.layers.length === 0) {
242
+ return [id, closeInheritedAction ? child._exitAction() : child];
243
+ }
196
244
  const resolved = applyResolvedDomains(
197
245
  child,
198
246
  id,
@@ -202,10 +250,11 @@ export function deriveFacetChildren(
202
250
  return [id, closeInheritedAction ? resolved._exitAction() : resolved];
203
251
  })
204
252
  );
253
+ const firstPopulatedId = Object.keys(populated)[0];
205
254
  const sharedScales = Object.fromEntries(
206
255
  Object.entries(resolution.scales)
207
256
  .filter(([, value]) => value.policy === "shared")
208
- .map(([id]) => [id, resolvedChildren[definition.cells[0].id].resolvedScales[id]])
257
+ .map(([id]) => [id, resolvedChildren[firstPopulatedId].resolvedScales[id]])
209
258
  );
210
259
  return freezeOwned({
211
260
  children: freezeOwned(resolvedChildren),
@@ -1,3 +1,4 @@
1
+ import { assertGuideOptions, guideConflict } from "../guides/reuse.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateKeys } from "../../core/validation.js";
3
4
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
@@ -120,23 +121,44 @@ export const rematerializeGradientPlotLegend = action(
120
121
  }
121
122
  );
122
123
 
124
+ function resolveDensityLegend(program, args) {
125
+ validateKeys(args, LEGEND_OPTIONS, "createGradientPlotLegend");
126
+ requireLegendOwner(program, args.owner);
127
+ if (args.position !== undefined && args.position !== "right") {
128
+ throw new Error('Gradient plot density legend currently requires position "right".');
129
+ }
130
+ if (args.title !== undefined && (typeof args.title !== "string" || args.title.length === 0)) {
131
+ throw new TypeError("Gradient plot density legend title must be a non-empty string.");
132
+ }
133
+ const ids = legendIds(args.owner);
134
+ const densityScale = `${args.owner}Density`;
135
+ const title = args.title ?? "Relative density";
136
+ return { ids, densityScale, title };
137
+ }
138
+
139
+ export function fulfillGradientPlotLegend(program, args) {
140
+ const { ids, densityScale } = resolveDensityLegend(program, args);
141
+ const occupied = program.semanticSpec.guides.legend?.color;
142
+ if (occupied !== undefined && occupied.scale !== densityScale) {
143
+ guideConflict("Gradient plot density legend uses a different scale or family");
144
+ }
145
+ const existing = Object.values(ids).filter(id => program.graphicSpec.objects[id] !== undefined);
146
+ if (existing.length === 0) return program.createGradientPlotLegend(args);
147
+ if (existing.length !== Object.keys(ids).length || occupied?.scale !== densityScale) {
148
+ guideConflict("Gradient plot density legend has incomplete ownership");
149
+ }
150
+ const { owner, ...explicit } = args;
151
+ assertGuideOptions(explicit, program.markConfigs[owner].gradientPlot.guides.legend, "Gradient plot density legend");
152
+ return program;
153
+ }
154
+
123
155
  export const createGradientPlotLegend = action(
124
156
  {
125
157
  op: "createGradientPlotLegend",
126
158
  description: "Create the neutral density legend owned by one gradient plot."
127
159
  },
128
160
  function (args = {}) {
129
- validateKeys(args, LEGEND_OPTIONS, "createGradientPlotLegend");
130
- requireLegendOwner(this, args.owner);
131
- if (args.position !== undefined && args.position !== "right") {
132
- throw new Error('Gradient plot density legend currently requires position "right".');
133
- }
134
- if (args.title !== undefined && (typeof args.title !== "string" || args.title.length === 0)) {
135
- throw new TypeError("Gradient plot density legend title must be a non-empty string.");
136
- }
137
- const ids = legendIds(args.owner);
138
- const densityScale = `${args.owner}Density`;
139
- const title = args.title ?? "Relative density";
161
+ const { ids, densityScale, title } = resolveDensityLegend(this, args);
140
162
  let next = this;
141
163
  for (const [property, value] of [
142
164
  [`scale[${densityScale}].type`, "sequential"],
@@ -1,3 +1,5 @@
1
+ import { applyFacadeGuides } from "../charts/shared.js";
2
+ import { fulfillGradientPlotLegend } from "./components.js";
1
3
  import { action } from "../../core/action.js";
2
4
  import { validateUserId } from "../../core/identifiers.js";
3
5
  import { isPlainObject } from "../../core/immutable.js";
@@ -253,19 +255,15 @@ export const materializeGradientPlot = action(
253
255
  if (guides !== false) {
254
256
  const wantsStandard = guides.axes !== false || guides.grid !== false;
255
257
  if (wantsStandard) {
256
- next = next.createGuides({
258
+ next = applyFacadeGuides(next, {
257
259
  axes: gradientAxesOptions(next.markConfigs[owner].gradientPlot),
258
260
  grid: guides.grid,
259
261
  legend: false
260
- });
262
+ }, owner, guides);
261
263
  }
262
264
  if (guides.legend !== false) {
263
265
  const legend = isPlainObject(guides.legend) ? guides.legend : {};
264
- next = next.createGradientPlotLegend({
265
- owner,
266
- title: legend.title ?? "Relative density",
267
- position: legend.position ?? "right"
268
- });
266
+ next = fulfillGradientPlotLegend(next, { owner, ...legend });
269
267
  }
270
268
  }
271
269
  return next._withContext({ currentMark: owner, currentData: layer.data });