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,6 @@
1
+ import { validateLegendOverflow } from "../grammar/legendOverflow.js";
2
+ import { normalizeTextOverrides } from "../grammar/text.js";
3
+ import { normalizeLegendDisplayLabelMap } from "../grammar/displayLabels.js";
1
4
  import { validateFacetAxisPolicy } from "../core/facetAxisPolicy.js";
2
5
  import { resolveFacetFamily } from "../grammar/facets/dependencies.js";
3
6
  import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
@@ -18,15 +21,13 @@ import { axisGraphicIds } from "../selectors/axes.js";
18
21
 
19
22
  import {findLayer} from '../selectors/layers.js';
20
23
  import {findCoordinate} from '../selectors/coordinates.js';
21
- import {resolveColorSizeMapping,normalizeColorSizeSymbol} from '../grammar/colorSizeLegend.js';
24
+ import {resolveColorSizeMapping,normalizeColorSizeSymbol,colorSizeChannels,validateColorSizeShapeSamples} from '../grammar/colorSizeLegend.js';
22
25
  import{validateLegendClip}from"../grammar/legendClip.js";
23
26
  import {validateLegendLayerOpacity}from"../grammar/legendOpacity.js";
24
27
  import { isSizeShapeChannels, resolveSizeShapeMapping } from "../grammar/legendCombinations.js";
25
- import { describeLegendBlock } from "../grammar/legendBlocks.js";
28
+ import { describeLegendBlock, validateLegendChannels } from "../grammar/legendBlocks.js";
26
29
  import { localLegendPosition } from "../grammar/legendGroups.js";
27
30
  import { normalizeLegendBorderDash } from "../grammar/legendBorder.js";
28
- import { normalizeNumericDisplayLabelMap } from "../grammar/displayLabels.js";
29
- import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
30
31
  import { validateGraphicItemLimit } from "../core/validation.js";
31
32
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
32
33
  import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
@@ -50,7 +51,8 @@ import { validateUserId } from "../core/identifiers.js";
50
51
  import { collectAllResourceReferences } from "../core/resourceReferences.js";
51
52
  import { requiredTraceExtension } from "../core/extensionRegistry.js";
52
53
  import { resolveDatasetReference } from "../selectors/datasets.js";
53
- import { normalizeMarkSelector } from "../grammar/markSelection.js";
54
+ import { normalizeMarkSelector, validateRowSelectionDataset } from "../grammar/markSelection.js";
55
+ import { highlightTargets } from "../selectors/selectionTargets.js";
54
56
  import { parseSemanticPath } from "../grammar/schemas/semanticPath.js";
55
57
  import { validateStoredSemanticValue } from "../actions/primitives/semantic.js";
56
58
  import { validateGraphicType, isGraphicContainerType } from "../grammar/schemas/graphic.js";
@@ -227,6 +229,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
227
229
  throw new TypeError("Facet row field title requires a row-column facet grid.");
228
230
  }
229
231
  }
232
+ validateGuideVisibilityState(program);
230
233
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
231
234
  for (const config of [
232
235
  ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
@@ -319,7 +322,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
319
322
  }
320
323
  const config = program.guideConfigs.axis?.[id];
321
324
  requireObject(config, `${path}.guides.axis.${id}`);
322
- validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title"], "Stored named axis config");
325
+ validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title", "visibility"], "Stored named axis config");
323
326
  const components = ["line", "ticks", "labels", "title"];
324
327
  if (!components.some(component => config[component] !== undefined)) {
325
328
  throw invalidSnapshot("Named axes require at least one retained component.", path);
@@ -340,30 +343,50 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
340
343
  coordinate: semanticAxis.coordinate, position: semanticAxis.position });
341
344
  }
342
345
  if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
346
+ if (program.guideConfigs.suppressedLegends !== undefined) {
347
+ requireObject(program.guideConfigs.suppressedLegends, `${path}.materializationConfigs.guides.suppressedLegends`);
348
+ for (const [target, config] of Object.entries(program.guideConfigs.suppressedLegends)) {
349
+ validateUserId(target, "Suppressed legend target");
350
+ requireObject(config, `${path}.materializationConfigs.guides.suppressedLegends.${target}`);
351
+ if (!semantic.layers.some(layer => layer.id === target) || Object.keys(config).some(key => key !== "channels")) {
352
+ throw invalidSnapshot("Suppressed legend requires an existing mark and channel selection.", path);
353
+ }
354
+ validateLegendChannels(config.channels, "Suppressed legend");
355
+ }
356
+ }
343
357
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
358
+ validateLegendOverflow(kind, config);
344
359
  validateLegendClip(kind,config);
