ggaction 0.0.24 → 0.0.26

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 (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -1,3 +1,4 @@
1
+ import { prepareFacadeMissingData } from './missing.js';
1
2
  import { validateUserId } from "../../core/identifiers.js";
2
3
  import { action } from "../../core/action.js";
3
4
  import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
@@ -14,6 +15,7 @@ import { findDataset } from "../../selectors/datasets.js";
14
15
  import { findSemanticScale } from "../../selectors/scales.js";
15
16
  import {
16
17
  applyFacadeGuides,
18
+ retainFacadeAxisPresence,
17
19
  normalizeAppearance,
18
20
  normalizeFieldEncoding,
19
21
  normalizeGuides,
@@ -24,7 +26,7 @@ import {
24
26
  } from "./shared.js";
25
27
 
26
28
  const COMMON_OPTIONS = Object.freeze([
27
- "id", "data", "coordinate", "category", "orientation", "summary", "guides", "labels"
29
+ "id", "data", "coordinate", "category", "orientation", "summary", "guides", "labels", "missing"
28
30
  ]);
29
31
  const DOT_OPTIONS = Object.freeze([...COMMON_OPTIONS, "value", "point"]);
30
32
  const LOLLIPOP_OPTIONS = Object.freeze([...DOT_OPTIONS, "baseline", "stem"]);
@@ -48,7 +50,7 @@ const LABEL_OPTIONS = Object.freeze([
48
50
  const SUMMARY_OPERATIONS = Object.freeze(["mean", "median", "sum", "min", "max"]);
49
51
  const EDIT_OPTIONS = Object.freeze([
50
52
  "target", "data", "coordinate", "category", "value", "start", "end",
51
- "orientation", "summary", "baseline"
53
+ "orientation", "summary", "baseline", "missing"
52
54
  ]);
53
55
 
54
56
  function normalizeCategory(value, operation) {
@@ -124,9 +126,12 @@ function outputField(id, role) {
124
126
  return `__${id}_${role}`;
125
127
  }
126
128
 
127
- function prepareData(program, { id, data, category, measures, summary, operation }) {
129
+ function prepareData(program, { id, data, category, measures, summary, operation, missing }) {
130
+ const prepared = prepareFacadeMissingData(program, { id, source: data,
131
+ fields: [category.field, ...measures.map(item => item.field)], missing });
132
+ program = prepared.program; data = prepared.data;
128
133
  validateRows(program, data, category.field, measures.map(item => item.field), operation);
129
- if (summary === false) return { program, data, measures };
134
+ if (summary === false) return { program, data, measures, ownedData: prepared.ownedData };
130
135
  const derived = `${id}SummaryData`;
131
136
  const outputs = measures.map((item, index) => ({
132
137
  ...item,
@@ -144,6 +149,7 @@ function prepareData(program, { id, data, category, measures, summary, operation
144
149
  }))
145
150
  }),
146
151
  data: derived,
152
+ ownedData: prepared.ownedData,
147
153
  measures: outputs
148
154
  };
149
155
  }
@@ -249,10 +255,10 @@ function attachLabels(program, id, labels, defaultField) {
249
255
  });
250
256
  }
251
257
 
252
- function finish(program, { id, source, data, kind, roles, summary, orientation, options }) {
258
+ function finish(program, { id, source, data, kind, roles, summary, orientation, options, ownedData }) {
253
259
  return program._withMarkConfig(id, {
254
260
  ...program.markConfigs[id],
255
- endpointPlot: { kind, source, data, roles, summary, orientation, options }
261
+ endpointPlot: { kind, source, data, roles, summary, orientation, options, ownedData }
256
262
  });
257
263
  }
258
264
 
@@ -271,7 +277,7 @@ export const createDotPlot = /* @__PURE__ */ action(
271
277
  const guides = normalizeGuides(args.guides, operation);
272
278
  const labels = normalizeLabels(args.labels, operation, false);
273
279
  const prepared = prepareData(this, {
274
- id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation
280
+ id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation, missing: args.missing
275
281
  });
276
282
  const finalValue = prepared.measures[0];
277
283
  const bindings = positionBindings(id, category, finalValue, orientation, args.coordinate);
@@ -280,7 +286,8 @@ export const createDotPlot = /* @__PURE__ */ action(
280
286
  next = applyFacadeGuides(next, guides, id, guides);
281
287
  return finish(next, {
282
288
  id, source, data: prepared.data, kind: "dot", roles: { category, value }, summary, orientation,
283
- options: { id, data: source, coordinate: args.coordinate, category, value, orientation,
289
+ ownedData: prepared.ownedData,
290
+ options: { id, data: source, missing: args.missing, coordinate: args.coordinate, category, value, orientation,
284
291
  summary, point, guides, labels }
285
292
  });
286
293
  }
@@ -304,7 +311,7 @@ export const createLollipopPlot = /* @__PURE__ */ action(
304
311
  const guides = normalizeGuides(args.guides, operation);
305
312
  const labels = normalizeLabels(args.labels, operation, false);
306
313
  const prepared = prepareData(this, {
307
- id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation
314
+ id, data: source, category, measures: [{ ...value, role: "value" }], summary, operation, missing: args.missing
308
315
  });
309
316
  const finalValue = prepared.measures[0];
310
317
  const bindings = positionBindings(id, category, finalValue, orientation, args.coordinate);
@@ -323,7 +330,8 @@ export const createLollipopPlot = /* @__PURE__ */ action(
323
330
  return finish(next, {
324
331
  id, source, data: prepared.data, kind: "lollipop",
325
332
  roles: { category, value, baseline, stemId }, summary, orientation,
326
- options: { id, data: source, coordinate: args.coordinate, category, value, orientation,
333
+ ownedData: prepared.ownedData,
334
+ options: { id, data: source, missing: args.missing, coordinate: args.coordinate, category, value, orientation,
327
335
  summary, baseline, point, stem, guides, labels }
328
336
  });
329
337
  }
@@ -358,7 +366,7 @@ export const createDumbbellPlot = /* @__PURE__ */ action(
358
366
  { ...start, scale: sharedScale, role: "start" },
359
367
  { ...end, scale: sharedScale, role: "end" }
360
368
  ],
361
- summary, operation
369
+ summary, operation, missing: args.missing
362
370
  });
363
371
  const [finalStart, finalEnd] = prepared.measures;
364
372
  const startBindings = positionBindings(id, category, finalStart, orientation, args.coordinate);
@@ -396,7 +404,8 @@ export const createDumbbellPlot = /* @__PURE__ */ action(
396
404
  return finish(next, {
397
405
  id, source, data: prepared.data, kind: "dumbbell",
398
406
  roles: { category, start, end, startId, connectorId }, summary, orientation,
399
- options: { id, data: source, coordinate: args.coordinate, category, start, end, orientation,
407
+ ownedData: prepared.ownedData,
408
+ options: { id, data: source, missing: args.missing, coordinate: args.coordinate, category, start, end, orientation,
400
409
  summary, startPoint, endPoint, connector, guides, labels }
401
410
  });
402
411
  }
@@ -410,12 +419,12 @@ const KIND_METHOD = Object.freeze({
410
419
  dumbbell: "createDumbbellPlot"
411
420
  });
412
421
  const KIND_ROLES = Object.freeze({
413
- dot: new Set(["data", "coordinate", "category", "value", "orientation", "summary"]),
422
+ dot: new Set(["data", "coordinate", "category", "value", "orientation", "summary", "missing"]),
414
423
  lollipop: new Set([
415
- "data", "coordinate", "category", "value", "orientation", "summary", "baseline"
424
+ "data", "coordinate", "category", "value", "orientation", "summary", "baseline", "missing"
416
425
  ]),
417
426
  dumbbell: new Set([
418
- "data", "coordinate", "category", "start", "end", "orientation", "summary"
427
+ "data", "coordinate", "category", "start", "end", "orientation", "summary", "missing"
419
428
  ])
420
429
  });
421
430
 
@@ -494,7 +503,7 @@ export const editEndpointPlot = /* @__PURE__ */ action(
494
503
  if (invalid !== undefined) {
495
504
  throw new Error(`editEndpointPlot ${invalid} is not available for ${current.kind} plots.`);
496
505
  }
497
- const revised = { ...current.options };
506
+ const revised = { ...current.options, guides: retainFacadeAxisPresence(this, owner, current.options.guides) };
498
507
  for (const key of changes) revised[key] = args[key];
499
508
  const axes = { fromAxis: current.orientation === "vertical" ? "y" : "x",
500
509
  toAxis: revised.orientation === "vertical" ? "y" : "x" };
@@ -1,3 +1,4 @@
1
+ import { findSemanticScale } from "../../selectors/scales.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { isPlainObject } from "../../core/immutable.js";
3
4
  import {
@@ -71,7 +72,8 @@ function normalizeBinnedColor(value) {
71
72
  return { ...value };
72
73
  }
73
74
 
74
- function resolvedPosition(encoding, field, extent) {
75
+ function resolvedPosition(encoding, field, extent, program) {
76
+ if (encoding.scale?.id && findSemanticScale(program, encoding.scale.id)) return {field,fieldType:"quantitative",scale:encoding.scale};
75
77
  const scale = encoding.scale ?? {};
76
78
  return {
77
79
  field,
@@ -178,8 +180,8 @@ export const createHeatmap = /* @__PURE__ */ action(
178
180
  });
179
181
  const transform = requireDataset(binned, generatedData).transform[0];
180
182
  const resolved = transform.resolved;
181
- const xEncoding = resolvedPosition(x, transform.as.x0, resolved.extent.x);
182
- const yEncoding = resolvedPosition(y, transform.as.y0, resolved.extent.y);
183
+ const xEncoding = resolvedPosition(x, transform.as.x0, resolved.extent.x, this);
184
+ const yEncoding = resolvedPosition(y, transform.as.y0, resolved.extent.y, this);
183
185
  const next = binned
184
186
  .createRectMark({ id, data: generatedData, ...rect })
185
187
  .encodeX(positionArgs(xEncoding, { target: id, coordinate: args.coordinate }))
@@ -0,0 +1,17 @@
1
+ import { validateItemMissing } from '../../grammar/itemMissing.js';
2
+
3
+ // Complete-case source ownership for composite statistical charts. Keep the
4
+ // original source in the facade config; every child consumes this same view.
5
+ // Native filters preserve schemas, dependencies, trace and rematerialization.
6
+ export function prepareFacadeMissingData(program, { id, source, fields, missing }) {
7
+ const policy = validateItemMissing(missing ?? 'error', 'Chart missing');
8
+ if (policy === 'error') return { program, data: source, ownedData: [] };
9
+ let next = program, data = source;
10
+ const ownedData = [];
11
+ for (const [index, field] of [...new Set(fields.filter(Boolean))].entries()) {
12
+ const target = `${id}CompleteCases${index + 1}`;
13
+ next = next.filterData({ id: target, source: data, field, noneOf: [null], nulls: 'exclude' });
14
+ data = target; ownedData.unshift(target);
15
+ }
16
+ return { program: next, data, ownedData };
17
+ }
@@ -10,7 +10,6 @@ import { findDataset } from "../../selectors/datasets.js";
10
10
  import { resolveEligibleLayer } from "../../selectors/layers.js";
11
11
  import { planDerivedDataRevision } from "../../materialization/dataProvenance.js";
12
12
  import { applyLayerDataRematerialization } from "../../materialization/dependencies.js";
13
- import { deriveFoldRows, normalizeFoldTransform } from "../../grammar/fold.js";
14
13
  import {
15
14
  normalizeAppearance,
16
15
  normalizeEncoding,
@@ -33,7 +32,7 @@ const CATEGORY_SCALE_OPTIONS = Object.freeze([
33
32
  "id", "type", "domain", "range", "reverse", "paddingInner", "paddingOuter",
34
33
  "padding", "align"
35
34
  ]);
36
- const WIDE_OPTIONS = Object.freeze(["fields", "as"]);
35
+ const WIDE_OPTIONS = Object.freeze(["fields", "as", "summary", "normalize"]);
37
36
  const WIDE_AS_OPTIONS = Object.freeze(["key", "value"]);
38
37
  const LINE_OPTIONS = Object.freeze([
39
38
  "strokeWidth", "curve", "stroke", "opacity", "closed",
@@ -105,7 +104,15 @@ function normalizeWide(value, id, operation, minimum = 3) {
105
104
  if (key === foldedValue || fields.includes(key) || fields.includes(foldedValue)) {
106
105
  throw new Error(`${operation} wide aliases must be distinct from selected fields.`);
107
106
  }
108
- return { fields, as: { key, value: foldedValue } };
107
+ const summary = value.summary ?? false;
108
+ const normalize = value.normalize ?? false;
109
+ if (![false, "mean", "median"].includes(summary)) {
110
+ throw new TypeError(`${operation} wide.summary must be false, mean, or median.`);
111
+ }
112
+ if (![false, "minmax"].includes(normalize)) {
113
+ throw new TypeError(`${operation} wide.normalize must be false or minmax.`);
114
+ }
115
+ return { fields, as: { key, value: foldedValue }, summary, normalize };
109
116
  }
110
117
 
111
118
  function orderedCategories(rows, field, requested, operation) {
@@ -189,6 +196,33 @@ function scaleWithOrder(category, order, operation) {
189
196
  return { ...category, scale: { ...scale, domain: order } };
190
197
  }
191
198
 
199
+ // Keep summaries and normalization in the public data grammar. Original values
200
+ // remain available, and each derived dataset has ordinary trace/provenance.
201
+ function prepareWideData(program, { source, wide, groupBy, color, strokeDash, data, operation }) {
202
+ let next = program, input = source;
203
+ const ownedData = [];
204
+ if (wide.summary) {
205
+ input = `${data}Summary`;
206
+ const groups = [...new Set([...groupBy, color?.field, strokeDash?.field].filter(Boolean))];
207
+ next = next.createSummaryData({ id: input, source,
208
+ ...(groups.length ? { groupBy: groups } : {}),
209
+ aggregates: wide.fields.filter(field => !groups.includes(field)).map(field => ({ op: wide.summary, field, as: field })), missing: "error" });
210
+ ownedData.push(input);
211
+ }
212
+ const folded = wide.normalize ? `${data}Fold` : data;
213
+ next = next.createFoldData({ id: folded, source: input, fields: wide.fields, as: wide.as });
214
+ ownedData.push(folded);
215
+ const value = wide.normalize ? `${wide.as.value}Normalized` : wide.as.value;
216
+ if (wide.normalize) {
217
+ next = next.createNormalizedData({ id: data, source: folded, field: wide.as.value,
218
+ as: value, groupBy: wide.as.key, method: "minmax", zeroDenominator: "zero" });
219
+ ownedData.push(data);
220
+ }
221
+ validateRadarRows(findDataset(next, data).values, { category: wide.as.key,
222
+ value, groupBy, order: wide.fields }, operation);
223
+ return { next, data, value, ownedData };
224
+ }
225
+
192
226
  export const createRadarPlot = /* @__PURE__ */ action(
193
227
  {
194
228
  op: "createRadarPlot",
@@ -240,6 +274,7 @@ export const createRadarPlot = /* @__PURE__ */ action(
240
274
  let category;
241
275
  let radarValue;
242
276
  let order;
277
+ let radarPlot;
243
278
  if (longForm) {
244
279
  category = normalizeCategory(args.category, operation);
245
280
  radarValue = normalizeValue(args.value, operation);
@@ -252,7 +287,7 @@ export const createRadarPlot = /* @__PURE__ */ action(
252
287
  }, operation);
253
288
  } else {
254
289
  const wide = normalizeWide(args.wide, id, operation);
255
- if (sourceRows.length > 1 && groupBy.length === 0) {
290
+ if (sourceRows.length > 1 && groupBy.length === 0 && !wide.summary) {
256
291
  throw new Error(`${operation} wide rows require groupBy as series identity.`);
257
292
  }
258
293
  for (const field of wide.fields) readQuantitativeField(sourceRows, field);
@@ -271,21 +306,14 @@ export const createRadarPlot = /* @__PURE__ */ action(
271
306
  );
272
307
  }
273
308
  data = `${id}FoldData`;
274
- next = next.createFoldData({
275
- id: data,
276
- source,
277
- fields: wide.fields,
278
- as: wide.as
279
- });
280
- const foldedRows = findDataset(next, data).values;
309
+ const prepared = prepareWideData(next, { source, wide, groupBy: seriesFields,
310
+ color, strokeDash, data, operation });
311
+ next = prepared.next;
281
312
  category = { field: wide.as.key, fieldType: "nominal" };
282
- radarValue = { field: wide.as.value, fieldType: "quantitative" };
283
- validateRadarRows(foldedRows, {
284
- category: category.field,
285
- value: radarValue.field,
286
- groupBy: seriesFields,
287
- order
288
- }, operation);
313
+ radarValue = { field: prepared.value, fieldType: "quantitative",
314
+ ...(wide.normalize ? { scale: { domain: [0, 1], nice: false } } : {}) };
315
+ radarPlot = { source, wide, ownedData: prepared.ownedData };
316
+
289
317
  }
290
318
 
291
319
  category = scaleWithOrder(category, order, operation);
@@ -303,7 +331,8 @@ export const createRadarPlot = /* @__PURE__ */ action(
303
331
  });
304
332
  return next._withMarkConfig(id, {
305
333
  ...next.markConfigs[id],
306
- compositionRole: "radar"
334
+ compositionRole: "radar",
335
+ ...(radarPlot ? { radarPlot } : {})
307
336
  });
308
337
  }
309
338
  );
@@ -316,30 +345,34 @@ export const editRadarPlot = /* @__PURE__ */ action(
316
345
  function (args = {}) {
317
346
  const operation = "editRadarPlot";
318
347
  validateFacadeOptions(args, ["target", "wide"], operation);
319
- validateOptionObject(args.wide, ["fields"], `${operation} wide`);
348
+ validateOptionObject(args.wide, ["fields", "summary", "normalize"], `${operation} wide`);
320
349
  const owner = resolveEligibleLayer(this, {
321
350
  target: args.target,
322
351
  label: "wide Radar plot",
323
352
  predicate: layer => this.markConfigs[layer.id]?.compositionRole === "radar" &&
324
- findDataset(this, layer.data)?.transform?.[0]?.type === "fold"
353
+ (this.markConfigs[layer.id]?.radarPlot !== undefined ||
354
+ findDataset(this, layer.data)?.transform?.[0]?.type === "fold")
325
355
  });
326
356
  const previous = findDataset(this, owner.data);
327
- const prior = previous.transform[0];
328
- const source = findDataset(this, previous.source);
329
- const wide = normalizeWide({ ...args.wide, as: prior.as }, owner.id, operation, 2);
357
+ const stored = this.markConfigs[owner.id].radarPlot;
358
+ const prior = stored?.wide ?? previous.transform[0];
359
+ const source = findDataset(this, stored?.source ?? previous.source);
360
+ const wide = normalizeWide({ summary: prior.summary, normalize: prior.normalize, ...args.wide, as: prior.as,
361
+ fields: args.wide.fields }, owner.id, operation, 2);
330
362
  for (const field of wide.fields) readQuantitativeField(source.values, field);
331
- const transform = normalizeFoldTransform(wide, previous.id);
332
- const rows = deriveFoldRows(source.values, transform);
333
363
  const group = owner.encoding.group;
334
364
  const groupBy = group?.fields ?? (group?.field ? [group.field] : []);
335
- validateRadarRows(rows, { category: prior.as.key, value: prior.as.value,
336
- groupBy, order: wide.fields }, operation);
337
365
  const revision = planDerivedDataRevision(this, {
338
366
  owner: owner.id, role: "RadarData", previous: previous.id, consumers: [owner.id]
339
367
  });
340
- let next = this.createFoldData({
341
- id: revision.id, source: source.id, fields: wide.fields, as: prior.as
342
- }).rebindLayerData(revision.rebinds[0]);
368
+ const prepared = prepareWideData(this, { source: source.id, wide, groupBy,
369
+ color: owner.encoding.color, strokeDash: owner.encoding.strokeDash,
370
+ data: revision.id, operation });
371
+ let next = prepared.next.rebindLayerData(revision.rebinds[0]);
372
+ if (Boolean(prior.normalize) !== Boolean(wide.normalize)) {
373
+ next = next.encodeR({ target: owner.id, field: prepared.value,
374
+ fieldType: "quantitative", scale: { domain: wide.normalize ? [0, 1] : "auto", nice: false } });
375
+ }
343
376
  const theta = owner.encoding.theta.scale;
344
377
  const revisedValues = config => {
345
378
  const full = config.inferredValues || !Array.isArray(config.values) ||
@@ -359,7 +392,11 @@ export const editRadarPlot = /* @__PURE__ */ action(
359
392
  next = next.editThetaScale({ target: owner.id, domain: wide.fields });
360
393
  next = applyLayerDataRematerialization(next, owner.id);
361
394
  if (next.markConfigs[owner.id]?.closed !== true) next = next.editLineMark({ target: owner.id, closed: true });
362
- return next.releaseDerivedData(revision.release)
363
- ._withContext({ currentMark: owner.id, currentData: source.id });
395
+ next = next._withMarkConfig(owner.id, { ...next.markConfigs[owner.id],
396
+ radarPlot: { source: source.id, wide, ownedData: prepared.ownedData } });
397
+ for (const id of [...(stored?.ownedData ?? [previous.id])].reverse()) {
398
+ next = next.releaseDerivedData({ id });
399
+ }
400
+ return next._withContext({ currentMark: owner.id, currentData: source.id });
364
401
  }
365
402
  );
@@ -1,3 +1,4 @@
1
+ import { prepareFacadeMissingData } from './missing.js';
1
2
  import { action } from "../../core/action.js";
2
3
  import { isPlainObject } from "../../core/immutable.js";
3
4
  import { validateUserId } from "../../core/identifiers.js";
@@ -5,6 +6,7 @@ import { validateOptionObject } from "../../core/validation.js";
5
6
  import { findDataset } from "../../selectors/datasets.js";
6
7
  import { findLayer } from "../../selectors/layers.js";
7
8
  import { findSemanticScale } from "../../selectors/scales.js";
9
+ import { resolveColorScaleDefinition } from "../scales/definitions.js";
8
10
  import { retainStatisticalDependents, restoreStatisticalDependents } from "../marks/dependentReferences.js";
9
11
  import { resolveViolinRoles } from "../violinPlots/create.js";
10
12
  import {
@@ -18,11 +20,11 @@ import {
18
20
 
19
21
  const CREATE_OPTIONS = Object.freeze([
20
22
  "id", "data", "coordinate", "category", "value", "orientation", "side",
21
- "density", "summary", "points", "color", "guides"
23
+ "density", "summary", "points", "color", "guides", "missing"
22
24
  ]);
23
25
  const EDIT_OPTIONS = Object.freeze([
24
26
  "target", "data", "category", "value", "orientation", "side", "density",
25
- "summary", "points", "color"
27
+ "summary", "points", "color", "missing"
26
28
  ]);
27
29
  const DENSITY_OPTIONS = Object.freeze([
28
30
  "bandwidth", "extent", "steps", "kernel", "normalization", "width", "area"
@@ -275,6 +277,12 @@ function addPoints(program, config, positions) {
275
277
  }
276
278
 
277
279
  function buildRaincloud(program, config) {
280
+ const original = config;
281
+ const prepared = prepareFacadeMissingData(program, { id: config.id, source: config.source,
282
+ fields: [config.category.field, config.value.field, config.color?.field,
283
+ config.points?.size?.field, config.points?.shape?.field], missing: config.missing });
284
+ program = prepared.program;
285
+ config = { ...config, source: prepared.data };
278
286
  const positions = rolePositions(config.category, config.value, config.orientation);
279
287
  let next = program;
280
288
  if (config.density !== false) {
@@ -308,7 +316,7 @@ function buildRaincloud(program, config) {
308
316
  .filter(id => findLayer(next, id) !== undefined);
309
317
  return next._withMarkConfig(ownerId, {
310
318
  ...next.markConfigs[ownerId],
311
- raincloudPlot: { ...config, ownerId, ownedChildIds, materialized: true }
319
+ raincloudPlot: { ...original, ownedData: prepared.ownedData, ownerId, ownedChildIds, materialized: true }
312
320
  })._withContext({ currentMark: ownerId, currentData: config.source });
313
321
  }
314
322
 
@@ -317,7 +325,11 @@ function alignExistingRoleScales(program, config) {
317
325
  for (const encoding of [config.category, config.value, config.color]) {
318
326
  const scale = encoding?.scale;
319
327
  if (!isPlainObject(scale) || findSemanticScale(next, scale.id) === undefined) continue;
320
- const patch = Object.fromEntries(Object.entries(scale).filter(
328
+ // Rebuilding removes the owned consumers first. Normalize the known color
329
+ // role before editing its retained resource, so palette does not depend on
330
+ // a temporarily absent consumer to infer that role.
331
+ const definition = encoding === config.color ? resolveColorScaleDefinition(next, scale) : scale;
332
+ const patch = Object.fromEntries(Object.entries(definition).filter(
321
333
  ([property, value]) => property !== "id" && value !== undefined
322
334
  ));
323
335
  if (Object.keys(patch).length > 0) next = next.editScale({ id: scale.id, ...patch });
@@ -358,6 +370,7 @@ function normalizeCreate(program, args, operation) {
358
370
  return {
359
371
  id,
360
372
  source,
373
+ missing: args.missing ?? "error",
361
374
  ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
362
375
  category,
363
376
  value,
@@ -419,6 +432,7 @@ export const editRaincloudPlot = /* @__PURE__ */ action(
419
432
  const candidateArgs = {
420
433
  id: current.id,
421
434
  data: Object.hasOwn(args, "data") ? args.data : current.source,
435
+ missing: args.missing ?? current.missing,
422
436
  ...(current.coordinate === undefined ? {} : { coordinate: current.coordinate }),
423
437
  category,
424
438
  value: Object.hasOwn(args, "value") ? args.value : current.value,
@@ -24,7 +24,7 @@ const TICK_OPTIONS = Object.freeze([
24
24
  "clip", "length", "stroke", "strokeWidth", "opacity", ...STROKE_STYLE_PROPERTIES
25
25
  ]);
26
26
  const STRIP_OPTIONS = Object.freeze([
27
- "id", "data", "coordinate", "x", "y", "color", "size", "shape", "point", "jitter", "guides"
27
+ "id", "data", "coordinate", "x", "y", "color", "size", "shape", "point", "jitter", "guides", "missing"
28
28
  ]);
29
29
  const POINT_OPTIONS = Object.freeze([
30
30
  "clip", "radius", "shape", "fill", "opacity", "stroke", "strokeWidth",
@@ -260,7 +260,7 @@ export const createStripPlot = /* @__PURE__ */ action(
260
260
  operation
261
261
  );
262
262
 
263
- let next = this.createPointMark({ id, data, ...point });
263
+ let next = this.createPointMark({ id, data, ...point, ...(args.missing === undefined ? {} : { missing: args.missing }) });
264
264
  if (args.coordinate !== undefined) {
265
265
  next = next.createCoordinate({ id: args.coordinate, type: "cartesian", layers: [id] });
266
266
  }
@@ -173,6 +173,23 @@ export function applyFacadeGuides(program, guides, target, explicitGuides = guid
173
173
  return fulfillFacadeGuides(program, guides, target, explicitGuides);
174
174
  }
175
175
 
176
+ // Rebuilding a facade must not resurrect an axis removed after construction.
177
+ // This also lets callers fit a changed variable by removing its old axis and
178
+ // recreating it against the new domain through ordinary public guide actions.
179
+ export function retainFacadeAxisPresence(program, owner, guides) {
180
+ if (guides === false) return false;
181
+ const axes = { ...guides?.axes };
182
+ for (const channel of ['x', 'y']) {
183
+ if (!owner.encoding[channel]?.scale) continue;
184
+ const present = Object.entries(program.semanticSpec.guides.axis ?? {}).some(([id, axis]) =>
185
+ (axis.channel ?? id) === channel && axis.scale === owner.encoding[channel].scale &&
186
+ (axis.coordinate ?? owner.coordinate) === owner.coordinate);
187
+ if (!present) axes[channel] = false;
188
+ else axes[channel] ??= {};
189
+ }
190
+ return { ...guides, axes };
191
+ }
192
+
176
193
  export function normalizeArcCategory(value, operation) {
177
194
  const label = `${operation} category`;
178
195
  const category = normalizeFieldEncoding(value, label);
@@ -1,3 +1,5 @@
1
+ import { normalizeCompositionLegends } from "../../core/compositionState.js";
2
+ import { normalizeCompositionShares } from "../../core/compositionScales.js";
1
3
  import { resolveGraphicBounds } from "../../layout/canvas.js";
2
4
  import { inheritTextMetrics } from "../textMetrics/index.js";
3
5
  import { action, closedAction } from "../../core/action.js";
@@ -17,10 +19,10 @@ import { materializeFacetGraphics } from "../../materialization/facets.js";
17
19
  import { replayInheritedThemeFrames } from "../theme/composition.js";
18
20
 
19
21
  const CONCAT_OPTIONS = Object.freeze([
20
- "id", "programs", "gap", "align", "padding"
22
+ "id", "programs", "gap", "align", "padding", "share", "legends"
21
23
  ]);
22
24
  const LAYOUT_EDIT_OPTIONS = Object.freeze([
23
- "columns", "gap", "spacing", "topLane", "align", "padding"
25
+ "columns", "gap", "spacing", "topLane", "align", "padding", "legends"
24
26
  ]);
25
27
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
26
28
  const INSERTION_OPTIONS = Object.freeze(["id", "program", "before", "after"]);
@@ -94,7 +96,7 @@ function normalizeCompositionInput(args, direction) {
94
96
  direction,
95
97
  children: entries.map(childDescriptor),
96
98
  ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
97
- ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
99
+ ...(Object.hasOwn(args, "align") ? { align: args.align === "plot" ? "start" : args.align } : {}),
98
100
  ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {})
99
101
  });
100
102
  return {
@@ -106,8 +108,10 @@ function normalizeCompositionInput(args, direction) {
106
108
  direction,
107
109
  children: ids,
108
110
  gap: layout.gap,
109
- align: layout.align,
110
- padding: layout.padding
111
+ align: args.align === "plot" ? "plot" : layout.align,
112
+ padding: layout.padding,
113
+ ...(Object.hasOwn(args, "legends") ? { legends: normalizeCompositionLegends(args.legends) } : {}),
114
+ ...(args.share === undefined ? {} : { share: normalizeCompositionShares(args.share) })
111
115
  }
112
116
  };
113
117
  }
@@ -175,6 +179,7 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
175
179
  throw new TypeError("editCompositionLayout requires at least one layout option.");
176
180
  }
177
181
  const current = this.compositionSpec;
182
+ if (Object.hasOwn(args, "legends") && current.type === "facet") throw new Error("Facet uses editFacetGuides for legend placement.");
178
183
  if (current.type !== "facet" && Object.hasOwn(args, "columns")) {
179
184
  throw new Error(
180
185
  "editCompositionLayout columns is available only on a facet composition."
@@ -218,7 +223,7 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
218
223
  program: this.children[id]
219
224
  })),
220
225
  gap: option(args, "gap", current.gap),
221
- align: option(args, "align", current.align),
226
+ align: option(args, "align", current.align) === "plot" ? "start" : option(args, "align", current.align),
222
227
  padding
223
228
  });
224
229
  }
@@ -227,11 +232,12 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
227
232
  children: this.children,
228
233
  compositionSpec: {
229
234
  ...currentLayoutSpec,
235
+ ...(Object.hasOwn(args, "legends") ? { legends: normalizeCompositionLegends(args.legends) } : {}),
230
236
  ...(current.type === "facet" ? { columns: layout.columns,
231
237
  ...(layout.spacing !== undefined ? { spacing: layout.spacing } : current.spacing !== undefined ? { spacing: "canvas" } : {}),
232
238
  ...(layout.topLane === undefined ? {} : { topLane: layout.topLane }) } : {}),
233
239
  gap: layout.gap,
234
- align: layout.align,
240
+ align: current.type !== "facet" && option(args, "align", current.align) === "plot" ? "plot" : layout.align,
235
241
  padding: layout.padding
236
242
  }
237
243
  });
@@ -376,7 +382,18 @@ const reorderCompositionChildren = /* @__PURE__ */ closedAction(
376
382
  }
377
383
  );
378
384
 
385
+ const editCompositionScales = /* @__PURE__ */ closedAction({
386
+ op: "editCompositionScales", description: "Edit shared scale policies across adjacent chart views.", scope: "composition"
387
+ }, ["share"], function ({ share } = {}) {
388
+ requireConcat(this, "editCompositionScales");
389
+ if (share === undefined || !Object.keys(share).length) throw new Error("editCompositionScales requires a nonempty share edit.");
390
+ const patch = normalizeCompositionShares(share);
391
+ return applyCompositionState(this, { children: this.children,
392
+ compositionSpec: { ...this.compositionSpec, share: { ...this.compositionSpec.share, ...patch } } });
393
+ });
394
+
379
395
  export function registerCompositionActions(ProgramClass) {
396
+ ProgramClass.prototype.editCompositionScales = editCompositionScales;
380
397
  ProgramClass.prototype.useProgram = useProgram;
381
398
  ProgramClass.prototype.materializeComposition = materializeComposition;
382
399
  ProgramClass.prototype.editCompositionLayout = editCompositionLayout;