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
@@ -0,0 +1,90 @@
1
+ import { adjacentLegendPresentation } from "../../materialization/compositionLegends.js";
2
+ import { facetUnitTemplate } from "../facets/actions.js";
3
+ import { compositionPlotBounds } from "../../materialization/composition.js";
4
+ import { closedAction, getWrappedActionMetadata } from "../../core/action.js";
5
+ import { validateOptionObject } from "../../core/validation.js";
6
+ import { validateUserId } from "../../core/identifiers.js";
7
+ import { resolveAdjacentSource } from "../../materialization/adjacentSource.js";
8
+ import { requireMaterializedDataset } from "../../selectors/datasets.js";
9
+ import { findSemanticScale } from "../../selectors/scales.js";
10
+ import { inheritTextMetrics } from "../textMetrics/index.js";
11
+ import { resolveEffectiveThemeFrame } from "../theme/state.js";
12
+ import { normalizeCompositionShares } from "../../core/compositionScales.js";
13
+ import { hconcatAction, vconcatAction } from "./actions.js";
14
+
15
+ import { plotComposition } from "../../selectors/plotComposition.js";
16
+ import { inferCompositionOptions, constructorChannels } from "../../grammar/plotComposition.js";
17
+ import { COMPOSITION_PLOT_ACTIONS } from "../../grammar/plotComposition.js";
18
+ export const ADJACENT_PLOT_ACTIONS = COMPOSITION_PLOT_ACTIONS;
19
+
20
+ const createAdjacentPlot = /* @__PURE__ */ closedAction({
21
+ op: "createAdjacentPlot", description: "Add a chart beside the current view with shared scales and aligned plots.", scope: "any"
22
+ }, ["id", "source", "position", "plot", "share", "gap", "align", "legends"], function (args = {}) {
23
+ const grouped = this.compositionSpec && this.compositionSpec.type !== "facet";
24
+ const { view: sourceView, layer: source } = resolveAdjacentSource(this, args.source);
25
+ const template = sourceView.compositionSpec ? facetUnitTemplate(sourceView, true) : sourceView;
26
+ const position = args.position ?? "right";
27
+ if (!["left", "right", "top", "bottom"].includes(position)) throw new Error("Adjacent position must be left, right, top, or bottom.");
28
+ const horizontal = ["left", "right"].includes(position);
29
+ const share = { ...normalizeCompositionShares(args.share ?? { [horizontal ? "y" : "x"]: true }) };
30
+ const plot = args.plot ?? { action: "createLinePlot", options: {} };
31
+ validateOptionObject(plot, ["action", "options", "inherit"], "Adjacent plot");
32
+ const action = plot.action ?? "createLinePlot";
33
+ if (!ADJACENT_PLOT_ACTIONS.includes(action) || !getWrappedActionMetadata(this[action])) throw new Error(`Unknown adjacent plot action "${action}".`);
34
+ const id = args.id === undefined ? "adjacent" : validateUserId(args.id, "Adjacent view id");
35
+ const sourceId = grouped ? this.compositionSpec.id : source.id;
36
+ if (id === sourceId) throw new Error("Adjacent view id must differ from its source view id.");
37
+ const supplied = plot.options ?? {};
38
+ if (supplied === null || typeof supplied !== "object" || Array.isArray(supplied)) throw new TypeError("Adjacent plot options must be an object.");
39
+ if (Object.hasOwn(supplied, "id")) throw new Error("Adjacent plot id is owned by the adjacent view; use the top-level id.");
40
+ const meaning = plotComposition(template,source.id);
41
+ const dataset = requireMaterializedDataset(template, supplied.data ?? meaning.data);
42
+ const bounds = compositionPlotBounds(this);
43
+ const sourceBounds = compositionPlotBounds(sourceView);
44
+ const canvas = this.graphicSpec.objects.canvas.properties;
45
+ let child = new this.constructor().createCanvas({ plot: { width: grouped && horizontal ? sourceBounds.width : bounds.width,
46
+ height: grouped && !horizontal ? sourceBounds.height : bounds.height },
47
+ margin: template.materializationConfigs.canvas.margin, background: canvas.background });
48
+ const theme = resolveEffectiveThemeFrame(template.materializationConfigs.theme);
49
+ if (theme) child = child.applyTheme({ theme: { base: theme.name, tokens: theme.tokens } });
50
+ child = inheritTextMetrics(template, child);
51
+ // Keep source tables available for subsequent authoring. A summary used by
52
+ // this constructor must not erase unused input variables from its new view.
53
+ // These remain independent immutable snapshots, not cross-view dependencies.
54
+ for (const input of template.semanticSpec.datasets) {
55
+ if (input.id !== dataset.id && (input.source || !Array.isArray(input.values))) continue;
56
+ child = child.createData({ id: input.id, values: input.values });
57
+ }
58
+ const options = { ...inferCompositionOptions(meaning,action,supplied,plot.inherit), id, data:dataset.id };
59
+ const roles=constructorChannels(action,options,meaning);
60
+ if(args.share===undefined) {
61
+ for(const channel of Object.keys(share)) {
62
+ const from=meaning.channels[channel],to=roles[channel];
63
+ if(!from||!to||from.field!==to.field||JSON.stringify(from.measurement.operations)!==JSON.stringify(to.operations)||
64
+ (meaning.coordinate?.type??'cartesian')!=='cartesian') delete share[channel];
65
+ }
66
+ }
67
+ for(const channel of ['x','y','color']) {
68
+ const role=meaning.channels[channel],selected=options[channel];
69
+ if(!role || (channel!=='color'&&!share[channel]) || !selected || selected.field!==role.field)continue;
70
+ const scale=findSemanticScale(template,role.scale);
71
+ if(scale) { const {id:_scaleId,...definition}=scale; if(selected.scale?.palette!==undefined) delete definition.range; options[channel]={...selected,scale:{...definition,...selected.scale}}; }
72
+ }
73
+ child = child[action](options);
74
+ if (share.color) {
75
+ const presentation = adjacentLegendPresentation(sourceView, source.id, child);
76
+ if (presentation) child = child.editLegend(presentation);
77
+ }
78
+ const original = new this.constructor({ ...this, actionStack: [], actionSequence: this._actionSequence });
79
+ const parent = new this.constructor({ trace: this.trace, actionStack: this.actionStack, actionSequence: this._actionSequence });
80
+ const entries = [{ id: sourceId, program: original }, { id, program: child }];
81
+ if (["left", "top"].includes(position)) entries.reverse();
82
+ return (horizontal ? hconcatAction : vconcatAction).call(parent, {
83
+ programs: entries, align: args.align ?? "plot", ...(args.gap === undefined ? {} : { gap: args.gap }),
84
+ share, legends: args.legends === undefined ? "collect" : args.legends
85
+ });
86
+ });
87
+
88
+ export function registerAdjacentPlotActions(ProgramClass) {
89
+ ProgramClass.prototype.createAdjacentPlot = createAdjacentPlot;
90
+ }
@@ -1 +1,11 @@
1
- export { registerCompositionActions } from "./actions.js";
1
+ import { registerActionTargetDispatcher } from "../../core/action.js";
2
+ import { dispatchActionTargets } from "./targets.js";
3
+ import { registerCompositionActions as registerStructureActions } from "./actions.js";
4
+ import { registerAdjacentPlotActions } from "./adjacent.js";
5
+ import { registerLayerPlotActions } from "./layer.js";
6
+ export function registerCompositionActions(ProgramClass) {
7
+ registerActionTargetDispatcher(ProgramClass, dispatchActionTargets);
8
+ registerStructureActions(ProgramClass);
9
+ registerAdjacentPlotActions(ProgramClass);
10
+ registerLayerPlotActions(ProgramClass);
11
+ }
@@ -0,0 +1,42 @@
1
+ import { closedAction } from '../../core/action.js';
2
+ import { planLayerPlot } from './layerPlan.js';
3
+ import { findLayer } from '../../selectors/layers.js';
4
+ import { plotComposition } from '../../selectors/plotComposition.js';
5
+
6
+ const createLayerPlot = /* @__PURE__ */ closedAction({op:'createLayerPlot',description:'Layer a native chart over a compatible source using shared coordinates and scales.'},
7
+ ['id','source','plot','placement'],function(args={}) {
8
+ const plan=planLayerPlot(this,args);
9
+ let next=this;
10
+ for (const edit of plan.scaleEdits) next=next.editScale(edit);
11
+ next=next[plan.action](plan.options);
12
+ for (const [channel, scale] of Object.entries(plan.rebind)) {
13
+ const owner = plotComposition(next, plan.id);
14
+ const layer = findLayer(next, owner.owner);
15
+ const method = {x:'encodeX',y:'encodeY',theta:'encodeTheta',radius:'encodeR'}[channel];
16
+ const {field,fieldType} = layer.encoding[channel];
17
+ next = next[method]({target:owner.owner,field,fieldType,scale:{id:scale}});
18
+ }
19
+ if(plan.deferredColor) next=next[plan.action==='createRugPlot'?'encodeStroke':'encodeColor']({target:plan.id,...plan.deferredColor});
20
+ const owner=plotComposition(next,plan.id);
21
+ const existing=new Set(this.semanticSpec.layers.map(layer=>layer.id));
22
+ const added=new Set(next.semanticSpec.layers.filter(layer=>!existing.has(layer.id)).map(layer=>layer.id));
23
+ if(!owner.ownedMarks.every(id=>added.has(id))) throw new Error('A layer must own its new compound marks independently.');
24
+ // Preserve the native mark block as one unit. Concrete roots keep their
25
+ // relative order; guides and unrelated graphics retain their positions.
26
+ const reorder = order => {
27
+ const addedRoots=order.filter(id=>added.has(id));
28
+ if(!addedRoots.length) return order;
29
+ const rest=order.filter(id=>!added.has(id));
30
+ const anchors=rest.map((id,index)=>existing.has(id)?index:-1).filter(i=>i>=0);
31
+ const at=anchors.length?(plan.placement==='back'?anchors[0]:anchors.at(-1)+1):rest.length;
32
+ rest.splice(at,0,...addedRoots); return rest;
33
+ };
34
+ for (const [id, graphic] of Object.entries(next.graphicSpec.objects)) {
35
+ if (!graphic.children) continue;
36
+ const children = reorder(graphic.children);
37
+ if (children.some((child, index) => child !== graphic.children[index]))
38
+ next = next.editGraphics({ target:id, property:'children', value:children });
39
+ }
40
+ return next;
41
+ });
42
+ export function registerLayerPlotActions(ProgramClass){ProgramClass.prototype.createLayerPlot=createLayerPlot;}
@@ -0,0 +1,176 @@
1
+ import { isPlainObject } from '../../core/immutable.js';
2
+ import { validateUserId } from '../../core/identifiers.js';
3
+ import { validateOptionObject } from '../../core/validation.js';
4
+ import { hasLayer } from '../../selectors/layers.js';
5
+ import { requireMaterializedDataset } from '../../selectors/datasets.js';
6
+ import { normalizeGroupFields, validatePathSeriesAppearance } from '../../grammar/pathSeries.js';
7
+ import { deriveLineSeries, resolveLineBins } from '../../grammar/lineSeries.js';
8
+ import { findSemanticScale } from '../../selectors/scales.js';
9
+ import { plotComposition, measurementUnit } from '../../selectors/plotComposition.js';
10
+ import { inferCompositionOptions, constructorChannels, PLOT_COMPOSITION_ROLES, fieldEncoding } from '../../grammar/plotComposition.js';
11
+
12
+ const get=(object,path)=>path.split('.').reduce((o,k)=>o?.[k],object);
13
+ function set(object,path,value) {
14
+ const parts=path.split('.'),last=parts.pop(); let node=object;
15
+ for(const key of parts) { node[key]={...fieldEncoding(node[key])}; node=node[key]; }
16
+ node[last]=value;
17
+ }
18
+ function bindScale(program, options, path, id) {
19
+ if(!id) throw new Error(`The source has no scale for ${path}.`);
20
+ const requested=get(options,path), stored=findSemanticScale(program,id);
21
+ if(requested?.id!==undefined && requested.id!==id) throw new Error(`${path} must reuse source scale "${id}".`);
22
+ for(const [key,value] of Object.entries(requested??{})) {
23
+ if(key==='id') continue;
24
+ // Explicit automatic limits are compatible with an automatic source. A
25
+ // shared resource must never be silently redefined by a new consumer.
26
+ if(JSON.stringify(value)!==JSON.stringify(key==='palette'?stored?.range?.palette:stored?.[key])) throw new Error(`${path}.${key} conflicts with the shared source scale. Edit that scale first.`);
27
+ }
28
+ set(options,path,{ ...stored, id });
29
+ }
30
+ export function planLayerPlot(program,args={}) {
31
+ if(program.compositionSpec) throw new Error('Select one chart view; use editFacetSource for a faceted chart.');
32
+ const id=validateUserId(args.id??'layer','Layer plot id');
33
+ if(hasLayer(program,id)||Object.values(program.markConfigs).some(c=>c.raincloudPlot?.id===id)) throw new Error(`Layer plot "${id}" already exists. Supply a unique id.`);
34
+ const placement=args.placement??'front';
35
+ if(!['front','back'].includes(placement)) throw new Error('Layer placement must be front or back.');
36
+ const spec=args.plot;
37
+ validateOptionObject(spec,['action','options','inherit'],'Layer plot');
38
+ if(!PLOT_COMPOSITION_ROLES[spec?.action]) throw new Error('Layer plot requires a supported native plot.action.');
39
+ const supplied=spec.options??{};
40
+ if(!isPlainObject(supplied)) throw new TypeError('Layer plot options must be an object.');
41
+ if(Object.hasOwn(supplied,'id')) throw new Error('Use the top-level layer id, not plot.options.id.');
42
+ const source=plotComposition(program,args.source), kind=PLOT_COMPOSITION_ROLES[spec.action];
43
+ if(args.source && args.source!==source.owner && program.markConfigs[source.owner]?.raincloudPlot?.id!==args.source) throw new Error('Layer source must be an independent plot owner, not one of its generated marks.');
44
+ const family=['polar','radial','radar'].includes(kind)?'polar':kind==='parallel'?'parallel':'cartesian';
45
+ if(source.coordinate?.type!==family) throw new Error(`Cannot overlay ${family} and ${source.coordinate?.type} coordinates. Add an adjacent chart instead.`);
46
+ if(supplied.coordinate!==undefined&&supplied.coordinate!==source.coordinate.id) throw new Error('Layer plots must reuse the source coordinate.');
47
+ const options=inferCompositionOptions(source,spec.action,supplied,spec.inherit);
48
+ if (['createLinePlot','createPolarLinePlot','createAreaPlot'].includes(spec.action)) {
49
+ // A point's per-row color does not necessarily describe one whole path.
50
+ // Reuse the native series contract before deciding whether omitted color
51
+ // can be inherited; explicit choices must still fail rather than disappear.
52
+ const color=options.color===undefined?undefined:{fieldType:'nominal',...fieldEncoding(options.color)};
53
+ const pathColor=color && {...color,scale:color.field===source.channels.color?.field?source.channels.color.scale:color.scale?.id};
54
+ const path={id,mark:{type:spec.action==='createAreaPlot'?'area':'line',missing:options.line?.missing??'error'},encoding:{...(pathColor?{color:pathColor}:{}),
55
+ ...(options.groupBy===undefined?{}:{group:{fieldType:'nominal',fields:normalizeGroupFields(options.groupBy)}})}};
56
+ const rows=requireMaterializedDataset(program,options.data).values;
57
+ const validate=()=>{
58
+ if (spec.action==='createAreaPlot' && path.encoding.color && !['nominal','ordinal'].includes(color.fieldType))
59
+ throw new Error('createAreaPlot color requires a categorical field.');
60
+ validatePathSeriesAppearance(rows,path,program.semanticSpec.scales);
61
+ if(path.mark.type==='line') {
62
+ for(const channel of ['x','y','theta','radius']) if(options[channel])
63
+ path.encoding[channel]={...fieldEncoding(options[channel]),scale:source.channels[channel]?.scale};
64
+ deriveLineSeries(rows,path,{scales:program.semanticSpec.scales,
65
+ xBinBoundaries:path.encoding.x?.bin===undefined?undefined:
66
+ resolveLineBins(rows,path,findSemanticScale(program,source.channels.x?.scale)).boundaries,
67
+ xDomain:program.resolvedScales[source.channels.x?.scale]?.domain,
68
+ yDomain:program.resolvedScales[source.channels.y?.scale]?.domain,
69
+ thetaDomain:program.resolvedScales[source.channels.theta?.scale]?.domain});
70
+ }
71
+ };
72
+ try {validate();} catch (error) {
73
+ if (supplied.color!==undefined || options.color===undefined) throw error;
74
+ delete options.color;
75
+ delete path.encoding.color;
76
+ validate();
77
+ }
78
+ }
79
+ if(spec.action==='createRegressionPlot' && ['x','y'].some(axis=>source.channels[axis]?.fieldType!=='quantitative')) throw new Error('Regression requires two quantitative source axes.');
80
+ if(spec.action==='createBarPlot' && ['x','y'].some(axis=>options[axis]?.aggregate) && !['x','y'].some(axis=>['nominal','ordinal'].includes(options[axis]?.fieldType)||options[axis]?.bin)) throw new Error('Bar layers require a categorical or binned position.');
81
+ if(kind==='heatmap' && ['x','y'].some(axis=>source.channels[axis]?.fieldType!=='quantitative')) throw new Error('A density heatmap layer requires two quantitative source axes.');
82
+ if(spec.action==='createDumbbellPlot') {
83
+ if(!fieldEncoding(options.start)?.field) throw new Error('Dumbbell requires an explicit start field; it cannot be inferred from one measure.');
84
+ if(fieldEncoding(options.start).field===fieldEncoding(options.end)?.field) throw new Error('Dumbbell start and end fields must be distinct.');
85
+ }
86
+ if(['summaryXY','categoryValue','density','endpoint'].includes(kind)&&options.color?.fieldType==='quantitative') throw new Error('A grouped distribution layer requires a categorical color field.');
87
+ if(['createStripPlot','createBeeswarmPlot'].includes(spec.action) && ['x','y'].filter(axis=>['nominal','ordinal'].includes(source.channels[axis]?.fieldType)).length!==1) throw new Error('Strip and Beeswarm require one categorical slot and one measure.');
88
+ if(kind==='rug') {
89
+ const axis=options.y?'y':'x';
90
+ if(!(axis==='y'?['left','right']:['top','bottom']).includes(options.edge)) throw new Error(`Rug ${axis} uses ${axis==='y'?'left or right':'top or bottom'} edges.`);
91
+ if(!['quantitative','temporal'].includes(options[axis]?.fieldType))throw new Error('Rug requires a quantitative or temporal axis.');
92
+ }
93
+ const category=options.category?.field ?? ['x','y'].map(axis=>options[axis]).find(e=>['nominal','ordinal'].includes(e?.fieldType))?.field;
94
+ if (options.color?.field && ['summaryXY','categoryValue'].includes(kind) && spec.action!=='createIntervalPlot' &&
95
+ options.color.field!==category && !(spec.action==='createViolinPlot'&&options.split?.field===options.color.field))
96
+ throw new Error('This summary color must name its category or an explicit supported split field.');
97
+ if (kind === 'rug' && spec.inherit?.color !== false && !options.color && source.channels.color?.field) {
98
+ options.color = { field: source.channels.color.field, fieldType: source.channels.color.fieldType };
99
+ }
100
+ let deferredColor;
101
+ if(['endpoint','rug'].includes(kind)&&options.color) {
102
+ if(kind==='endpoint' && options.color.field!==options.category.field) throw new Error('Endpoint summary color must be the category field.');
103
+ deferredColor=options.color; delete options.color;
104
+ }
105
+ if (spec.action==='createAreaPlot' && options.missing!=='break') {
106
+ const fields=options.groupBy===undefined?[]:normalizeGroupFields(options.groupBy);
107
+ const groups=new Map();
108
+ for (const row of requireMaterializedDataset(program,options.data).values) {
109
+ const key=JSON.stringify(fields.map(field=>row[field]));
110
+ groups.set(key,(groups.get(key)??0)+1);
111
+ }
112
+ if (!groups.size || [...groups.values()].some(count=>count<2))
113
+ throw new Error('Each area series requires at least two observations. Choose another layer or a less granular group.');
114
+ }
115
+ options.id=id; options.coordinate=source.coordinate.id;
116
+ // A layer consumes existing guides. createGuides can be called once after
117
+ // constructing the view; constructors must not duplicate or reset them.
118
+ if(options.guides!==undefined&&options.guides!==false) throw new Error('Layer guides are shared; call createGuides on the completed view.');
119
+ options.guides=false;
120
+ const rebind = {}, scaleEdits = [];
121
+ if(kind==='radial') {
122
+ if(!['count','sum'].includes(options.aggregate)||(options.aggregate==='sum')!==(options.value!==undefined)) throw new Error('Radial plots require count without a value field or sum with a value field.');
123
+ if(spec.action==='createPiePlot') {
124
+ const expected={aggregate:options.aggregate,...(options.aggregate==='sum'?{weight:options.value}:{})};
125
+ if(source.role!=='pie'||JSON.stringify(expected)!==JSON.stringify(source.channels.theta?.measurement.parameters)) throw new Error('Pie layers require the same angle partition, aggregate and weight.');
126
+ } else {
127
+ const mapping=spec.action==='createRosePlot'?'area':'radius-length';
128
+ if(findSemanticScale(program,source.channels.radius?.scale)?.radialMapping!==mapping) throw new Error('Radial layers require the same radius mapping (area or radius-length).');
129
+ }
130
+ }
131
+ if (['createViolinPlot','createRaincloudPlot','createBoxPlot','createGradientPlot','createBarPlot'].includes(spec.action)) {
132
+ for (const channel of ['x','y']) {
133
+ const role = source.channels[channel], scale = findSemanticScale(program,role?.scale);
134
+ if (scale?.type === 'point') scaleEdits.push({id:scale.id,type:'band',paddingInner:0,paddingOuter:0});
135
+ }
136
+ }
137
+ if(kind==='radar') {
138
+ const fields=source.channels.theta?.fields;
139
+ if(source.role!=='radar'||JSON.stringify(options.wide?.fields)!==JSON.stringify(fields)) throw new Error('Radar layers require identical dimensions and order.');
140
+ if(options.order&&JSON.stringify(options.order)!==JSON.stringify(fields)) throw new Error('Radar layers require the source dimension order.');
141
+ rebind.theta = source.channels.theta.scale;
142
+ rebind.radius = source.channels.radius.scale;
143
+ } else if(kind==='parallel') {
144
+ const dimensions=options.dimensions?.map(fieldEncoding);
145
+ if(!dimensions||JSON.stringify(dimensions.map(d=>d.field))!==JSON.stringify(source.dimensions?.map(d=>d.field))) throw new Error('Parallel layers require identical dimensions and order.');
146
+ options.dimensions=dimensions.map((d,index)=>{const o={...d};bindScale(program,{dimension:o},'dimension.scale',source.dimensions[index].scale);return {...o,scale:{id:source.dimensions[index].scale}};});
147
+ } else {
148
+ const channels=constructorChannels(spec.action,options,source);
149
+ if(!Object.keys(channels).length || (kind==='polar' && (!channels.theta || !channels.radius))) throw new Error('The source does not supply the required positional fields.');
150
+ for(const [channel,candidate] of Object.entries(channels)) {
151
+ const original=source.channels[channel];
152
+ if(!original||candidate.field!==original.field||candidate.fieldType!==original.fieldType||
153
+ measurementUnit(candidate.operations)!==original.measurement.unit||
154
+ (candidate.temporalUnit??null)!==(original.temporalUnit??null))
155
+ throw new Error(`Layer ${channel} must measure the same field, type, and units as the source. Use a follow overlay or an adjacent chart for different measurements.`);
156
+ if (kind === 'ecdf') rebind[channel] = original.scale;
157
+ else if (kind === 'horizon' && channel === 'y') set(options,candidate.path,{id:original.scale});
158
+ else {
159
+ bindScale(program,options,candidate.path,original.scale);
160
+ const promotion=scaleEdits.find(s=>s.id===original.scale);
161
+ if(promotion) { const bound={...get(options,candidate.path),...promotion}; delete bound.padding; set(options,candidate.path,bound); }
162
+ }
163
+ }
164
+ if(kind==='horizon'&&JSON.stringify(source.channels.y.measurement.parameters)!==JSON.stringify({bands:options.bands,baseline:options.baseline,extent:options.extent,resolve:options.resolve})) throw new Error('Horizon layers require the same folding parameters.');
165
+ }
166
+ if(spec.action==='createDumbbellPlot') bindScale(program,options,'start.scale',source.channels[options.orientation==='horizontal'?'x':'y'].scale);
167
+ if(kind==='radial') {
168
+ options.category.scale={id:source.channels.theta.scale};
169
+ if(options.radiusScale) options.radiusScale={id:source.channels.radius.scale};
170
+ }
171
+ if(kind==='heatmap') options.color={...(options.color??{}),scale:{...(options.color?.scale??{}),id:options.color?.scale?.id??`${id}Color`}};
172
+ else if(options.color?.field && options.color.field!==source.channels.color?.field) options.color={...options.color,scale:{...options.color.scale,id:options.color.scale?.id??`${id}Color`}};
173
+ if(options.color?.field && options.color.field===source.channels.color?.field) bindScale(program,options,'color.scale',source.channels.color.scale);
174
+ if(deferredColor?.field===source.channels.color?.field && deferredColor) deferredColor={...deferredColor,scale:{id:source.channels.color.scale}};
175
+ return {id,source,action:spec.action,options,placement,rebind,scaleEdits,deferredColor};
176
+ }
@@ -0,0 +1,105 @@
1
+ import { getWrappedActionMetadata } from "../../core/action.js";
2
+ import { restoreProgramContextKeys } from "../../core/programState.js";
3
+ import { normalizeActionTargets } from "../../grammar/actionTargets.js";
4
+ import { compositionLegends } from "../../materialization/compositionLegends.js";
5
+ import { legendBlockChannels } from "../../grammar/legendBlocks.js";
6
+ import { assertGuideCollisionBlocks } from "../../layout/guideCollisions.js";
7
+ import { resolveGuideCollisionBlocks } from "../../materialization/guides/resources.js";
8
+
9
+ function collectedLegends(program, path = [], result = new Map()) {
10
+ if (!program.compositionSpec || program.compositionSpec.facet) return result;
11
+ const legends = compositionLegends(program, program.children);
12
+ for (const legend of legends) for (const target of legend.targets) {
13
+ const child = atPath(program, target.childPath), key = JSON.stringify([...path, ...target.childPath]);
14
+ const kinds = result.get(key) ?? new Set();
15
+ for (const [kind, config] of Object.entries(child.guideConfigs.legend ?? {})) {
16
+ if ((target.id === undefined || config.target === target.id) && legendBlockChannels(kind, config).some(channel => legend.channels.includes(channel))) kinds.add(kind);
17
+ }
18
+ result.set(key, kinds);
19
+ }
20
+ for (const [id, child] of Object.entries(program.children)) collectedLegends(child, [...path,id], result);
21
+ return result;
22
+ }
23
+
24
+ function visibleGuideConfigs(program, hidden) {
25
+ return { ...program.guideConfigs, legend: Object.fromEntries(Object.entries(program.guideConfigs.legend ?? {})
26
+ .filter(([kind]) => !hidden?.has(kind))) };
27
+ }
28
+
29
+ // Internal planning frame: callers still replay one public scoped action on
30
+ // the composition. The collected copies are not visible in this local frame.
31
+ export function compositionTargetProgram(program, path) {
32
+ const selected = atPath(program, path), hidden = collectedLegends(program).get(JSON.stringify(path));
33
+ return hidden ? selected._withContext({ compositionCollectedLegends: [...hidden] }) : selected;
34
+ }
35
+
36
+ function atPath(program, path) {
37
+ for (const id of path) {
38
+ if (program.compositionSpec?.type === "facet") throw new Error("Facet cells follow their source; use editFacetSource for persistent edits.");
39
+ if (!Object.hasOwn(program.children, id)) throw new Error(`Unknown target child "${id}".`);
40
+ program = program.children[id];
41
+ }
42
+ return program;
43
+ }
44
+
45
+ function changePath(program, path, transform) {
46
+ if (!path.length) return transform(program);
47
+ const [id, ...rest] = path;
48
+ return program._withCompositionState({ compositionSpec: program.compositionSpec,
49
+ children: { ...program.children, [id]: changePath(program.children[id], rest, transform) } });
50
+ }
51
+
52
+ function refreshAncestors(original, revised) {
53
+ if (!revised.compositionSpec || original === revised) return revised;
54
+ let children = revised.children;
55
+ const changed = [];
56
+ for (const [id, child] of Object.entries(children)) {
57
+ if (child === original.children[id]) continue;
58
+ children = { ...children, [id]: original.children[id] ? refreshAncestors(original.children[id], child) : child };
59
+ changed.push(id);
60
+ }
61
+ if (!changed.length) return revised;
62
+ let next = revised._withCompositionState({ compositionSpec: revised.compositionSpec, children });
63
+ for (const id of changed) next = next.useProgram({ id });
64
+ return next.materializeComposition();
65
+ }
66
+
67
+ // Explicit public dispatch only. Edits run on immutable candidates, then shared
68
+ // domains and all changed ancestor snapshots are materialized once from the
69
+ // complete final state. This avoids intermediate mixed-palette/domain failures.
70
+ export function dispatchActionTargets(program, wrappedAction, args) {
71
+ const targets = normalizeActionTargets(args.target);
72
+ for (const target of targets) {
73
+ const selected = atPath(program, target.childPath);
74
+ if (target.facetSource && selected.compositionSpec?.type !== "facet") throw new Error("facetSource requires a facet view.");
75
+ if (selected.compositionSpec?.type === "facet" && target.id !== undefined && !target.facetSource) {
76
+ throw new Error("Target a facet source through editFacetSource, or identify an explicit childPath.");
77
+ }
78
+ }
79
+ const { target: _target, ...options } = args;
80
+ const collected = collectedLegends(program);
81
+ let next = program;
82
+ for (const target of targets) next = changePath(next, target.childPath, selected => {
83
+ const hidden = collected.get(JSON.stringify(target.childPath));
84
+ const candidate = hidden ? selected._withContext({ compositionCollectedLegends: [...hidden] }) : selected;
85
+ const nativeOptions = { ...options, ...(target.id === undefined ? {} : { target: target.id }) };
86
+ const result = target.facetSource
87
+ ? candidate.editFacetSource({ action: { method: getWrappedActionMetadata(wrappedAction).op, options: nativeOptions } })
88
+ : wrappedAction.call(candidate, nativeOptions);
89
+ if (!hidden) return result;
90
+ return restoreProgramContextKeys(result, selected, ["compositionCollectedLegends"]);
91
+ });
92
+ const revised = refreshAncestors(program, next);
93
+ // The parent layout checks the collected graphic against retained content.
94
+ // Releasing sharing must also validate the newly visible local guides.
95
+ if (collected.size) {
96
+ const remaining = collectedLegends(revised);
97
+ for (const key of collected.keys()) {
98
+ const child = JSON.parse(key).reduce((node,id)=>node?.children?.[id],revised);
99
+ if (!child) continue;
100
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(child.graphicSpec,
101
+ visibleGuideConfigs(child, remaining.get(key)), child.titleConfig, child.materializationConfigs.textMetrics));
102
+ }
103
+ }
104
+ return revised;
105
+ }
@@ -1,3 +1,4 @@
1
+ import { assertIndependentOverlayGeometry } from "../../grammar/overlay.js";
1
2
  import {resolveDatasetTransformInputs} from "./shared.js";
