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 { validateTextAlignment } from "../../core/validation.js";
1
2
  import { textBoundsFitCanvas } from "../../core/textMetrics.js";
2
3
  import { resolveGuideCollisionBlocks } from "../../materialization/guides/resources.js";
3
4
  import { canvasOverflowError } from "../../layout/canvas.js";
@@ -27,7 +28,7 @@ const OPTIONS = Object.freeze([
27
28
  "subtitleStyle", "maxWidth", "wrap", "lineHeight", "subtitleLineHeight"
28
29
  ]);
29
30
  const STYLE_OPTIONS = Object.freeze([
30
- "color", "fontSize", "fontFamily", "fontWeight", "fontStyle"
31
+ "color", "fontSize", "fontFamily", "fontWeight", "fontStyle", "align"
31
32
  ]);
32
33
  const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
33
34
  const ALIGNS = Object.freeze(["left", "center", "right"]);
@@ -69,6 +70,7 @@ function normalizeStyle(value, defaults, label) {
69
70
  }
70
71
  const style = { ...defaults, ...(value ?? {}) };
71
72
  validateKeys(style, STYLE_OPTIONS, label);
73
+ validateTextAlignment(style, label);
72
74
  validateTitleString(style.color, `${label} color`);
73
75
  validateTitleString(style.fontFamily, `${label} fontFamily`);
74
76
  validatePositiveFinite(style.fontSize, `${label} fontSize`);
