ggaction 0.0.23 → 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 (228) hide show
  1. package/CHANGELOG.md +65 -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 +6 -4
  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/basicBin2d.js +1 -1
  27. package/src/actions/data/bin2d.js +2 -2
  28. package/src/actions/data/derived.js +3 -0
  29. package/src/actions/data/filter.js +1 -1
  30. package/src/actions/data/raincloudFilter.js +3 -1
  31. package/src/actions/encodings/appearance.js +26 -7
  32. package/src/actions/encodings/channels.js +1 -0
  33. package/src/actions/encodings/color/continuous.js +37 -34
  34. package/src/actions/encodings/color/index.js +57 -43
  35. package/src/actions/encodings/color/policy.js +15 -0
  36. package/src/actions/encodings/density/resolve.js +3 -2
  37. package/src/actions/encodings/density.js +2 -2
  38. package/src/actions/encodings/offset.js +2 -0
  39. package/src/actions/encodings/pathOrder.js +2 -0
  40. package/src/actions/encodings/position/areaAlignment.js +42 -0
  41. package/src/actions/encodings/position/index.js +37 -3
  42. package/src/actions/encodings/position/policies/line.js +1 -1
  43. package/src/actions/encodings/ranged.js +4 -2
  44. package/src/actions/encodings/remove.js +13 -10
  45. package/src/actions/encodings/ruleAppearance.js +14 -2
  46. package/src/actions/facets/actions.js +1 -1
  47. package/src/actions/guides/axes/axes.js +2 -1
  48. package/src/actions/guides/axes/components.js +5 -0
  49. package/src/actions/guides/axes/edit.js +1 -188
  50. package/src/actions/guides/axes/labels.js +2 -537
  51. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  52. package/src/actions/guides/axes/parallel/policy.js +5 -2
  53. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  54. package/src/actions/guides/axes/parallel.js +9 -4
  55. package/src/actions/guides/axes/tickGroups.js +16 -1
  56. package/src/actions/guides/axes/ticks.js +33 -24
  57. package/src/actions/guides/axes/titles.js +1 -389
  58. package/src/actions/guides/facadeLegend.js +3 -2
  59. package/src/actions/guides/grids/grid.js +3 -2
  60. package/src/actions/guides/grids/resolve.js +2 -1
  61. package/src/actions/guides/guides.js +3 -2
  62. package/src/actions/guides/legends/automatic.js +28 -0
  63. package/src/actions/guides/legends/blocks.js +15 -21
  64. package/src/actions/guides/legends/categorical/actions.js +1 -1
  65. package/src/actions/guides/legends/categorical/components.js +4 -3
  66. package/src/actions/guides/legends/categorical/layout.js +5 -2
  67. package/src/actions/guides/legends/categorical/options.js +9 -14
  68. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  69. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  70. package/src/actions/guides/legends/colorSize.js +51 -14
  71. package/src/actions/guides/legends/continuous/common.js +1 -1
  72. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  73. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  74. package/src/actions/guides/legends/edit.js +1 -1
  75. package/src/actions/guides/legends/focused.js +2 -2
  76. package/src/actions/guides/legends/group.js +1 -1
  77. package/src/actions/guides/legends/lane.js +1 -1
  78. package/src/actions/guides/legends/lifecycle.js +2 -2
  79. package/src/actions/guides/legends/remove.js +18 -4
  80. package/src/actions/guides/legends/size.js +1 -1
  81. package/src/actions/guides/legends/transition.js +15 -6
  82. package/src/actions/guides/polar/axes/facade.js +4 -2
  83. package/src/actions/guides/polar/axes/labels.js +6 -1
  84. package/src/actions/guides/polar/axes/shared.js +4 -4
  85. package/src/actions/guides/polar/axes/titles.js +13 -2
  86. package/src/actions/guides/polar/grids.js +6 -3
  87. package/src/actions/guides/tickValues.js +7 -1
  88. package/src/actions/index.js +2 -0
  89. package/src/actions/marks/area/actions.js +9 -2
  90. package/src/actions/marks/area/materialize.js +1 -1
  91. package/src/actions/marks/bar/materialize.js +4 -4
  92. package/src/actions/marks/lifecycle.js +14 -0
  93. package/src/actions/marks/line/actions.js +3 -0
  94. package/src/actions/marks/line/materialize.js +4 -2
  95. package/src/actions/marks/point/edit.js +2 -0
  96. package/src/actions/marks/point/jitter.js +2 -0
  97. package/src/actions/marks/point/materialize.js +5 -4
  98. package/src/actions/marks/point/packing.js +2 -0
  99. package/src/actions/marks/references.js +5 -3
  100. package/src/actions/marks/remove.js +16 -0
  101. package/src/actions/marks/shared.js +2 -1
  102. package/src/actions/marks/text/actions.js +5 -2
  103. package/src/actions/overlays/index.js +168 -0
  104. package/src/actions/overlays/snapshot.js +45 -0
  105. package/src/actions/primitives/editGraphics.js +21 -27
  106. package/src/actions/primitives/semanticAction.js +4 -5
  107. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  108. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  109. package/src/actions/scales/consumers/common.js +10 -0
  110. package/src/actions/scales/consumers/families.js +1 -1
  111. package/src/actions/scales/preview.js +1 -1
  112. package/src/actions/selection/actions.js +63 -11
  113. package/src/actions/titles/actions.js +3 -14
  114. package/src/actions/titles/resolve.js +23 -10
  115. package/src/core/action.js +41 -25
  116. package/src/core/compositionScales.js +12 -0
  117. package/src/core/compositionState.js +20 -3
  118. package/src/core/facetGap.js +44 -0
  119. package/src/core/immutable.js +11 -6
  120. package/src/core/programState.js +16 -0
  121. package/src/core/resourceReferences.js +19 -3
  122. package/src/core/validation.js +7 -0
  123. package/src/core/vocabulary.js +8 -2
  124. package/src/grammar/actionTargets.js +24 -0
  125. package/src/grammar/axisTextPlacement.js +1 -1
  126. package/src/grammar/bin2d.js +41 -4
  127. package/src/grammar/colorSizeLegend.js +56 -5
  128. package/src/grammar/displayLabels.js +10 -0
  129. package/src/grammar/facetHeaders.js +1 -1
  130. package/src/grammar/facets/dependencies.js +10 -3
  131. package/src/grammar/facets/guides.js +2 -1
  132. package/src/grammar/facets/legendIntent.js +7 -5
  133. package/src/grammar/legendBlocks.js +1 -1
  134. package/src/grammar/legendCombinations.js +1 -1
  135. package/src/grammar/legendGroups.js +1 -1
  136. package/src/grammar/legendItemsOffset.js +12 -0
  137. package/src/grammar/legendOverflow.js +25 -0
  138. package/src/grammar/lineSeries.js +1 -1
  139. package/src/grammar/markSelection.js +23 -0
  140. package/src/grammar/overlay.js +51 -0
  141. package/src/grammar/parallelCoordinates.js +1 -208
  142. package/src/grammar/pathCommands.js +8 -2
  143. package/src/grammar/pathSeries.js +1 -1
  144. package/src/grammar/plotComposition.js +152 -0
  145. package/src/grammar/pointShapes.js +18 -0
  146. package/src/grammar/scales/transformed.js +1 -1
  147. package/src/grammar/schemas/concreteGraphic.js +8 -1
  148. package/src/grammar/schemas/graphicBounds.js +8 -1
  149. package/src/grammar/schemas/graphicTree.js +18 -2
  150. package/src/grammar/schemas/semanticPath.js +3 -0
  151. package/src/grammar/strokeStyle.js +14 -8
  152. package/src/grammar/text.js +72 -0
  153. package/src/inspection.js +1 -1
  154. package/src/inspectionDescriptors.js +1 -1
  155. package/src/layout/composition.js +32 -10
  156. package/src/layout/compositionLegend.js +91 -0
  157. package/src/layout/facetContentFlow.js +15 -7
  158. package/src/layout/facetHeaderTitles.js +1 -1
  159. package/src/layout/facets.js +1 -1
  160. package/src/layout/labels.js +55 -27
  161. package/src/layout/legend.js +3 -2
  162. package/src/layout/legendGroup.js +1 -1
  163. package/src/layout/legendItems.js +7 -6
  164. package/src/layout/legendLane.js +1 -1
  165. package/src/layout/legendText.js +1 -1
  166. package/src/layout/title.js +1 -1
  167. package/src/materialization/adjacentSource.js +27 -0
  168. package/src/materialization/composition.js +165 -52
  169. package/src/materialization/compositionLegends.js +120 -0
  170. package/src/materialization/compositionScales.js +125 -0
  171. package/src/materialization/compositionTitle.js +96 -0
  172. package/src/materialization/facetGuides/edit.js +22 -2
  173. package/src/materialization/facetGuides/layout.js +1 -1
  174. package/src/materialization/facetGuides/owner.js +4 -3
  175. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  176. package/src/materialization/facetGuides/placement.js +19 -22
  177. package/src/materialization/facetGuides/preparation.js +17 -5
  178. package/src/materialization/facetHeaders.js +5 -1
  179. package/src/materialization/facets.js +1 -1
  180. package/src/materialization/guides/layout.js +1 -1
  181. package/src/materialization/guides/resources.js +1 -1
  182. package/src/materialization/guides/visibility.js +105 -0
  183. package/src/materialization/legendClip.js +1 -1
  184. package/src/materialization/legendGroups.js +1 -1
  185. package/src/materialization/legendItemsOffset.js +68 -0
  186. package/src/materialization/legendSuppression.js +22 -0
  187. package/src/materialization/legends.js +3 -2
  188. package/src/materialization/marks/capabilities.js +4 -0
  189. package/src/materialization/marks/index.js +5 -1
  190. package/src/materialization/marks/pathOrder.js +1 -1
  191. package/src/materialization/marks/policies.js +2 -1
  192. package/src/materialization/overlayItems.js +154 -0
  193. package/src/materialization/planner.js +19 -4
  194. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  195. package/src/materialization/scales/policies/series.js +12 -4
  196. package/src/materialization/scales/resolve.js +2 -0
  197. package/src/materialization/selection/items/bar.js +21 -3
  198. package/src/materialization/selection/items/common.js +18 -5
  199. package/src/materialization/selection/items/overlay.js +15 -0
  200. package/src/materialization/selection/items/path.js +1 -0
  201. package/src/materialization/selection/items/point.js +2 -3
  202. package/src/materialization/selection/items/tick.js +3 -12
  203. package/src/materialization/selection/path.js +2 -2
  204. package/src/materialization/selection/policies/bar.js +2 -0
  205. package/src/materialization/selection/policies/index.js +35 -1
  206. package/src/materialization/selection/policies/point.js +2 -0
  207. package/src/materialization/selection/policies/text.js +17 -0
  208. package/src/materialization/selection/rows.js +103 -0
  209. package/src/materialization/selection/state.js +19 -3
  210. package/src/materialization/selection/styles.js +8 -0
  211. package/src/materialization/text.js +6 -489
  212. package/src/materialization/textGeometry.js +503 -0
  213. package/src/materialization/textLines.js +20 -0
  214. package/src/persistence/validation.js +116 -25
  215. package/src/renderers/canvas/index.js +12 -7
  216. package/src/renderers/canvas/text.js +5 -5
  217. package/src/renderers/nativeBackend.js +25 -0
  218. package/src/renderers/pdf.js +3 -3
  219. package/src/renderers/png.js +4 -3
  220. package/src/renderers/svgSerialization.js +1 -643
  221. package/src/scene.js +5 -3
  222. package/src/selectors/plotComposition.js +115 -0
  223. package/src/selectors/selectionTargets.js +19 -0
  224. package/src/version.js +1 -1
  225. package/types/basic.d.ts +4 -2
  226. package/types/index.d.ts +14 -0
  227. package/types/inspection.d.ts +34 -0
  228. package/types/program.d.ts +572 -372