2
3
  import { closedAction } from "../../core/action.js";
3
4
  import { validateGraphicItemLimit } from "../../core/validation.js";
@@ -96,6 +97,7 @@ export const releaseDerivedData = /* @__PURE__ */ closedAction(
96
97
  "calculations", "datasets", validatedId
97
98
  ]);
98
99
  next = next._withoutMaterializationConfig(["regressionFits", validatedId]);
100
+ next = next._withoutMaterializationConfig(["rowProvenance", validatedId]);
99
101
  for (const owner of selfOwners) {
100
102
  const [, family, ownerId] = owner.path;
101
103
  next = next._withoutMaterializationConfig(["data", family, ownerId]);
@@ -136,6 +138,7 @@ export const rebindLayerData = /* @__PURE__ */ closedAction(
136
138
  );
137
139
 
138
140
  function assertIndependentMark(program, layer) {
141
+ assertIndependentOverlayGeometry(layer, "bindMarkData");
139
142
  const config = program.markConfigs[layer.id] ?? {};
140
143
  const role = OWNED_MARK_ROLES.find(key => config[key] !== undefined);
141
144
  if (role !== undefined) {
@@ -1 +1 @@
1
- import{findRaincloudParent as b}from"../../selectors/raincloud.js";import{filterRaincloud as T,materializeRaincloudFilter as B,removeRaincloudFilter as A}from"./raincloudFilter.js";import{closedAction as m}from"../../core/action.js";import{validateUserId as p}from"../../core/identifiers.js";import{withPreviewDatasetValues as z}from"../primitives/semanticAction.js";import{deriveFilteredRows as P,normalizeFilterTransform as L}from"../../grammar/filter.js";import{deriveMarkFilteredRows as N,markFilterSelectors as E,normalizeMarkFilterTransform as K}from"../../grammar/markFilter.js";import{resolveMarkFilterSelection as M}from"../../materialization/selection/filter.js";import{synchronizeBoxFilter as y}from"../boxPlots/components.js";import{findSelectionPolicy as q}from"../../materialization/selection/policies/index.js";import{applyLayerDataRematerialization as w,applyLayerEmptyDataView as $}from"../../materialization/dependencies.js";import{hasDataset as R,findDataset as O,resolveDatasetReference as V,requireLayer as h,resolveEligibleLayer as C}from"../../selectors/index.js";import{MATERIALIZE_OPTIONS as I,requireDerivedDataset as _}from"./shared.js";import{assertFieldsAvailable as H,deriveTransformSchema as J,transformInputFields as U}from"../../grammar/datasetSchema.js";const Y=Object.freeze(["id","source","field","oneOf","noneOf","predicate","range","nulls"]),x=Object.freeze(["grain","field","channel","property","op","value","values","min","max","inclusive","count","groupBy","ties"]),Z=Object.freeze(["target","mode",...x]),G=Object.freeze(["target"]),Q=Object.freeze(["id"]);function W(i,t){if(i.mark?.type!=="bar"||i.encoding?.x?.bin===void 0)return;const n=[...new Set(t.flatMap(e=>[e.channels.x,e.channels.x2]).filter(Number.isFinite))].sort((e,r)=>e-r);return n.length<2?void 0:n}const ke=m({op:"materializeFilteredData",description:"Materialize one filtered derived dataset."},I,function(i={}){const{id:t,source:n,transform:e}=_(this,i.id,"filter");H(n.schema,U(e),{data:n.id,operation:"materializeFilteredData"});const r=P(n.values,e),d=J(n.schema,e,r,{sourceId:n.id,ownerId:t});return this.editSemantic({property:`dataset[${t}].values`,value:r})}),ve=m({op:"filterData",description:"Create a named dataset from one field filter."},Y,function(i={}){const t=p(i.id,"Filtered dataset id"),n=p(i.source??this.context.currentData,"Source dataset id"),e=V(this,n,"Filter source dataset").id,r=L(i),d=this.createDerivedData({id:t,source:e,transform:[r]}).materializeFilteredData({id:t});return this.actionStack.length===1?d._withMaterializationConfig(["data","filter",t],{current:t}):d}),Fe=m({op:"materializeMarkFilteredData",description:"Materialize member rows retained by one final-item mark selection."},I,function(i={}){const{id:t,dataset:n,source:e,transform:r}=_(this,i.id,"markFilter"),d=b(this,r.target);if(d!==void 0)return B(this,{id:t,source:e,transform:r},d);const f=h(this,r.target);if(f?.data!==n.source)throw new Error(`Mark filter target "${r.target}" must still use source dataset "${n.source}".`);let s=this,c=e.values;const o=E(r);for(const[g,k]of o.entries()){const u=M(s,r.target,k);c=N(c,u.items,u.keys,f),g<o.length-1&&(s=z(s,{id:t,values:c,target:r.target}),s=y(s,r.target,t),s=c.length===0?$(s,r.target):w(s,r.target),c=O(s,t).values)}o.length>1&&(s=z(s,{id:t,omitValues:!0}));let a=s.editSemantic({property:`dataset[${t}].values`,value:c});return h(a,r.target).data!==t&&(a=a.editSemantic({property:`layer[${r.target}].data`,value:t})),a=a._withMarkConfig(r.target,{...a.markConfigs[r.target],markFilter:{...a.markConfigs[r.target].markFilter,empty:c.length===0}}),a=y(a,r.target,t),c.length===0?a.materializeEmptyMark({id:r.target}):w(a,r.target)}),be=m({op:"materializeEmptyMark",description:"Clear one active empty-filter mark without changing its domains."},Q,function(i={}){const t=p(i.id,"Empty mark id"),n=h(this,t),e=this.markConfigs[t]?.markFilter,r=O(this,n.data);if(e?.empty!==!0||r?.values?.length!==0)throw new Error(`Mark "${t}" does not own an active empty filter.`);return $(this,t)});function D(i,t){const n=i.markConfigs[t.id]?.markFilter,e=O(i,t.data),r=e?.transform?.[0];if(!(e?.source===void 0||r?.type!=="markFilter"||r.target!==t.id))return{dataset:e.id,source:e.source,selectors:E(r),...n?.originalBin===void 0?{}:{originalBin:n.originalBin}}}function X(i,t){const n=i.markConfigs[t]??{},{markFilter:e,...r}=n;return i._withMarkConfig(t,r)}function ee(i,t,n){let e=i;const r=t.encoding?.x?.bin??{};for(const d of["maxBins","step","boundaries"])Object.hasOwn(n,d)?e=e.editSemantic({property:`layer[${t.id}].encoding.x.bin.${d}`,value:n[d]}):Object.hasOwn(r,d)&&(e=e.editSemantic({property:`layer[${t.id}].encoding.x.bin.${d}`,remove:!0}));return e}function j(i,t,n){let e=y(i,t.id).editSemantic({property:`layer[${t.id}].data`,value:n.source});return n.originalBin!==void 0&&(e=ee(e,h(e,t.id),n.originalBin)),e=X(e,t.id).releaseDerivedData({id:n.dataset})._withContext({currentData:n.source}),w(e,t.id)}function te(i,t){const n=`${t}FilteredData`;if(!R(i,n))return n;let e=2;for(;R(i,`${n}${e}`);)e+=1;return`${n}${e}`}function re(i,t){return JSON.stringify(i)===JSON.stringify(t)}const Me=m({op:"removeMarkFilter",description:"Remove one active final-item filter and restore its source."},G,function(i={}){const t=i.target===void 0?void 0:p(i.target,"Mark filter target id"),n=b(this,t);if(n!==void 0)return A(this,n);const e=C(this,{target:t,label:"mark filter",predicate:r=>D(this,r)!==void 0});return j(this,e,D(this,e))}),ye=m({op:"filterMarks",description:"Retain selected final mark items through immutable derived data."},Z,function(i={}){if(i.mode!==void 0&&!["replace","compose"].includes(i.mode))throw new Error(`Unknown mark filter mode "${i.mode}".`);const t=i.target===void 0?void 0:p(i.target,"Filter mark target id"),n=b(this,t);if(n!==void 0){const l=Object.fromEntries(x.flatMap(F=>Object.hasOwn(i,F)?[[F,i[F]]]:[]));return T(this,i,n,l)}const e=C(this,{target:t,label:"filter mark",predicate:l=>l.data!==void 0&&q(l.mark?.type)!==void 0});if(this.markConfigs[e.id]?.itemFilterable===!1)throw new Error(`filterMarks does not support composite mark "${e.id}"; filter its source dataset first.`);const r=D(this,e),d=Object.fromEntries(x.flatMap(l=>Object.hasOwn(i,l)?[[l,i[l]]]:[])),f=M(this,e.id,d),s=r?.selectors.at(-1);if(s!==void 0&&re(s,f.selector))return this;if(r!==void 0&&i.mode===void 0)throw new Error('Repeated filterMarks requires mode "replace" or "compose".');const c=r===void 0||i.mode==="replace"?[f.selector]:[...r.selectors,f.selector];let o=r===void 0?this:j(this,e,r);const a=h(o,e.id),g=M(o,e.id,c[0]),k=K(e.id,c),u=W(a,g.items),S=a.mark?.type==="bar"&&a.encoding?.x?.bin!==void 0?a.encoding.x.bin:void 0,v=te(o,e.id);if(o=o.createDerivedData({id:v,source:a.data,transform:[k]}),u!==void 0){for(const l of["maxBins","step"])Object.hasOwn(a.encoding.x.bin,l)&&(o=o.editSemantic({property:`layer[${a.id}].encoding.x.bin.${l}`,remove:!0}));o=o.editSemantic({property:`layer[${a.id}].encoding.x.bin.boundaries`,value:u})}return o=o._withMarkConfig(a.id,{...o.markConfigs[a.id],markFilter:{dataset:v,source:a.data,selectors:c,empty:!1,...S===void 0?{}:{originalBin:S}}}),o.materializeMarkFilteredData({id:v})});export{ve as filterData,ye as filterMarks,be as materializeEmptyMark,ke as materializeFilteredData,Fe as materializeMarkFilteredData,Me as removeMarkFilter};
1
+ import{isSourceOwnedText as j}from"../../grammar/text.js";import{markFilterSourceIndices as B}from"../../materialization/selection/rows.js";import{assertIndependentOverlayGeometry as P}from"../../grammar/overlay.js";import{findRaincloudParent as M}from"../../selectors/raincloud.js";import{filterRaincloud as A,materializeRaincloudFilter as L,removeRaincloudFilter as N}from"./raincloudFilter.js";import{closedAction as m}from"../../core/action.js";import{validateUserId as p}from"../../core/identifiers.js";import{withPreviewDatasetValues as z}from"../primitives/semanticAction.js";import{deriveFilteredRows as K,normalizeFilterTransform as q}from"../../grammar/filter.js";import{deriveMarkFilteredRows as V,markFilterSelectors as E,normalizeMarkFilterTransform as H}from"../../grammar/markFilter.js";import{resolveMarkFilterSelection as b}from"../../materialization/selection/filter.js";import{synchronizeBoxFilter as y}from"../boxPlots/components.js";import{findSelectionPolicy as J}from"../../materialization/selection/policies/index.js";import{applyLayerDataRematerialization as w,applyLayerEmptyDataView as $}from"../../materialization/dependencies.js";import{hasDataset as R,findDataset as O,resolveDatasetReference as U,requireLayer as h,resolveEligibleLayer as C}from"../../selectors/index.js";import{MATERIALIZE_OPTIONS as I,requireDerivedDataset as _}from"./shared.js";import{assertFieldsAvailable as Y,deriveTransformSchema as G,transformInputFields as Z}from"../../grammar/datasetSchema.js";const Q=Object.freeze(["id","source","field","oneOf","noneOf","predicate","range","nulls"]),x=Object.freeze(["rows","grain","field","channel","property","op","value","values","min","max","inclusive","count","groupBy","ties"]),W=Object.freeze(["target","mode",...x]),X=Object.freeze(["target"]),ee=Object.freeze(["id"]);function te(i,t){if(i.mark?.type!=="bar"||i.encoding?.x?.bin===void 0)return;const n=[...new Set(t.flatMap(e=>[e.channels.x,e.channels.x2]).filter(Number.isFinite))].sort((e,r)=>e-r);return n.length<2?void 0:n}const we=m({op:"materializeFilteredData",description:"Materialize one filtered derived dataset."},I,function(i={}){const{id:t,source:n,transform:e}=_(this,i.id,"filter");Y(n.schema,Z(e),{data:n.id,operation:"materializeFilteredData"});const r=K(n.values,e),d=G(n.schema,e,r,{sourceId:n.id,ownerId:t});return this.editSemantic({property:`dataset[${t}].values`,value:r})}),Oe=m({op:"filterData",description:"Create a named dataset from one field filter."},Q,function(i={}){const t=p(i.id,"Filtered dataset id"),n=p(i.source??this.context.currentData,"Source dataset id"),e=U(this,n,"Filter source dataset").id,r=q(i),d=this.createDerivedData({id:t,source:e,transform:[r]}).materializeFilteredData({id:t});return this.actionStack.length===1?d._withMaterializationConfig(["data","filter",t],{current:t}):d}),xe=m({op:"materializeMarkFilteredData",description:"Materialize member rows retained by one final-item mark selection."},I,function(i={}){const{id:t,dataset:n,source:e,transform:r}=_(this,i.id,"markFilter"),d=M(this,r.target);if(d!==void 0)return L(this,{id:t,source:e,transform:r},d);const f=h(this,r.target);if(f?.data!==n.source)throw new Error(`Mark filter target "${r.target}" must still use source dataset "${n.source}".`);let s=this,c=e.values;const o=E(r);for(const[g,k]of o.entries()){const u=b(s,r.target,k);c=V(c,u.items,u.keys,f),g<o.length-1&&(s=z(s,{id:t,values:c,target:r.target}),s=y(s,r.target,t),s=c.length===0?$(s,r.target):w(s,r.target),c=O(s,t).values)}o.length>1&&(s=z(s,{id:t,omitValues:!0}));let a=s.editSemantic({property:`dataset[${t}].values`,value:c})._withMaterializationConfig(["rowProvenance",t],{indices:B(e.values,c)});return h(a,r.target).data!==t&&(a=a.editSemantic({property:`layer[${r.target}].data`,value:t})),a=a._withMarkConfig(r.target,{...a.markConfigs[r.target],markFilter:{...a.markConfigs[r.target].markFilter,empty:c.length===0}}),a=y(a,r.target,t),c.length===0?a.materializeEmptyMark({id:r.target}):w(a,r.target)}),Se=m({op:"materializeEmptyMark",description:"Clear one active empty-filter mark without changing its domains."},ee,function(i={}){const t=p(i.id,"Empty mark id"),n=h(this,t),e=this.markConfigs[t]?.markFilter,r=O(this,n.data);if(e?.empty!==!0||r?.values?.length!==0)throw new Error(`Mark "${t}" does not own an active empty filter.`);return $(this,t)});function S(i,t){const n=i.markConfigs[t.id]?.markFilter,e=O(i,t.data),r=e?.transform?.[0];if(!(e?.source===void 0||r?.type!=="markFilter"||r.target!==t.id))return{dataset:e.id,source:e.source,selectors:E(r),...n?.originalBin===void 0?{}:{originalBin:n.originalBin}}}function re(i,t){const n=i.markConfigs[t]??{},{markFilter:e,...r}=n;return i._withMarkConfig(t,r)}function ie(i,t,n){let e=i;const r=t.encoding?.x?.bin??{};for(const d of["maxBins","step","boundaries"])Object.hasOwn(n,d)?e=e.editSemantic({property:`layer[${t.id}].encoding.x.bin.${d}`,value:n[d]}):Object.hasOwn(r,d)&&(e=e.editSemantic({property:`layer[${t.id}].encoding.x.bin.${d}`,remove:!0}));return e}function T(i,t,n){let e=y(i,t.id).editSemantic({property:`layer[${t.id}].data`,value:n.source});return n.originalBin!==void 0&&(e=ie(e,h(e,t.id),n.originalBin)),e=re(e,t.id).releaseDerivedData({id:n.dataset})._withContext({currentData:n.source}),w(e,t.id)}function ne(i,t){const n=`${t}FilteredData`;if(!R(i,n))return n;let e=2;for(;R(i,`${n}${e}`);)e+=1;return`${n}${e}`}function ae(i,t){return JSON.stringify(i)===JSON.stringify(t)}const De=m({op:"removeMarkFilter",description:"Remove one active final-item filter and restore its source."},X,function(i={}){const t=i.target===void 0?void 0:p(i.target,"Mark filter target id"),n=M(this,t);if(n!==void 0)return N(this,n);const e=C(this,{target:t,label:"mark filter",predicate:r=>S(this,r)!==void 0});return T(this,e,S(this,e))}),ze=m({op:"filterMarks",description:"Retain selected final mark items through immutable derived data."},W,function(i={}){if(i.mode!==void 0&&!["replace","compose"].includes(i.mode))throw new Error(`Unknown mark filter mode "${i.mode}".`);const t=i.target===void 0?void 0:p(i.target,"Filter mark target id"),n=M(this,t);if(n!==void 0){const l=Object.fromEntries(x.flatMap(F=>Object.hasOwn(i,F)?[[F,i[F]]]:[]));return A(this,i,n,l)}const e=C(this,{target:t,label:"filter mark",predicate:l=>l.data!==void 0&&(t!==void 0||!j(l))&&J(l.mark?.type)!==void 0});if(P(e,"filterMarks"),this.markConfigs[e.id]?.itemFilterable===!1)throw new Error(`filterMarks does not support composite mark "${e.id}"; filter its source dataset first.`);const r=S(this,e),d=Object.fromEntries(x.flatMap(l=>Object.hasOwn(i,l)?[[l,i[l]]]:[])),f=b(this,e.id,d),s=r?.selectors.at(-1);if(s!==void 0&&ae(s,f.selector))return this;if(r!==void 0&&i.mode===void 0)throw new Error('Repeated filterMarks requires mode "replace" or "compose".');const c=r===void 0||i.mode==="replace"?[f.selector]:[...r.selectors,f.selector];let o=r===void 0?this:T(this,e,r);const a=h(o,e.id),g=b(o,e.id,c[0]),k=H(e.id,c),u=te(a,g.items),D=a.mark?.type==="bar"&&a.encoding?.x?.bin!==void 0?a.encoding.x.bin:void 0,v=ne(o,e.id);if(o=o.createDerivedData({id:v,source:a.data,transform:[k]}),u!==void 0){for(const l of["maxBins","step"])Object.hasOwn(a.encoding.x.bin,l)&&(o=o.editSemantic({property:`layer[${a.id}].encoding.x.bin.${l}`,remove:!0}));o=o.editSemantic({property:`layer[${a.id}].encoding.x.bin.boundaries`,value:u})}return o=o._withMarkConfig(a.id,{...o.markConfigs[a.id],markFilter:{dataset:v,source:a.data,selectors:c,empty:!1,...D===void 0?{}:{originalBin:D}}}),o.materializeMarkFilteredData({id:v})});export{Oe as filterData,ze as filterMarks,Se as materializeEmptyMark,we as materializeFilteredData,xe as materializeMarkFilteredData,De as removeMarkFilter};
@@ -1,4 +1,5 @@
1
1
  import { findDataset, hasDataset } from "../../selectors/datasets.js";
2
+ import { markFilterSourceIndices } from "../../materialization/selection/rows.js";
2
3
  import { findRaincloudParent } from "../../selectors/raincloud.js";
3
4
  import { normalizeMarkSelector, selectMarkItemKeys } from "../../grammar/markSelection.js";
4
5
  import { deriveMarkFilteredRows, normalizeMarkFilterTransform } from "../../grammar/markFilter.js";
@@ -18,7 +19,8 @@ export function deriveRaincloudFilteredRows(values, selectors) {
18
19
 
19
20
  export function materializeRaincloudFilter(program, { id, source, transform }, owner) {
20
21
  const values = deriveRaincloudFilteredRows(source.values, transform.selectors);
21
- let next = program.editSemantic({ property: `dataset[${id}].values`, value: values });
22
+ let next = program.editSemantic({ property: `dataset[${id}].values`, value: values })
23
+ ._withMaterializationConfig(["rowProvenance", id], { indices: markFilterSourceIndices(source.values, values) });
22
24
  next = next.editRaincloudPlot({ target: owner.id, data: id });
23
25
  const revised = findRaincloudParent(next, owner.id);
24
26
  return next._withMarkConfig(revised.ownerId, {
@@ -1,8 +1,10 @@
1
+ import { validatePointShape } from "../../grammar/pointShapes.js";
1
2
  import { APPEARANCE_MARK_TYPES } from "../../core/vocabulary.js";
2
3
  import { removeEncoding } from "./remove.js";
3
4
  import { canMaterializeBar } from "../../materialization/marks/index.js";
4
5
  import { action } from "../../core/action.js";
5
6
  import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
7
+ import { validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
6
8
  import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
7
9
  import {
8
10
  readScaleField,
@@ -179,9 +181,24 @@ const encodeSize = /* @__PURE__ */ action(
179
181
  const encodeShape = /* @__PURE__ */ action(
180
182
  {
181
183
  op: "encodeShape",
182
- description: "Encode a nominal field as point shape."
184
+ description: "Assign constant or field-driven point shape."
183
185
  },
184
186
  function (args = {}) {
187
+ validateOptions(args, [...FIELD_OPTIONS, "value"], "encodeShape");
188
+ const hasValue = Object.hasOwn(args, "value");
189
+ if (hasValue === Object.hasOwn(args, "field")) {
190
+ throw new Error("encodeShape requires exactly one of value or field.");
191
+ }
192
+ if (hasValue) {
193
+ if (args.fieldType !== undefined || args.scale !== undefined) {
194
+ throw new Error("Constant shape does not accept fieldType or scale.");
195
+ }
196
+ const shape = validatePointShape(args.value);
197
+ const { id: target, layer } = resolveTarget(this, args.target, APPEARANCE_MARK_TYPES.encodeShape, "point mark");
198
+ const next = layer.encoding?.shape === undefined ? this
199
+ : removeEncoding.call(this, { target, channel: "shape" });
200
+ return next.editPointMark({ target, shape });
201
+ }
185
202
  return encodeAppearanceField(this, "shape", args, "encodeShape");
186
203
  }
187
204
  );
@@ -231,7 +248,7 @@ const encodeOpacity = /* @__PURE__ */ action(
231
248
  this,
232
249
  args.target,
233
250
  APPEARANCE_MARK_TYPES.encodeOpacity,
234
- "point, rule, line, bar or rect mark"
251
+ "point, rule, line, area, bar or rect mark"
235
252
  );
236
253
  if (hasValue) {
237
254
  validateOpacityValue(args.value, "encodeOpacity");
@@ -244,7 +261,7 @@ const encodeOpacity = /* @__PURE__ */ action(
244
261
  .clearOpacityEncoding({ target })
245
262
  ._withoutMaterializationConfig(["marks", target, "opacity"])
246
263
  ._withMarkConfig(target, { ...config, opacity: args.value });
247
- if (["line", "rect"].includes(layer.mark.type)) {
264
+ if (["line", "area", "rect"].includes(layer.mark.type)) {
248
265
  return rematerializeEncoding(next, target, "opacity", undefined, layer);
249
266
  }
250
267
  if (layer.mark.type === "bar") {
@@ -268,13 +285,15 @@ const encodeOpacity = /* @__PURE__ */ action(
268
285
  args.scale ?? {}
269
286
  );
270
287
  const scale = resolveOpacityScaleDefinition(this, requestedScale);
271
- if (["line", "rect"].includes(layer.mark.type)) {
288
+ if (["line", "area", "rect"].includes(layer.mark.type)) {
272
289
  if (Object.hasOwn(scale, "unknown")) {
273
- throw new Error("Line and Rect opacity do not support unknown fallback.");
290
+ throw new Error("Line, Area and Rect opacity do not support unknown fallback.");
274
291
  }
275
- validatePathSeriesAppearance(dataset.values, {
292
+ const prospective = {
276
293
  ...layer, encoding: { ...layer.encoding, opacity: { field: args.field, fieldType } }
277
- }, this.semanticSpec.scales);
294
+ };
295
+ if (layer.mark.type === "area") validatePathGroupAppearance(this, prospective, dataset);
296
+ else validatePathSeriesAppearance(dataset.values, prospective, this.semanticSpec.scales);
278
297
  }
279
298
  readScaleField(dataset.values, args.field, fieldType, {
280
299
  allowUnknown: Object.hasOwn(scale, "unknown")
@@ -61,6 +61,7 @@ const CHANNEL_METHODS = Object.freeze({
61
61
  const FORBIDDEN_PAYLOAD_KEYS = Object.freeze(["target", "coordinate", "id"]);
62
62
 
63
63
  const DEFERRED_MARK_OPERATIONS = Object.freeze([
64
+ "rematerializeOverlay",
64
65
  "materializeEmptyMark",
65
66
  "rematerializePointMark",
66
67
  "rematerializeTickMark",