@@ -251,11 +253,12 @@ export function resolveTitleLayout(program, config) {
251
253
  : config.position === "top"
252
254
  ? 16 + config.offset
253
255
  : plot.y + plot.height + config.offset;
254
- component = (lines, centers) => ({
256
+ const left = x - (config.align === "right" ? block.width : config.align === "center" ? block.width / 2 : 0);
257
+ component = (lines, centers, style) => ({
255
258
  lines,
256
- x,
259
+ x: style.align === undefined ? x : alignedTextAnchor(left, block.width, style.align),
257
260
  y: centers.map(value => blockTop + value),
258
- textAlign: config.align,
261
+ textAlign: style.align ?? config.align,
259
262
  rotation: 0,
260
263
  explicitRotation: config.position !== "top" || lines.length > 1
261
264
  });
@@ -267,19 +270,19 @@ export function resolveTitleLayout(program, config) {
267
270
  ? 16 + config.offset
268
271
  : canvas.properties.width - 16 + config.offset;
269
272
  const mapX = value => config.position === "left" ? edge + value : edge - value;
270
- component = (lines, centers) => ({
273
+ component = (lines, centers, style) => ({
271
274
  lines,
272
275
  x: centers.map(mapX),
273
- y,
274
- textAlign: "center",
276
+ y: y + Math.sin(rotation) * (style.align === "left" ? -block.width / 2 : style.align === "right" ? block.width / 2 : 0),
277
+ textAlign: style.align ?? "center",
275
278
  rotation,
276
279
  explicitRotation: true
277
280
  });
278
281
  }
279
- const title = component(block.titleLines, block.titleCenters);
282
+ const title = component(block.titleLines, block.titleCenters, config.titleStyle);
280
283
  const subtitle = block.subtitleLines.length === 0
281
284
  ? undefined
282
- : component(block.subtitleLines, block.subtitleCenters);
285
+ : component(block.subtitleLines, block.subtitleCenters, config.subtitleStyle);
283
286
  const titleBounds = unionTitleBounds([
284
287
  resolveTitleComponentBounds(title, config.titleStyle, program.materializationConfigs.textMetrics),
285
288
  ...(subtitle === undefined
@@ -5,6 +5,21 @@ import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "./immutable
5
5
  const metadataByWrappedAction = new WeakMap();
6
6
  const implementationByWrappedAction = new WeakMap();
7
7
  let actionCompletionHook;
8
+ const targetDispatchers = new WeakMap();
9
+ const UNSCOPED_PRIMITIVES = new Set(["createGraphics", "editGraphics", "editSemantic", "createData"]);
10
+ export function registerActionTargetDispatcher(ProgramClass, dispatcher) {
11
+ targetDispatchers.set(ProgramClass, dispatcher);
12
+ }
13
+ function targetDispatcher(ProgramClass) {
14
+ for (let owner = ProgramClass; owner; owner = Object.getPrototypeOf(owner)) {
15
+ if (targetDispatchers.has(owner)) return targetDispatchers.get(owner);
16
+ }
17
+ }
18
+
19
+
20
+ export function supportsScopedActionTargets(ProgramClass, operation) {
21
+ return !UNSCOPED_PRIMITIVES.has(operation) && targetDispatcher(ProgramClass) !== undefined;
22
+ }
8
23
 
9
24
  export function setActionCompletionHook(hook) {
10
25
  if (hook !== undefined && typeof hook !== "function") {
@@ -89,29 +104,28 @@ function childList(node) {
89
104
  return list;
90
105
  }
91
106
 
92
- function withChildList(node, list) {
93
- let children;
94
- const next = {};
95
- for (const key of Object.keys(node)) {
96
- if (key !== "children") Object.defineProperty(next, key, {
97
- value: node[key], enumerable: true
98
- });
99
- }
100
- Object.defineProperty(next, "children", {
101
- enumerable: true,
102
- get() {
103
- if (children === undefined) {
104
- const values = new Array(list.length);
105
- let item = list.tail;
106
- for (let index = list.length - 1; index >= 0; index -= 1) {
107
- values[index] = item.value;
108
- item = item.previous;
109
- }
110
- children = freezeOwned(values);
111
- }
112
- return children;
107
+ function readChildren() {
108
+ const list = childLists.get(this);
109
+ if (list.children === undefined) {
110
+ const values = new Array(list.length);
111
+ let item = list.tail;
112
+ for (let index = list.length - 1; index >= 0; index -= 1) {
113
+ values[index] = item.value;
114
+ item = item.previous;
113
115
  }
114
- });
116
+ list.children = freezeOwned(values);
117
+ }
118
+ return list.children;
119
+ }
120
+
121
+ function withChildList(node, list) {
122
+ // Keep one accessor identity across revisions. A new getter closure and
123
+ // nonconfigurable descriptors per append force dictionary-shaped objects;
124
+ // freezing those dominates long nested materialization traces in V8.
125
+ // Copy data properties without evaluating the lazy children array.
126
+ const next = Object.fromEntries(Object.keys(node)
127
+ .filter(key => key !== "children").map(key => [key, node[key]]));
128
+ Object.defineProperty(next, "children", { enumerable: true, get: readChildren });
115
129
  childLists.set(next, list);
116
130
  return freezeOwned(next);
117
131
  }
@@ -202,15 +216,17 @@ export function action(metadata, implementation) {
202
216
  throw new TypeError("Action arguments must be a plain object.");
203
217
  }
204
218
 
205
- if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
206
- if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
219
+ const scoped = isPlainObject(args.target) || Array.isArray(args.target);
220
+ const dispatch = scoped && !UNSCOPED_PRIMITIVES.has(ownedMetadata.op) ? targetDispatcher(this.constructor) : undefined;
221
+ if (!dispatch && scope === "unit") this._assertUnitProgram(ownedMetadata.op);
222
+ if (!dispatch && scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
207
223
 
208
224
  const summarizedArgs = summarizeArgs(args);
209
225
  const entered = this._enterAction({
210
226
  ...ownedMetadata,
211
227
  args: summarizedArgs
212
228
  });
213
- let result = implementation.call(entered, args);
229
+ let result = dispatch ? dispatch(entered, wrappedAction, args) : implementation.call(entered, args);
214
230
 
215
231
  if (!(result instanceof this.constructor)) {
216
232
  throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
@@ -0,0 +1,12 @@
1
+ import { isPlainObject, cloneAndFreeze } from "./immutable.js";
2
+
3
+ export const COMPOSITION_SCALE_CHANNELS = Object.freeze(["x", "y", "color", "stroke", "size", "shape", "opacity", "strokeWidth", "strokeDash"]);
4
+
5
+ export function normalizeCompositionShares(value = {}) {
6
+ if (!isPlainObject(value)) throw new TypeError("Composition share requires a channel object.");
7
+ for (const [channel, shared] of Object.entries(value)) {
8
+ if (!COMPOSITION_SCALE_CHANNELS.includes(channel)) throw new Error(`Unknown composition share channel "${channel}".`);
9
+ if (typeof shared !== "boolean") throw new TypeError(`Composition share.${channel} must be boolean.`);
10
+ }
11
+ return cloneAndFreeze(value);
12
+ }
@@ -1,3 +1,4 @@
1
+ import { normalizeCompositionShares } from "./compositionScales.js";
1
2
  import { normalizeFacetGap } from "./facetGap.js";
2
3
  import { normalizeFacetAxes } from "./facetAxisPolicy.js";
3
4
  import { isOwned, isPlainObject } from "./immutable.js";
@@ -9,6 +10,11 @@ import {
9
10
  DEFAULT_FACET_TOP_LANE
10
11
  } from "./vocabulary.js";
11
12
 
13
+ export function normalizeCompositionLegends(value) {
14
+ if (!["local", "collect"].includes(value)) throw new TypeError('Composition legends must be "local" or "collect".');
15
+ return value;
16
+ }
17
+
12
18
  // Primitive graphics/trace edits retain both immutable owners. Validate their
13
19
  // relationship once, rather than scanning every facet cell on every primitive.
14
20
  const validatedCompositions = new WeakMap();
@@ -248,7 +254,7 @@ function validateLayout(compositionSpec) {
248
254
  } else if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
249
255
  throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
250
256
  }
251
- if (!["start", "center", "end"].includes(compositionSpec.align)) {
257
+ if (!(compositionSpec.type === "facet" ? ["start", "center", "end"] : ["start", "center", "end", "plot"]).includes(compositionSpec.align)) {
252
258
  throw new Error("compositionSpec.align must be start, center, or end.");
253
259
  }
254
260
  if (!isPlainObject(compositionSpec.padding)) {
@@ -284,7 +290,7 @@ export function ownCompositionSpec(compositionSpec, children) {
284
290
  }
285
291
  const allowed = [
286
292
  "id", "type", "direction", "children", "columns", "gap", "align",
287
- "padding", "facet", "spacing", "topLane"
293
+ "padding", "facet", "spacing", "topLane", "share", "legends"
288
294
  ];
289
295
  const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
290
296
  if (unknown !== undefined) {
@@ -317,6 +323,14 @@ export function ownCompositionSpec(compositionSpec, children) {
317
323
  ) {
318
324
  throw new Error("Concat compositionSpec does not accept facet properties.");
319
325
  }
326
+ if (compositionSpec.legends !== undefined) {
327
+ if (facet) throw new Error("Facet uses guides.legend, not composition legends.");
328
+ normalizeCompositionLegends(compositionSpec.legends);
329
+ }
330
+ if (compositionSpec.share !== undefined) {
331
+ if (facet) throw new Error("Facet uses scales, not composition share.");
332
+ normalizeCompositionShares(compositionSpec.share);
333
+ }
320
334
  validateLayout(compositionSpec);
321
335
  const owned = ownProgramState(compositionSpec);
322
336
  if (isOwned(children)) {
@@ -46,12 +46,17 @@ export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state")
46
46
  if (Array.isArray(value)) {
47
47
  clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
48
48
  } else if (isPlainObject(value)) {
49
- clone = Object.fromEntries(
50
- Object.entries(value).map(([key, item]) => [
51
- key,
52
- cloneAndFreeze(item, ancestors, `${path}.${key}`)
53
- ])
54
- );
49
+ clone = {};
50
+ // Snapshot entries before descending, as with Object.fromEntries, but avoid
51
+ // allocating a second entries array and a pair for every stored property.
52
+ for (const [key, item] of Object.entries(value)) {
53
+ const child = cloneAndFreeze(item, ancestors, `${path}.${key}`);
54
+ if (key === "__proto__") {
55
+ Object.defineProperty(clone, key, {
56
+ value: child, enumerable: true, writable: true, configurable: true
57
+ });
58
+ } else clone[key] = child;
59
+ }
55
60
  } else {
56
61
  throw new TypeError("ChartProgram state only supports plain objects and arrays.");
57
62
  }
@@ -48,9 +48,25 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
48
48
  materializationConfigs,
49
49
  ["regressionFits", dataOwner?.current ?? id]
50
50
  ).value;
51
+ materializationConfigs = removeMaterializationConfig(
52
+ materializationConfigs,
53
+ ["rowProvenance", dataOwner?.current ?? id]
54
+ ).value;
51
55
  const context = program.context.currentData === id
52
56
  ? freezeOwned({ ...program.context, currentData: undefined })
53
57
  : program.context;
54
58
  if (context === program.context && materializationConfigs === program.materializationConfigs) return program;
55
59
  return program._clone({ context, materializationConfigs });
56
60
  }
61
+
62
+ // Scoped planners temporarily annotate context. Restore only those keys after
63
+ // the action so genuine target/data context changes survive, and absent keys
64
+ // remain absent in persistence rather than becoming serialized undefined tags.
65
+ export function restoreProgramContextKeys(program, reference, keys) {
66
+ const context = { ...program.context };
67
+ for (const key of keys) {
68
+ if (Object.hasOwn(reference.context, key)) context[key] = reference.context[key];
69
+ else delete context[key];
70
+ }
71
+ return program._clone({ context: freezeOwned(context) });
72
+ }
@@ -13,7 +13,7 @@ const DATA_VALUE_KEYS = new Set([
13
13
  const DATA_SOURCE_CONFIGS = new Set([
14
14
  "boxPlot", "density", "ecdfPlot", "endpointPlot", "errorBand", "errorBar",
15
15
  "gradientPlot", "horizon", "intervalPlot", "markFilter", "raincloudPlot",
16
- "regression", "violinPlot"
16
+ "regression", "violinPlot", "radarPlot"
17
17
  ]);
18
18
  const MARK_ID_KEYS = new Set([
19
19
  "bandId", "bodyId", "centerId", "connectorId", "intervalId", "lineId",
@@ -98,7 +98,7 @@ export function collectSemanticScaleReferences(program) {
98
98
  }
99
99
  }
100
100
  collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
101
- if (["scale", "colorScale", "sizeScale"].includes(key)) {
101
+ if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
102
102
  append(references, reference(
103
103
  "scale", value, "semanticGuide", "guides", path
104
104
  ));
@@ -134,6 +134,10 @@ export function collectMarkConfigReferences(program) {
134
134
  append(references, reference(
135
135
  "mark", layer.source, "layer", layer.id, ["source"]
136
136
  ));
137
+ append(references, reference(
138
+ "mark", layer.derivedBindings?.overlay?.source, "layer", layer.id,
139
+ ["derivedBindings", "overlay", "source"]
140
+ ));
137
141
  }
138
142
  for (const [ownerId, config] of Object.entries(program.markConfigs)) {
139
143
  collectKnownKeys(config, [], (key, value, path, parentPath) => {
@@ -204,13 +208,16 @@ export function collectGuideConfigReferences(program) {
204
208
  for (const target of Object.keys(program.guideConfigs?.legendGroups ?? {})) {
205
209
  append(references, reference("mark", target, "legend", target, ["legendGroups", target]));
206
210
  }
211
+ for (const target of Object.keys(program.guideConfigs?.suppressedLegends ?? {})) {
212
+ append(references, reference("mark", target, "legend", target, ["suppressedLegends", target]));
213
+ }
207
214
  for (const [family, configs] of Object.entries(program.guideConfigs ?? {})) {
208
215
  collectKnownKeys(configs, [], (key, value, path, parentPath) => {
209
216
  const ownerId = parentPath.length === 0 ? family : parentPath.join(".");
210
217
  const relativePath = parentPath.length === 0
211
218
  ? path
212
219
  : path.slice(parentPath.length);
213
- if (["scale", "colorScale", "sizeScale"].includes(key)) {
220
+ if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
214
221
  append(references, reference(
215
222
  "scale", value, family, ownerId, relativePath
216
223
  ));
@@ -240,6 +247,12 @@ export function collectSelectionReferences(program) {
240
247
  append(references, reference(
241
248
  "mark", config.target, "selection", id, ["target"]
242
249
  ));
250
+ append(references, reference(
251
+ "selection", config.sourceSelection, "selection", id, ["sourceSelection"]
252
+ ));
253
+ append(references, reference(
254
+ "data", config.selector?.rows?.data, "selection", id, ["selector", "rows", "data"]
255
+ ));
243
256
  }
244
257
  for (const [id, config] of Object.entries(
245
258
  program.materializationConfigs.highlights ?? {}
@@ -250,6 +263,9 @@ export function collectSelectionReferences(program) {
250
263
  append(references, reference(
251
264
  "selection", config.selection, "highlight", id, ["selection"]
252
265
  ));
266
+ append(references, reference(
267
+ "selection", config.groupSelection, "highlight", id, ["groupSelection"]
268
+ ));
253
269
  }
254
270
  return freezeOwned(references);
255
271
  }
@@ -127,7 +127,14 @@ export function validateFontWeight(value, label, numberType = "number") {
127
127
  return value;
128
128
  }
129
129
 
130
+ export function validateTextAlignment(style, label) {
131
+ if (Object.hasOwn(style, "align") && !["left", "center", "right"].includes(style.align)) {
132
+ throw new TypeError(`${label} align requires left, center or right.`);
133
+ }
134
+ }
135
+
130
136
  export function validateTextStyle(config, label) {
137
+ validateTextAlignment(config, label);
131
138
  validateNonEmptyString(config.color, `${label} color`);
132
139
  validatePositiveFinite(config.fontSize, `${label} fontSize`);
133
140
  validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
@@ -253,7 +253,7 @@ const marks = (...types) => Object.freeze(types);
253
253
  export const APPEARANCE_MARK_TYPES = Object.freeze({
254
254
  encodeColor: marks('point', 'line', 'bar', 'area', 'arc', 'rect', 'text'),
255
255
  encodeStroke: marks('point', 'line', 'area', 'bar', 'rect', 'arc', 'rule', 'tick'),
256
- encodeOpacity: marks('point', 'rule', 'line', 'bar', 'rect'),
256
+ encodeOpacity: marks('point', 'rule', 'line', 'area', 'bar', 'rect'),
257
257
  encodeStrokeWidth: marks('rule', 'line', 'rect'),
258
258
  encodeStrokeDash: marks('line', 'rule'),
259
259
  encodeSize: marks('point'),
@@ -261,6 +261,12 @@ export const APPEARANCE_MARK_TYPES = Object.freeze({
261
261
  encodeAngle: marks('point', 'tick')
262
262
  });
263
263
 
264
+ // Constant appearance can support more marks than field mapping. Inspection
265
+ // and execution share this distinction instead of advertising unusable fields.
266
+ export const FIXED_APPEARANCE_MARK_TYPES = Object.freeze({
267
+ encodeStrokeWidth: marks('rule', 'line', 'rect', 'point', 'bar', 'area', 'arc', 'tick')
268
+ });
269
+
264
270
  // Generated default action methods: start
265
- export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries"]);
271
+ export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries","createOverlay","editOverlay","createLayerPlot","createAdjacentPlot","editCompositionScales"]);
266
272
  // Generated default action methods: end
@@ -0,0 +1,24 @@
1
+ import { validateOptionObject } from "../core/validation.js";
2
+ import { validateUserId } from "../core/identifiers.js";
3
+
4
+ function normalizeTarget(target) {
5
+ if (typeof target === "string") return { childPath: [], id: validateUserId(target, "Action target") };
6
+ validateOptionObject(target, ["childPath", "id", "facetSource"], "Scoped action target");
7
+ if (!Array.isArray(target.childPath) || target.childPath.some(id => typeof id !== "string")) {
8
+ throw new TypeError("Scoped action target requires a childPath array.");
9
+ }
10
+ if (target.facetSource !== undefined && target.facetSource !== true) throw new TypeError("facetSource must be true when supplied.");
11
+ target.childPath.forEach(id => validateUserId(id, "Target child id"));
12
+ return { childPath: [...target.childPath], ...(target.facetSource ? { facetSource: true } : {}), ...(target.id === undefined ? {} : { id: validateUserId(target.id, "Action target id") }) };
13
+ }
14
+
15
+
16
+ export function normalizeActionTargets(value) {
17
+ const requested = Array.isArray(value) ? value : [value];
18
+ if (!requested.length) throw new Error("Action target list must not be empty.");
19
+ const targets = requested.map(normalizeTarget);
20
+ if (new Set(targets.map(target => JSON.stringify(target))).size !== targets.length) {
21
+ throw new Error("Action targets must be unique.");
22
+ }
23
+ return targets;
24
+ }
@@ -370,3 +370,10 @@ export function requestedBin2DTransform(transform) {
370
370
  const { resolved: _resolved, ...requested } = transform;
371
371
  return cloneAndFreeze(requested);
372
372
  }
373
+
374
+ // Preserve caller-authored aliases; only the canonical generated count field
375
+ // receives a human-readable title when guide creation is deferred.
376
+ export function inferBin2DFieldTitle(transform, id, field) {
377
+ return transform?.type === 'bin2d' && transform.as.count === field &&
378
+ normalizeOutputFields(id, undefined, false).count === field ? 'Count' : field;
379
+ }
@@ -1,12 +1,15 @@
1
1
  import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
2
- import {isContinuousColorScaleType} from './scales/types.js';
2
+ import {isContinuousColorScaleType,isDiscretizedColorScaleType} from './scales/types.js';
3
3
  import {isContinuousSizeScaleType} from './scales/size.js';
4
4
 
5
- export function isColorSizeChannels(channels) {
6
- return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
5
+ export function isColorSizeChannels(channels,combine='color-size') {
6
+ return Array.isArray(channels)&&(combine==='color-size-shape'
7
+ ? channels.length===3&&channels.includes('shape')&&channels.includes('size')&&['color','stroke'].some(c=>channels.includes(c))
8
+ : channels.length===2&&channels.includes('color')&&channels.includes('size'));
7
9
  }
8
10
 
9
- export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
11
+ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate,combine='color-size',channels}) {
12
+ if(combine==='color-size-shape')return resolveColorSizeShapeMapping({layer,scales,coordinate,channels});
10
13
  const color=layer?.encoding?.color,size=layer?.encoding?.size;
11
14
  const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
12
15
  if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
@@ -26,7 +29,20 @@ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
26
29
  return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
27
30
  }
28
31
 
29
- export function normalizeColorSizeSymbol(value,previous) {
32
+ export function normalizeColorSizeSymbol(value,previous,mapping) {
33
+ if(mapping?.combine==='color-size-shape') {
34
+ if(value===undefined)return {...(previous??{type:'point'})};
35
+ if(value==='auto')return {type:'point'};
36
+ validateOptionObject(value,['type','fill','stroke','strokeWidth','opacity'],'Color-size-shape symbol');
37
+ if(value.type!==undefined&&value.type!=='point')throw new Error('Combined symbols require type point.');
38
+ const symbol={type:'point',...previous,...value};
39
+ const mapped=mapping.colorChannel==='stroke'?'stroke':'fill';
40
+ if(Object.hasOwn(symbol,mapped))throw new Error(`Combined symbols cannot override mapped ${mapped}.`);
41
+ for(const key of ['fill','stroke'])if(symbol[key]!==undefined)validateNonEmptyString(symbol[key],`Combined symbol ${key}`);
42
+ if(symbol.strokeWidth!==undefined)validateNonNegativeFinite(symbol.strokeWidth,'Combined symbol strokeWidth');
43
+ if(symbol.opacity!==undefined)validateUnitInterval(symbol.opacity,'Combined symbol opacity');
44
+ return symbol;
45
+ }
30
46
  if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
31
47
  if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
32
48
  validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
@@ -37,3 +53,38 @@ export function normalizeColorSizeSymbol(value,previous) {
37
53
  validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
38
54
  return symbol;
39
55
  }
56
+
57
+ function resolveColorSizeShapeMapping({layer,scales,coordinate,channels}) {
58
+ const hues=channels===undefined?['color','stroke'].filter(c=>layer?.encoding?.[c]?.scale!==undefined)
59
+ :channels.filter(c=>['color','stroke'].includes(c));
60
+ if(hues.length!==1)throw new Error('Color-size-shape requires one color or stroke channel; select channels explicitly.');
61
+ const colorChannel=hues[0],hue=layer?.encoding?.[colorChannel],size=layer?.encoding?.size,shape=layer?.encoding?.shape;
62
+ const colorScale=scales[hue?.scale],sizeScale=scales[size?.scale],shapeScale=scales[shape?.scale];
63
+ if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
64
+ hue?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
65
+ !['nominal','ordinal'].includes(shape?.fieldType)||typeof hue.field!=='string'||
66
+ [size,shape].some(e=>e.field!==hue.field||e.data!==hue.data||e.aggregate!==hue.aggregate)||
67
+ !isDiscretizedColorScaleType(colorScale?.type)||!isDiscretizedColorScaleType(sizeScale?.type)||shapeScale?.type!=='ordinal') {
68
+ throw new Error('Color-size-shape requires one Cartesian point owner with the same field and grain on interval hue/size and ordinal shape.');
69
+ }
70
+ if(!sameOrderedValues(colorScale.thresholds,sizeScale.thresholds))throw new Error('Combined interval scales require identical threshold boundaries.');
71
+ return {target:layer.id,field:hue.field,fieldType:'quantitative',colorScale:hue.scale,sizeScale:size.scale,
72
+ shapeScale:shape.scale,colorChannel,domain:[...colorScale.domain]};
73
+ }
74
+
75
+ export function validateColorSizeShapeSamples(sampling,scale) {
76
+ const cuts=scale.thresholds;
77
+ if(sampling.mode!=='values'||sampling.values.length!==cuts.length+1||sampling.values.some((value,index)=>
78
+ !Number.isFinite(value)||index>0&&value<cuts[index-1]||index<cuts.length&&value>=cuts[index])) {
79
+ throw new Error('Color-size-shape requires one explicit finite value in each ordered threshold interval.');
80
+ }
81
+ return [...sampling.values];
82
+ }
83
+
84
+ export function colorSizeScaleIds(config) {
85
+ return [config.colorScale,config.sizeScale,...(config.shapeScale===undefined?[]:[config.shapeScale])];
86
+ }
87
+
88
+ export function colorSizeChannels(config) {
89
+ return [config.colorChannel??'color','size',...(config.shapeScale===undefined?[]:['shape'])];
90
+ }
@@ -91,3 +91,13 @@ export function resolveDisplayLabelColor(value, map, fallback) {
91
91
  const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
92
92
  return entry?.color ?? fallback;
93
93
  }
94
+
95
+ export function normalizeLegendDisplayLabelMap(descriptor, map, scaleType) {
96
+ if (["size", "colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
97
+ return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
98
+ }
99
+ if (descriptor.family !== "categorical" && !(descriptor.family === "discrete-size" && scaleType === "ordinal")) {
100
+ throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
101
+ }
102
+ return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
103
+ }
@@ -1 +1 @@
1
- import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\r\n\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title text must be one line.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
1
+ import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title line breaks must use LF, CR or CRLF.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
@@ -115,9 +115,16 @@ export function resolveFacetFamily(semanticSpec) {
115
115
  semanticSpec.layers.length === 0) {
116
116
  throw new Error("facet requires at least one materializable layer.");
117
117
  }
118
- const primary = semanticSpec.layers.filter(layer =>
119
- !(layer.mark?.type === "text" && layer.source !== undefined)
120
- );
118
+ // Literal positioned notes decorate a chart rather than define its data
119
+ // coordinate family. Keep validating/materializing them, but do not confuse
120
+ // a plot-space Cartesian annotation with a second Cartesian chart in a polar
121
+ // or parallel facet. A text-only chart still supplies its own primary layer.
122
+ const annotations = semanticSpec.layers.filter(layer => layer.mark?.type === 'text' &&
123
+ ['x', 'y', 'text'].every(channel => Object.hasOwn(layer.encoding?.[channel] ?? {}, 'datum')));
124
+ const dataLayers = semanticSpec.layers.filter(layer =>
125
+ !(layer.mark?.type === 'text' && layer.source !== undefined) && !annotations.includes(layer));
126
+ const primary = dataLayers.length ? dataLayers : annotations;
127
+ for (const note of annotations) classifyPrimaryLayer(semanticSpec, note);
121
128
  if (primary.length === 0) {
122
129
  throw new Error("facet requires at least one primary materializable layer.");
123
130
  }
@@ -1,3 +1,4 @@
1
+ import {colorSizeScaleIds} from "../colorSizeLegend.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
3
 
3
4
 
@@ -167,7 +168,7 @@ function requireLegendChildren(children) {
167
168
  }
168
169
 
169
170
  function scaleIds(config, kind) {
170
- const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
171
+ const ids = kind === "colorSize" ? colorSizeScaleIds(config)
171
172
  : config.scales ?? (config.scale === undefined ? [] : [config.scale]);
172
173
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
173
174
  typeof id === "string" && id.length > 0