360
+ for (const [key, override] of Object.entries(config.blockOverrides ?? {})) {
361
+ if (override.labelMap !== undefined) {
362
+ const descriptor = describeLegendBlock(program, kind, config);
363
+ if (key !== descriptor.key) throw invalidSnapshot("Display labels require their existing legend block owner.", path);
364
+ normalizeLegendDisplayLabelMap(descriptor, override.labelMap, program.resolvedScales[descriptor.scaleIds[0]]?.type);
365
+ }
366
+ }
345
367
  validateLegendSpacing(kind,config);
346
368
  if (localLegendPosition(config) !== undefined) {
347
369
  const descriptor = describeLegendBlock(program, kind, config);
348
370
  for (const [key, override] of Object.entries(config.blockOverrides)) {
349
- if (override.position !== undefined && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
371
+ if ((override.position !== undefined || override.alongOffset !== undefined) && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
350
372
  }
351
373
  }
352
374
  for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
353
375
  if(kind==='colorSize') {
354
- if(config.combine!=='color-size')throw invalidSnapshot('Color-size legend requires its combined owner.',path);
376
+ if(!['color-size','color-size-shape'].includes(config.combine))throw invalidSnapshot('Color-size legend requires its combined owner.',path);
355
377
  const layer=findLayer(program,config.target);
356
- const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
357
- for(const key of ['target','field','fieldType','colorScale','sizeScale','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
378
+ const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate),combine:config.combine,channels:colorSizeChannels(config)});
379
+ for(const key of ['target','field','fieldType','colorScale','sizeScale','shapeScale','colorChannel','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
358
380
  const guide=semantic.guides.legend?.colorSize;
359
- if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
360
- requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
361
- if(config.symbol.type!=='point'||!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity'))throw invalidSnapshot('Color-size symbol requires point type.',path);
381
+ if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title||guide.shapeScale!==mapping.shapeScale||guide.colorChannel!==mapping.colorChannel)throw invalidSnapshot('Stale color-size semantic owner.',path);
382
+ requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol,undefined,config);
383
+ if(config.symbol.type!=='point'||config.combine==='color-size'&&(!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity')))throw invalidSnapshot('Color-size symbol requires point type.',path);
362
384
  const sample=config.sampling;requireObject(sample,path);
363
385
  exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
364
386
  if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
365
387
  validateGeneratedItemLimit(sample.count,'Color-size samples');
366
- if(sample.mode==='values'){
388
+ if(config.combine==='color-size-shape')validateColorSizeShapeSamples(sample,program.resolvedScales[mapping.colorScale]);
389
+ else if(sample.mode==='values'){
367
390
  requireArray(sample.values,path);
368
391
  if(!sample.values.length||sample.values.length>100||sample.values.some((v,i)=>!Number.isFinite(v)||i>0&&v<=sample.values[i-1]||v<Math.min(...mapping.domain)||v>Math.max(...mapping.domain)||[mapping.colorScale,mapping.sizeScale].some(id=>program.resolvedScales[id].type==='log'&&v<=0)))throw invalidSnapshot('Invalid exact color-size samples.',path);
369
392
  }
@@ -385,15 +408,6 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
385
408
  if (config.border && Object.hasOwn(config.border, "strokeDash")) {
386
409
  normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
387
410
  }
388
- if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
389
- for (const [key, override] of Object.entries(config.blockOverrides)) {
390
- if (override.labelMap === undefined) continue;
391
- if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
392
- throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
393
- }
394
- normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
395
- }
396
- }
397
411
  if (config.titleStyle !== undefined) {
398
412
  validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
399
413
  validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
@@ -458,13 +472,25 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
458
472
  }
459
473
  }
460
474
 
475
+ for (const [id, config] of Object.entries(program.materializationConfigs.rowProvenance ?? {})) {
476
+ exactKeys(config, ["indices"], `${path}.materializationConfigs.rowProvenance.${id}`);
477
+ const { indices } = config;
478
+ const dataset = resources.get("data").get(id), source = resources.get("data").get(dataset?.source);
479
+ if (dataset?.transform?.[0]?.type !== "markFilter" || !source || !Array.isArray(indices) ||
480
+ indices.length !== dataset.values?.length || Array.from(indices).some((index, offset) =>
481
+ !Number.isInteger(index) || index < 0 || index >= source.values.length ||
482
+ (offset > 0 && index <= indices[offset - 1]) || !sameLookupValue(dataset.values[offset], source.values[index]))) {
483
+ throw invalidSnapshot("Invalid filtered row provenance.", path);
484
+ }
485
+ }
461
486
  const selections = program.materializationConfigs.selections ?? {};
462
487
  requireObject(selections, `${path}.materializationConfigs.selections`);
463
488
  for (const [id, selection] of Object.entries(selections)) {
464
489
  validateUserId(id, "Selection id");
465
- exactKeys(selection, ["target", "selector"], `${path}.materializationConfigs.selections.${id}`);
490
+ exactKeys(selection, ["target", "selector", ...(Object.hasOwn(selection, "sourceSelection") ? ["sourceSelection"] : [])], `${path}.materializationConfigs.selections.${id}`);
466
491
  validateUserId(selection.target, "Selection target");
467
492
  normalizeMarkSelector(selection.selector);
493
+ if (Object.hasOwn(selection, "sourceSelection")) validateUserId(selection.sourceSelection, "Composite selection owner");
468
494
  }
469
495
  resources.set("selection", new Map(Object.entries(selections)));
470
496
  for (const reference of collectAllResourceReferences(program)) {
@@ -473,6 +499,24 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
473
499
  throw invalidSnapshot(`Unknown ${reference.kind} "${reference.id}" referenced by ${reference.ownerKind} "${reference.ownerId}".`, path);
474
500
  }
475
501
  }
502
+ for (const selection of Object.values(selections)) {
503
+ if (selection.sourceSelection !== undefined) {
504
+ const owner = selections[selection.sourceSelection];
505
+ if (owner.sourceSelection !== undefined || owner.target === selection.target ||
506
+ !highlightTargets(program, owner.target).includes(selection.target)) {
507
+ throw invalidSnapshot("Invalid composite selection owner.", path);
508
+ }
509
+ }
510
+ if (selection.selector.rows !== undefined) validateRowSelectionDataset(selection.selector.rows, resources.get("data").get(selection.selector.rows.data));
511
+ }
512
+ for (const config of Object.values(program.materializationConfigs.highlights ?? {})) {
513
+ if (config.groupSelection === undefined) continue;
514
+ const owner = selections[config.groupSelection], selection = selections[config.selection];
515
+ if (owner.sourceSelection !== undefined || selection.target !== config.target ||
516
+ (config.selection !== config.groupSelection && selection.sourceSelection !== config.groupSelection)) {
517
+ throw invalidSnapshot("Invalid composite highlight selection group.", path);
518
+ }
519
+ }
476
520
  for (const [id, scale] of Object.entries(program.resolvedScales)) {
477
521
  requireObject(scale, `${path}.resolvedScales.${id}`);
478
522
  requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
@@ -486,6 +530,10 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
486
530
  }
487
531
  }
488
532
  for (const id of Object.keys(program.markConfigs)) {
533
+ if (program.markConfigs[id].textOverrides !== undefined) {
534
+ if (resources.get("mark").get(id).mark.type !== "text") throw invalidSnapshot("Text overrides require a text mark.", path);
535
+ normalizeTextOverrides(program.markConfigs[id].textOverrides);
536
+ }
489
537
  validateRegressionPlacement(program.markConfigs[id].regression?.placement);
490
538
  requestedMarkClip(program.markConfigs[id]);
491
539
  validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
@@ -517,3 +565,28 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
517
565
  }));