@@ -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,19 +21,18 @@ 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";
33
- import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
34
+ import { validateAxisLabelFlushThreshold, validateAxisTextPlacement,
35
+ axisLabelHasText } from "../grammar/axisTextPlacement.js";
34
36
  import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
35
37
  import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
36
38
  import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
@@ -49,7 +51,8 @@ import { validateUserId } from "../core/identifiers.js";
49
51
  import { collectAllResourceReferences } from "../core/resourceReferences.js";
50
52
  import { requiredTraceExtension } from "../core/extensionRegistry.js";
51
53
  import { resolveDatasetReference } from "../selectors/datasets.js";
52
- import { normalizeMarkSelector } from "../grammar/markSelection.js";
54
+ import { normalizeMarkSelector, validateRowSelectionDataset } from "../grammar/markSelection.js";
55
+ import { highlightTargets } from "../selectors/selectionTargets.js";
53
56
  import { parseSemanticPath } from "../grammar/schemas/semanticPath.js";
54
57
  import { validateStoredSemanticValue } from "../actions/primitives/semantic.js";
55
58
  import { validateGraphicType, isGraphicContainerType } from "../grammar/schemas/graphic.js";
@@ -226,6 +229,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
226
229
  throw new TypeError("Facet row field title requires a row-column facet grid.");