518
566
  }
519
567
  }
568
+
569
+ function validateGuideVisibilityState(program) {
570
+ const check = (visibility, allowed) => {
571
+ if (visibility === undefined) return;
572
+ if (!isPlainObject(visibility) || Object.entries(visibility).some(([key, value]) => !allowed.includes(key) || typeof value !== "boolean"))
573
+ throw new TypeError("Stored guide visibility must contain only supported boolean flags.");
574
+ };
575
+ for (const config of Object.values(program.guideConfigs.axis ?? {})) {
576
+ check(config.visibility, ["all", "line", "ticks", "labels", "title"]);
577
+ for (const dimension of config.axes?.dimensions ?? []) check(dimension.visibility, ["all", "line", "ticks", "labels", "title"]);
578
+ }
579
+ for (const config of Object.values(program.guideConfigs.legend ?? {})) check(config.visibility, ["all", "labels", "titleStyle"]);
580
+ for (const config of Object.values(program.guideConfigs.grid ?? {})) if (Object.hasOwn(config, "visible") && typeof config.visible !== "boolean")
581
+ throw new TypeError("Stored grid visible must be a boolean.");
582
+ const paint = program.materializationConfigs.guideVisibilityPaint;
583
+ if (paint !== undefined) {
584
+ if (!isPlainObject(paint)) throw new TypeError("Stored guide visibility paint must be an object.");
585
+ for (const items of Object.values(paint)) {
586
+ if (!isPlainObject(items)) throw new TypeError("Stored guide visibility paint entries must be objects.");
587
+ for (const value of Object.values(items)) if (!isPlainObject(value) || Object.keys(value).length !== 1 ||
588
+ !Object.hasOwn(value, "opacity") || value.opacity !== null && (!Number.isFinite(value.opacity) || value.opacity < 0 || value.opacity > 1))
589
+ throw new TypeError("Stored guide visibility paint requires an original opacity or null.");
590
+ }
591
+ }
592
+ }
@@ -26,6 +26,9 @@ const DRAWERS = Object.freeze({
26
26
  path: drawPathGraphic
27
27
  });