227
230
  }
228
231
  }
232
+ validateGuideVisibilityState(program);
229
233
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
230
234
  for (const config of [
231
235
  ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
@@ -236,6 +240,23 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
236
240
  }
237
241
  const cartesianOwners = cartesianAxisOwners(program);
238
242
  for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
243
+ if (isPlainObject(axis?.title?.offset)) {
244
+ if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Relative title offset requires a Cartesian axis owner.", path);
245
+ validateAxisTextPlacement(axis.title, { title: true });
246
+ if (axis.title.inferredOffset === true) throw invalidSnapshot("Relative title offset cannot be inferred.", path);
247
+ const owner = cartesianOwners.find(owner => owner.id === id);
248
+ const labels = program.graphicSpec.objects[axisGraphicIds(owner.channel, id)[2]];
249
+ // Composition parents retain source intent, not local unit graphics.
250
+ // Their retained unit/children are validated through the same unit rule.
251
+ if (axis.labels?.position !== axis.title.position || (program.compositionSpec === undefined &&
252
+ !(labels?.items ?? (labels ? [labels] : [])).some(item => axisLabelHasText(item.properties)))) {
253
+ throw invalidSnapshot("Relative title offset requires nonempty same-side labels.", path);
254
+ }
255
+ }
256
+ if (axis?.labels?.flushThreshold !== undefined) {
257
+ if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Label flushThreshold requires a Cartesian axis owner.", path);
258
+ validateAxisLabelFlushThreshold(axis.labels.flushThreshold);
259
+ }
239
260
  for (const config of [axis?.ticks, axis?.labels]) {
240
261
  if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
241
262
  if (!cartesianOwners.some(owner => owner.id === id)) {
@@ -301,7 +322,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
301
322
  }
302
323
  const config = program.guideConfigs.axis?.[id];
303
324
  requireObject(config, `${path}.guides.axis.${id}`);
304
- 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");
305
326
  const components = ["line", "ticks", "labels", "title"];
306
327
  if (!components.some(component => config[component] !== undefined)) {
307
328
  throw invalidSnapshot("Named axes require at least one retained component.", path);
@@ -322,30 +343,50 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
322
343
  coordinate: semanticAxis.coordinate, position: semanticAxis.position });
323
344
  }
324
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
+ }
325
357
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
358
+ validateLegendOverflow(kind, config);
326
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
+ }
327
367
  validateLegendSpacing(kind,config);
328
368
  if (localLegendPosition(config) !== undefined) {
329
369
  const descriptor = describeLegendBlock(program, kind, config);
330
370
  for (const [key, override] of Object.entries(config.blockOverrides)) {
331
- 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);
332
372
  }
333
373
  }
334
374
  for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
335
375
  if(kind==='colorSize') {
336
- 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);
337
377
  const layer=findLayer(program,config.target);
338
- const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
339
- 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);
340
380
  const guide=semantic.guides.legend?.colorSize;
341
- if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
342
- requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
343
- 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);
344
384
  const sample=config.sampling;requireObject(sample,path);
345
385
  exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
346
386
  if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
347
387
  validateGeneratedItemLimit(sample.count,'Color-size samples');
348
- if(sample.mode==='values'){
388
+ if(config.combine==='color-size-shape')validateColorSizeShapeSamples(sample,program.resolvedScales[mapping.colorScale]);
389
+ else if(sample.mode==='values'){
349
390
  requireArray(sample.values,path);
350
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);
351
392
  }
@@ -367,15 +408,6 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
367
408
  if (config.border && Object.hasOwn(config.border, "strokeDash")) {
368
409
  normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
369
410
  }
370
- if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
371
- for (const [key, override] of Object.entries(config.blockOverrides)) {
372
- if (override.labelMap === undefined) continue;
373
- if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
374
- throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
375
- }
376
- normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
377
- }
378
- }
379
411
  if (config.titleStyle !== undefined) {
380
412
  validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
381
413
  validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
@@ -440,13 +472,25 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
440
472
  }
441
473
  }
442
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
+ }
443
486
  const selections = program.materializationConfigs.selections ?? {};
444
487
  requireObject(selections, `${path}.materializationConfigs.selections`);
445
488
  for (const [id, selection] of Object.entries(selections)) {
446
489
  validateUserId(id, "Selection id");
447
- exactKeys(selection, ["target", "selector"], `${path}.materializationConfigs.selections.${id}`);
490
+ exactKeys(selection, ["target", "selector", ...(Object.hasOwn(selection, "sourceSelection") ? ["sourceSelection"] : [])], `${path}.materializationConfigs.selections.${id}`);
448
491
  validateUserId(selection.target, "Selection target");
449
492
  normalizeMarkSelector(selection.selector);
493
+ if (Object.hasOwn(selection, "sourceSelection")) validateUserId(selection.sourceSelection, "Composite selection owner");
450
494
  }
451
495
  resources.set("selection", new Map(Object.entries(selections)));
452
496
  for (const reference of collectAllResourceReferences(program)) {
@@ -455,6 +499,24 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
455
499
  throw invalidSnapshot(`Unknown ${reference.kind} "${reference.id}" referenced by ${reference.ownerKind} "${reference.ownerId}".`, path);
456
500
  }
457
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
+ }
458
520
  for (const [id, scale] of Object.entries(program.resolvedScales)) {
459
521
  requireObject(scale, `${path}.resolvedScales.${id}`);
460
522
  requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
@@ -468,6 +530,10 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
468
530
  }
469
531
  }
470
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
+ }
471
537
  validateRegressionPlacement(program.markConfigs[id].regression?.placement);
472
538
  requestedMarkClip(program.markConfigs[id]);
473
539
  validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
@@ -499,3 +565,28 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
499
565
  }));
500
566
  }
501
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({