28
28
 
29
+ // Distinguish a completed empty preflight from a direct draw that still needs one.
30
+ const NO_ISOLATED_GLYPHS = Object.freeze({});
31
+
29
32
  const DRAWING_CONTEXT_METHODS = Object.freeze([
30
33
  "save",
31
34
  "restore",
@@ -174,14 +177,14 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
174
177
  return;
175
178
  }
176
179
  if (!Array.isArray(object.items)) {
177
- drawConcreteGraphic(context, id, object, isolated);
180
+ drawConcreteGraphic(context, id, object, isolated, options);
178
181
  }
179
182
  },
180
183
  item({ id, object, owner }) {
181
184
  drawConcreteGraphic(context, id, {
182
185
  ...object,
183
186
  type: object.type ?? owner.type
184
- }, isolated);
187
+ }, isolated, options);
185
188
  },
186
189
  exit({ id, object }) {
187
190
  if (
@@ -195,7 +198,7 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
195
198
  });
196
199
  }
197
200
 
198
- export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas) {
201
+ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas, drawingOptions = {}) {
199
202
  const graphicSpec = requireProgramGraphicSpec(program);
200
203
 
201
204
  requireCanvasContext(context);
@@ -253,7 +256,9 @@ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, c
253
256
  try {
254
257
  context.scale(pixelRatio, pixelRatio);
255
258
  context.clearRect(0, 0, width, height);
256
- drawResolvedGraphicSpec(target, context, {isolated, pixelRatio, createCanvas});
259
+ drawResolvedGraphicSpec(target, context, {
260
+ ...drawingOptions, isolated: isolated ?? NO_ISOLATED_GLYPHS, pixelRatio, createCanvas
261
+ });
257
262
  } finally {
258
263
  context.restore();
259
264
  }
@@ -263,10 +268,10 @@ export function render(program, context, options) {
263
268
  return renderCanvasProgram(program, context, options);
264
269
  }
265
270
 
266
- function drawConcreteGraphic(context, id, graphic, isolated) {
271
+ function drawConcreteGraphic(context, id, graphic, isolated, options) {
267
272
  if (graphic.type === "collection") {
268
273
  for (const item of graphic.items ?? []) {
269
- drawConcreteGraphic(context, item.id ?? id, item, isolated);
274
+ drawConcreteGraphic(context, item.id ?? id, item, isolated, options);
270
275
  }
271
276
  return;
272
277
  }
@@ -277,6 +282,6 @@ function drawConcreteGraphic(context, id, graphic, isolated) {
277
282
  if (["normal","difference"].includes(graphic.properties?.blend)) {
278
283
  drawIsolatedGlyph(context, id, graphic, draw, isolated);
279
284
  } else {
280
- draw(context, id, graphic);
285
+ draw(context, id, graphic, options);
281
286
  }
282
287
  }
@@ -4,7 +4,7 @@ import { validateConcreteGraphicProperties, validateConcreteGraphicValue } from
4
4
  "../../grammar/schemas/concreteGraphic.js";
5
5
  import { normalizeRendererFontWeight } from "../text.js";
6
6
 
7
- function drawText(context, child, collectionId) {
7
+ function drawText(context, child, collectionId, { resolveFontFamily } = {}) {
8
8
  const properties = child.properties ?? {};
9
9
  const graphicId = child.id ?? collectionId;
10
10
  validateConcreteGraphicProperties("text", properties);
@@ -77,7 +77,7 @@ function drawText(context, child, collectionId) {
77
77
  context.fillStyle = resolveCanvasSolidPaint(fill);
78
78
  context.globalAlpha = opacity;
79
79
  const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
80
- context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
80
+ context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${resolveFontFamily?.(fontFamily) ?? fontFamily}`;
81
81
  context.textAlign = textAlign;
82
82
  context.textBaseline = textBaseline;
83
83
  context.translate(x, y);
@@ -89,13 +89,13 @@ function drawText(context, child, collectionId) {
89
89
  }
90
90
  }
91
91
 
92
- export function drawTextGraphic(context, id, graphic) {
92
+ export function drawTextGraphic(context, id, graphic, options) {
93
93
  if (graphic.items) {
94
94
  for (const item of graphic.items) {
95
- drawText(context, item, id);
95
+ drawText(context, item, id, options);
96
96
  }
97
97
  return;
98
98
  }
99
99
 
100
- drawText(context, { id, properties: graphic.properties }, id);
100
+ drawText(context, { id, properties: graphic.properties }, id, options);
101
101
  }
@@ -8,3 +8,28 @@ export async function loadNativeBackend() {
8
8
  throw error;
9
9
  }
10
10
  }
11
+
12
+ // Supply installed glyph fallbacks after the authored family, as browsers do.
13
+ // This resolves font resources only; no text or layout enters program state.
14
+ const SYSTEM_FALLBACKS = [
15
+ "Noto Sans CJK KR", "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic",
16
+ "Noto Sans CJK JP", "Noto Sans JP", "Yu Gothic", "Meiryo",
17
+ "Noto Sans CJK SC", "Noto Sans SC", "PingFang SC", "Microsoft YaHei",
18
+ "Noto Sans CJK TC", "PingFang TC", "Microsoft JhengHei",
19
+ "Arial Unicode MS", "Noto Sans", "DejaVu Sans",
20
+ "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji"
21
+ ];
22
+
23
+ export function nativeFontResolver(globalFonts) {
24
+ const installed = new Set(globalFonts.families.map(entry => entry.family));
25
+ const fallbacks = SYSTEM_FALLBACKS.filter(family => installed.has(family));
26
+ const cache = new Map();
27
+ return requested => {
28
+ if (cache.has(requested)) return cache.get(requested);
29
+ const present = new Set(requested.split(',').map(family => family.trim().replace(/^["']|["']$/g, '').toLowerCase()));
30
+ const extra = fallbacks.filter(family => !present.has(family.toLowerCase()));
31
+ const resolved = [requested, ...extra.map(family => `"${family}"`)].join(', ');
32
+ cache.set(requested, resolved);
33
+ return resolved;
34
+ };
35
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
 
4
- import { loadNativeBackend } from "./nativeBackend.js";
4
+ import { loadNativeBackend, nativeFontResolver } from "./nativeBackend.js";
5
5
 
6
6
  import {
7
7
  drawResolvedGraphicSpec,
@@ -73,10 +73,10 @@ export async function renderToPDFBuffer(program, options = {}) {
73
73
  const target = resolveGraphicRenderTarget(graphicSpec);
74
74
  requirePDFPageDimensions(target);
75
75
  preflightCanvasGraphicSpec(target);
76
- const { PDFDocument, createCanvas } = await loadNativeBackend();
76
+ const { PDFDocument, createCanvas, GlobalFonts } = await loadNativeBackend();
77
77
  const document = new PDFDocument(metadata);
78
78
  const context = document.beginPage(target.width, target.height);
79
- drawResolvedGraphicSpec(target, context, {createCanvas});
79
+ drawResolvedGraphicSpec(target, context, {createCanvas, resolveFontFamily: nativeFontResolver(GlobalFonts)});
80
80
  document.endPage();
81
81
  const buffer = document.close();
82
82
  return Object.freeze({
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { renderCanvasProgram } from "./canvas/index.js";
4
4
  import { validateRendererOptions } from "./options.js";
5
- import { loadNativeBackend } from "./nativeBackend.js";
5
+ import { loadNativeBackend, nativeFontResolver } from "./nativeBackend.js";
6
6
 
7
7
  const PNG_OPTIONS = new Set(["output", "pixelRatio"]);
8
8
  const BUFFER_OPTIONS = new Set(["pixelRatio"]);
@@ -10,9 +10,10 @@ const BUFFER_OPTIONS = new Set(["pixelRatio"]);
10
10
  export async function renderToPNGBuffer(program, options = {}) {
11
11
  validateRendererOptions(options, BUFFER_OPTIONS, "renderToPNGBuffer options");
12
12
  const { pixelRatio = 1 } = options;
13
- const { createCanvas } = await loadNativeBackend();
13
+ const { createCanvas, GlobalFonts } = await loadNativeBackend();
14
14
  const canvas = createCanvas(1, 1);
15
- renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas);
15
+ renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas,
16
+ { resolveFontFamily: nativeFontResolver(GlobalFonts) });
16
17
  // Canvas drawing is synchronous; encoding uses the native async worker.
17
18
  const buffer = await canvas.encode("png");
18
19
  return Object.freeze({