ggaction 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -0,0 +1,152 @@
1
+ // One constructor capability registry for layering, adjacent inference and
2
+ // inspection. Native constructors remain responsible for statistical work.
3
+ const xy = ['createScatterPlot','createLinePlot','createBarPlot','createAreaPlot','createStripPlot','createBeeswarmPlot','createRegressionPlot','createTextPlot'];
4
+ const summaryXY = ['createBoxPlot','createViolinPlot','createGradientPlot','createIntervalPlot'];
5
+ export const PLOT_COMPOSITION_ROLES = Object.freeze(Object.fromEntries([
6
+ ...xy.map(a => [a, 'xy']), ...summaryXY.map(a => [a, 'summaryXY']),
7
+ ...['createDotPlot','createLollipopPlot','createDumbbellPlot'].map(a => [a, 'endpoint']),
8
+ ['createRaincloudPlot','categoryValue'], ['createHistogram','histogram'], ['createDensityPlot','density'],
9
+ ['createECDFPlot','ecdf'], ['createRugPlot','rug'], ['createHeatmap','heatmap'], ['createHorizonPlot','horizon'],
10
+ ...['createPolarScatterPlot','createPolarLinePlot'].map(a => [a, 'polar']),
11
+ ...['createPiePlot','createRosePlot','createRadialBarPlot'].map(a => [a, 'radial']),
12
+ ['createRadarPlot','radar'], ['createParallelCoordinates','parallel']
13
+ ]));
14
+ export const COMPOSITION_PLOT_ACTIONS = Object.freeze(Object.keys(PLOT_COMPOSITION_ROLES));
15
+ export const fieldEncoding = value => typeof value === 'string' ? { field:value } : value;
16
+ const categorical = c => c && ['nominal','ordinal'].includes(c.fieldType);
17
+ const encoding = (c, aggregate = true) => c?.field ? { field:c.field, fieldType:c.fieldType,
18
+ ...(aggregate && c.measurement.operations.length === 1 && ['mean','median','sum','min','max','count'].includes(c.measurement.operations[0]) ? { aggregate:c.measurement.operations[0] } : {}),
19
+ ...(c.bin ? { bin:c.bin } : {}), ...(c.temporalUnit ? { temporalUnit:c.temporalUnit } : {}) } : undefined;
20
+ const plain = c => c?.field ? { field:c.field, fieldType:c.fieldType } : undefined;
21
+ const merge = (base, supplied) => {
22
+ const value=typeof supplied==='string'?{field:supplied}:supplied;
23
+ // An explicit new field owns its type/grain. Source defaults only fill an
24
+ // omitted role or an options-only patch on that inferred field.
25
+ if (value?.field !== undefined && value.field !== base?.field || value?.lower !== undefined || value?.upper !== undefined) return value;
26
+ const merged={ ...base, ...value };
27
+ if (value?.center !== undefined || value?.extent !== undefined) {
28
+ // Native interval measurements are not ordinary field encodings.
29
+ for (const key of ['fieldType','temporalUnit','aggregate','bin']) delete merged[key];
30
+ }
31
+ return merged;
32
+ };
33
+
34
+ export function inferCompositionOptions(source, action, supplied = {}, inherit = {}) {
35
+ if (!inherit || typeof inherit !== 'object' || Array.isArray(inherit) ||
36
+ Object.keys(inherit).some(key => key !== 'color') ||
37
+ inherit.color !== undefined && typeof inherit.color !== 'boolean') {
38
+ throw new TypeError('Plot inheritance accepts color: true or false.');
39
+ }
40
+ const kind = PLOT_COMPOSITION_ROLES[action];
41
+ if (!kind) throw new Error(`Unknown plot constructor "${action}".`);
42
+ const c = source.channels;
43
+ const x = c.x ?? c.theta, y = c.y ?? c.radius;
44
+ const categoryAxis = categorical(x) ? 'x' : categorical(y) ? 'y' : undefined;
45
+ const category = categoryAxis === 'x' ? x : categoryAxis === 'y' ? y : undefined;
46
+ const measure = categoryAxis === 'x' ? y : categoryAxis === 'y' ? x : x;
47
+ let defaults = { data: source.data };
48
+ if (['xy','summaryXY','heatmap','horizon'].includes(kind)) {
49
+ defaults.x = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(x) : plain(x);
50
+ defaults.y = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(y) : plain(y);
51
+ if (kind === 'summaryXY') {
52
+ if (!categoryAxis && !(supplied.x !== undefined && supplied.y !== undefined)) throw new Error(`${action} requires one category axis and one quantitative measure.`);
53
+ if (categoryAxis && action === 'createIntervalPlot') defaults[categoryAxis === 'x' ? 'y' : 'x'] = { field:measure.field, center:'mean', extent:'ci', level:0.95 };
54
+ }
55
+ // Binning follows the new chart's roles, including explicitly supplied
56
+ // categorical/ordinal grids, rather than the source chart's old axes.
57
+ if (kind === 'heatmap' && !['x', 'y'].some(axis => categorical(
58
+ supplied[axis] === undefined ? defaults[axis] : merge(defaults[axis], supplied[axis])
59
+ ))) defaults.bin = { bins:12 };
60
+ if (kind === 'horizon' && c.y?.measurement.parameters) Object.assign(defaults, c.y.measurement.parameters);
61
+ if (action === 'createTextPlot') defaults.text = { field:c.color?.field ?? y?.field ?? x?.field };
62
+ } else if (['endpoint','categoryValue'].includes(kind)) {
63
+ if (!categoryAxis && !(supplied.category !== undefined && (supplied.value !== undefined || supplied.end !== undefined))) throw new Error(`${action} requires one category axis and one quantitative measure.`);
64
+ if (categoryAxis) Object.assign(defaults, {category:plain(category), orientation: categoryAxis === 'x' ? 'vertical' : 'horizontal'});
65
+ if (action === 'createDumbbellPlot') { defaults.end=plain(measure); } else defaults.value=plain(measure);
66
+ if (kind === 'endpoint') defaults.summary=['mean','median','sum','min','max'].includes(measure?.measurement.operations.at(-1))?measure.measurement.operations.at(-1):'mean';
67
+ } else if (['histogram','density','ecdf'].includes(kind)) {
68
+ defaults.field = supplied.field ?? measure?.field;
69
+ if (kind === 'density' && c.x?.measurement.unit === 'density') defaults.densityChannel='x';
70
+ if (kind === 'histogram' && x?.bin) {
71
+ defaults.maxBins=x.bin.maxBins; defaults.binStep=x.bin.step; defaults.binBoundaries=x.bin.boundaries;
72
+ }
73
+ if (inherit.color !== false && kind === 'density' && c.color?.field && categorical(c.color)) defaults.groupBy=c.color.field;
74
+ } else if (kind === 'rug') {
75
+ const axis = supplied.x !== undefined ? 'x' : supplied.y !== undefined ? 'y'
76
+ : supplied.edge === 'left' || supplied.edge === 'right' || categorical(c.x) ? 'y' : 'x';
77
+ defaults[axis] = plain(c[axis]); defaults.edge = axis === 'x' ? 'bottom' : 'left';
78
+ } else if (kind === 'polar') {
79
+ defaults.theta=plain(c.theta); defaults.radius=plain(c.radius);
80
+ if (action === 'createPolarLinePlot' && source.groupBy) defaults.groupBy=source.groupBy;
81
+ } else if (kind === 'radial') {
82
+ defaults.category=plain(c.theta);
83
+ defaults.aggregate=c.radius?.measurement.operations.at(-1) ?? c.theta?.measurement.parameters?.aggregate ?? 'count';
84
+ if(defaults.aggregate==='sum') defaults.value=c.radius?.field ?? c.theta?.measurement.parameters?.weight;
85
+ } else if (kind === 'radar') {
86
+ if (c.theta?.fields) defaults.wide={fields:c.theta.fields};
87
+ else {defaults.category=plain(c.theta); defaults.value=plain(c.radius);}
88
+ if (source.groupBy) defaults.groupBy=source.groupBy;
89
+ } else if (kind === 'parallel') defaults.dimensions=source.dimensions?.map(plain);
90
+ if (inherit.color !== false && c.color?.field && !['heatmap', 'horizon', 'rug'].includes(kind)) {
91
+ const summaryCategoryOnly = ['endpoint','categoryValue','summaryXY'].includes(kind) && action !== 'createIntervalPlot';
92
+ const colorSurvives = !summaryCategoryOnly || c.color.field === category?.field ||
93
+ action === 'createViolinPlot' && supplied.split?.field === c.color.field;
94
+ if (colorSurvives) defaults.color=plain(c.color);
95
+ if (action === 'createIntervalPlot' && categorical(c.color) && c.color.field !== category?.field) defaults.groupBy=c.color.field;
96
+ }
97
+ if (action === 'createAreaPlot') {
98
+ // Explicit native X/Y roles follow the native default value channel. The
99
+ // source orientation only applies when the new roles are inferred.
100
+ const axis = supplied.valueChannel ?? (supplied.x !== undefined && supplied.y !== undefined ? 'y' : categoryAxis === 'y' ? 'x' : 'y'); defaults.valueChannel=axis;
101
+ if(defaults[axis]) defaults[axis]={field:defaults[axis].field};
102
+ }
103
+ if (['createLinePlot','createAreaPlot'].includes(action) && source.groupBy) defaults.groupBy=source.groupBy;
104
+ if (inherit.color !== false && c.color?.field &&
105
+ (kind==='ecdf' || action==='createAreaPlot' && !defaults.groupBy && categorical(c.color))) defaults.groupBy=[c.color.field];
106
+ const result={...defaults,...supplied};
107
+ for(const key of ['x','y','category','value','theta','radius','color','end']) {
108
+ if (supplied[key] !== undefined && defaults[key] !== undefined) result[key]=merge(defaults[key],supplied[key]);
109
+ }
110
+ return Object.fromEntries(Object.entries(result).filter(([,v])=>v!==undefined));
111
+ }
112
+
113
+ // Describe prospective roles without constructing/rendering a candidate chart.
114
+ export function constructorChannels(action, options, source) {
115
+ const kind=PLOT_COMPOSITION_ROLES[action], c=source.channels, result={};
116
+ const add=(channel,value,path,operation) => {
117
+ const e=fieldEncoding(value); if(!e?.field) return;
118
+ const reference=c[channel]?.field===e.field?c[channel]:Object.values(c).find(r=>r.field===e.field);
119
+ result[channel]={ field:e.field,fieldType:e.fieldType??reference?.fieldType??'quantitative',
120
+ operations:operation ? [operation] : e.aggregate ? [e.aggregate] : [],
121
+ ...(e.temporalUnit ? {temporalUnit:e.temporalUnit}:{}), ...(e.bin ? {bin:e.bin}:{}), path };
122
+ };
123
+ if(['xy','summaryXY','heatmap','horizon'].includes(kind)) {
124
+ add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
125
+ if(kind==='horizon' && result.y) result.y.operations=['folded'];
126
+ } else if(['endpoint','categoryValue'].includes(kind)) {
127
+ const axis=options.orientation==='horizontal'?'y':'x', measureAxis=axis==='x'?'y':'x';
128
+ add(axis,options.category,'category.scale');
129
+ add(measureAxis,options.value??options.end,options.value?'value.scale':'end.scale',kind==='endpoint'?options.summary:undefined);
130
+ } else if(kind==='histogram') {
131
+ add('x',options.field,'xScale'); add('y',options.weight??options.field,'yScale',options.weight?'sum':'count');
132
+ } else if(kind==='density') {
133
+ const density=options.densityChannel??'y', value=density==='y'?'x':'y';
134
+ add(value,options.field,'valueScale'); add(density,options.field,'densityScale','density');
135
+ } else if(kind==='ecdf') {
136
+ const value=options.orientation==='horizontal'?'y':'x';
137
+ add(value,options.field,'valueScale'); add(value==='x'?'y':'x',options.field,'probabilityScale','probability');
138
+ } else if(kind==='rug') {
139
+ add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
140
+ } else if(kind==='polar') {
141
+ add('theta',options.theta,'theta.scale'); add('radius',options.radius,'radius.scale');
142
+ } else if(kind==='radial') {
143
+ add('theta',options.category,'category.scale');
144
+ if(action!=='createPiePlot') {
145
+ if(options.aggregate==='count') result.radius={fieldType:'quantitative',operations:['count'],path:'radiusScale'};
146
+ else add('radius',options.value,'radiusScale',options.aggregate);
147
+ } else if(result.theta) result.theta.operations=['partition'];
148
+ } else if(kind==='radar' && !options.wide) {
149
+ add('theta',options.category,'category.scale'); add('radius',options.value,'value.scale');
150
+ }
151
+ return result;
152
+ }
@@ -20,6 +20,24 @@ export const POINT_SHAPES = cloneAndFreeze([
20
20
  "arrow"
21
21
  ]);
22
22
 
23
+ // Recover the anchor from concrete geometry, including jitter, packing and rotation.
24
+ // Path glyphs can be asymmetric, so their bounding-box center is not their anchor.
25
+ export function pointGraphicAnchor(graphic, shape = "circle", angle) {
26
+ const p = graphic?.properties ?? {};
27
+ if (graphic?.type === "circle") return { x: p.x, y: p.y };
28
+ if (graphic?.type === "rect") return { x: p.x + p.width / 2, y: p.y + p.height / 2 };
29
+ const template = createPointShapeGraphic({ shape, angle, x: 0, y: 0, area: 1, fill: "black" });
30
+ const points = p.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
31
+ const origin = template.properties.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
32
+ if (!points?.length || points.length !== origin?.length) throw new Error("Point glyph does not match its shape geometry.");
33
+ const bounds = values => ({ left: Math.min(...values.map(p => p.x)), right: Math.max(...values.map(p => p.x)),
34
+ top: Math.min(...values.map(p => p.y)), bottom: Math.max(...values.map(p => p.y)) });
35
+ const actual = bounds(points), unit = bounds(origin);
36
+ const factor = unit.right !== unit.left ? (actual.right - actual.left) / (unit.right - unit.left)
37
+ : (actual.bottom - actual.top) / (unit.bottom - unit.top);
38
+ return { x: actual.left - factor * unit.left, y: actual.top - factor * unit.top };
39
+ }
40
+
23
41
  const SHAPE_SET = new Set(POINT_SHAPES);
24
42
 
25
43
  export function validatePointShape(shape) {
@@ -1 +1 @@
1
- import{cloneAndFreeze as h,isPlainObject as I}from"../../core/immutable.js";import{MAX_GENERATED_ITEMS as R}from"../../core/validation.js";import{interpolateNumber as D,inverseLerp as b,numericExtent as q}from"../numeric.js";import{niceTicks as $}from"../ticks.js";import{niceLinearDomain as j}from"./continuous.js";import{SCALE_ROLES as L,validateScaleTypeForRole as A}from"./types.js";import{validatePair as _}from"./validation.js";const z=Object.freeze(["log","pow","sqrt","symlog"]);function v(t,e){if(!Number.isFinite(t)||t<=0)throw new RangeError(`${e} must be a positive finite number.`);return t}function p(t,e={}){if(!z.includes(t)&&t!=="linear")throw new Error(`Scale type "${t}" is not a quantitative transform.`);if(!I(e))throw new TypeError("Scale transform options must be a plain object.");const o=t==="log"?["base"]:t==="pow"?["exponent"]:t==="symlog"?["constant"]:[];for(const n of Object.keys(e))if(!o.includes(n))throw new Error(`Scale type "${t}" does not support ${n}.`);if(t==="log"){const n=v(e.base??10,"Log scale base");if(n===1)throw new RangeError("Log scale base must not equal 1.");return h({base:n})}return t==="pow"?h({exponent:v(e.exponent??1,"Power scale exponent")}):t==="sqrt"?h({exponent:.5}):t==="symlog"?h({constant:v(e.constant??1,"Symlog scale constant")}):h({})}function E(t,e,o,n){const r=[e,o,t].map(i=>Math.sign(i)*n(Math.abs(i)));return r[0]!==r[1]&&r.every(Number.isFinite)?b(r[2],r[0],r[1]):void 0}function V(t,e,o){const[n,r]=e,i=E(t,n,r,c=>c**o);if(i!==void 0)return i;const s=Math.max(Math.abs(n),Math.abs(r)),a=E(t,n,r,c=>c===0?0:Math.exp(o*(Math.log(c)-Math.log(s))));if(a!==void 0)return a;if(n!==0&&r!==0&&t!==0&&Math.sign(n)===Math.sign(r)){const c=Math.log(Math.abs(r))-Math.log(Math.abs(n)),f=Math.log(Math.abs(t))-Math.log(Math.abs(n));if(Math.abs(o*c)<1e-8)return f/c;const u=Math.expm1(o*f)/Math.expm1(o*c);if(Number.isFinite(u))return u}return b(t,n,r)}function N(t,e){if(t===0)return 0;const o=Math.abs(t)/e;if(Number.isFinite(o))return Math.sign(t)*Math.log1p(o);const n=Math.log(Math.abs(t))-Math.log(e);return Math.sign(t)*(Math.max(0,n)+Math.log1p(Math.exp(-Math.abs(n))))}function C(t,e,o,n){const[r,i]=e;if(o==="linear")return b(t,r,i);if(o==="log"){const f=Math.log10(n.base);return E(t,r,i,d=>Math.log10(d)/f)??E(t,r,i,Math.log)??b(t,r,i)}if(o==="pow"||o==="sqrt")return V(t,e,n.exponent);const s=N(r,n.constant),a=N(i,n.constant);if(s===a)return b(t,r,i);const c=b(N(t,n.constant),s,a);return Number.isFinite(c)?c:b(t,r,i)}function x(t,e,o={}){A(t,L.quantitativePosition);const n=_(e,"Transformed scale domain");if(n[0]===n[1])throw new RangeError("Transformed scale domain values must be distinct.");if(t==="log"&&(n.includes(0)||Math.sign(n[0])!==Math.sign(n[1])))throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return p(t,o),n}function P(t,e,o){const n=Number.isFinite(t)?t**e:Number.NaN;if(Number.isFinite(n)&&n>0)return n;const r=e*o;return r>Math.log(Number.MAX_VALUE)?Number.POSITIVE_INFINITY:r<Math.log(Number.MIN_VALUE)?0:Math.exp(r)}function O(t,e){const o=Math.min(...t),n=Math.max(...t),r=n<0;return[e<1?-Math.log(e):Math.log(e),e<1?1/e:e,t[0]>t[1],r,r?Math.abs(n):o,r?Math.abs(o):n]}function U(t,e){const[o,n,r,i,s,a]=O(t,e);let c=P(n,Math.floor(Math.log(s)/o),o),f=P(n,Math.ceil(Math.log(a)/o),o);(!Number.isFinite(c)||c<=0||c>s)&&(c=s),(!Number.isFinite(f)||f<a)&&(f=a);const u=i?[-f,-c]:[c,f];return h(r?u.reverse():u)}function X(t,e,o){const n=p(t,o);if(e===0){if(t==="log")throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return[-1,1]}const r=Math.abs(e);let i=r/2,s=r*2;if(t==="pow"||t==="sqrt")s=r*(2-2**-n.exponent)**(1/n.exponent);else if(t==="symlog"){const a=Math.abs(N(e,n.constant));if(a===0)return[-1,1];s=n.constant*Math.expm1(2*a-N(i,n.constant))}return i<r&&s>r&&Number.isFinite(s)||(i=r/2,s=Number.isFinite(r*2)?r*2:r),t==="log"&&i===0&&(i=r),e>0?[i,s]:[-s,i===0?0:-i]}function W({type:t,domain:e="auto",values:o,nice:n=!1,zero:r=!1,...i}){if(A(t,L.quantitativePosition),!Array.isArray(o)||!o.every(Number.isFinite))throw new TypeError("Transformed scale values must be finite numbers.");if(typeof n!="boolean"||typeof r!="boolean")throw new TypeError("Scale nice and zero must be booleans.");if(t==="log"&&r)throw new Error('Scale type "log" does not support zero.');if(e!=="auto")return x(t,e,i);if(o.length===0)throw new Error("Cannot infer an automatic scale domain from no values.");let a=q(o);return r&&(a=[Math.min(0,a[0]),Math.max(0,a[1])]),a[0]===a[1]&&(a=X(t,a[0],i)),x(t,a,i),n&&(a=t==="log"?U(a,p(t,i).base):j(a)),x(t,a,i)}function Y(t,{reverse:e=!1}={}){const o=_(t,"Scale range");if(typeof e!="boolean")throw new TypeError("Scale reverse must be a boolean.");return e?h([...o].reverse()):o}function Z(t,e,o,{type:n="linear",clamp:r=!1,reverse:i=!1,...s}={}){if(!Array.isArray(t))throw new TypeError("Scale values must be an array.");if(typeof r!="boolean")throw new TypeError("Scale clamp must be a boolean.");const a=Object.hasOwn(s,"unknown"),{unknown:c,...f}=s,u=p(n,f),d=x(n,e,f),l=Y(o,{reverse:i}),w=l[1]-l[0];return h(t.map(g=>{if(!(Number.isFinite(g)&&!(n==="log"&&(g===0||Math.sign(g)!==Math.sign(d[0]))))){if(a)return c;throw new TypeError(`Scale type "${n}" received an invalid value.`)}let m=C(g,d,n,u);r&&(m=Math.max(0,Math.min(1,m)));const M=l[0]+m*w;return Number.isFinite(w)&&Number.isFinite(M)&&(m!==1||Math.abs(M-l[1])<=Number.EPSILON*Math.abs(l[1]))?M:D(l[0],l[1],m)}))}function tt(t,e,o,n={}){const r=x(t,e,n);if(!Number.isInteger(o)||o<=0)throw new RangeError("Tick count must be a positive integer.");if(t!=="log")return $(r,o);const{base:i}=p(t,n),[s,a,c,f,u,d]=O(r,i),l=Math.ceil(Math.log(u)/s-1e-12),w=Math.floor(Math.log(d)/s+1e-12),g=w-l+1,T=g<=R?g:Math.min(o,R),m=[];for(let M=0;M<T;M+=1){const k=g<=R?l+M:Math.round(l+(w-l)*M/Math.max(1,T-1)),S=P(a,k,s),F=(f?-1:1)*S;Number.isFinite(F)&&S>=u&&S<=d&&(m.length===0||F!==m.at(-1))&&m.push(F)}return f&&m.reverse(),c&&m.reverse(),h(m.length===0?[...r]:m)}function nt(t,e){if(t!=="log")return String(e);const o=Math.abs(e),r=[[1e9,"B"],[1e6,"M"],[1e3,"K"]].find(([s])=>o>=s);if(r===void 0)return String(e);const i=e/r[0];return`${Number(i.toPrecision(3))}${r[1]}`}export{nt as formatTransformedTick,Z as mapTransformedValues,p as normalizeTransformParameters,Y as resolveMappingRange,W as resolveTransformedDomain,tt as transformedTicks,x as validateTransformedDomain};
1
+ import{cloneAndFreeze as M,isPlainObject as D}from"../../core/immutable.js";import{MAX_GENERATED_ITEMS as T}from"../../core/validation.js";import{interpolateNumber as R,inverseLerp as w,numericExtent as q}from"../numeric.js";import{niceTicks as $}from"../ticks.js";import{niceLinearDomain as j}from"./continuous.js";import{SCALE_ROLES as L,validateScaleTypeForRole as _}from"./types.js";import{validatePair as I}from"./validation.js";const y=Object.freeze(["log","pow","sqrt","symlog"]);function P(t,e){if(!Number.isFinite(t)||t<=0)throw new RangeError(`${e} must be a positive finite number.`);return t}function p(t,e={}){if(!y.includes(t)&&t!=="linear")throw new Error(`Scale type "${t}" is not a quantitative transform.`);if(!D(e))throw new TypeError("Scale transform options must be a plain object.");const o=t==="log"?["base"]:t==="pow"?["exponent"]:t==="symlog"?["constant"]:[];for(const r of Object.keys(e))if(!o.includes(r))throw new Error(`Scale type "${t}" does not support ${r}.`);if(t==="log"){const r=P(e.base??10,"Log scale base");if(r===1)throw new RangeError("Log scale base must not equal 1.");return M({base:r})}return t==="pow"?M({exponent:P(e.exponent??1,"Power scale exponent")}):t==="sqrt"?M({exponent:.5}):t==="symlog"?M({constant:P(e.constant??1,"Symlog scale constant")}):M({})}function S(t,e,o,r){const n=[e,o,t].map(i=>Math.sign(i)*r(Math.abs(i)));return n[0]!==n[1]&&n.every(Number.isFinite)?w(n[2],n[0],n[1]):void 0}function z(t,e,o){const[r,n]=e,i=S(t,r,n,c=>c**o);if(i!==void 0)return i;const s=Math.max(Math.abs(r),Math.abs(n)),a=S(t,r,n,c=>c===0?0:Math.exp(o*(Math.log(c)-Math.log(s))));if(a!==void 0)return a;if(r!==0&&n!==0&&t!==0&&Math.sign(r)===Math.sign(n)){const c=Math.log(Math.abs(n))-Math.log(Math.abs(r)),m=Math.log(Math.abs(t))-Math.log(Math.abs(r));if(Math.abs(o*c)<1e-8)return m/c;const f=Math.expm1(o*m)/Math.expm1(o*c);if(Number.isFinite(f))return f}return w(t,r,n)}function x(t,e){if(t===0)return 0;const o=Math.abs(t)/e;if(Number.isFinite(o))return Math.sign(t)*Math.log1p(o);const r=Math.log(Math.abs(t))-Math.log(e);return Math.sign(t)*(Math.max(0,r)+Math.log1p(Math.exp(-Math.abs(r))))}function V(t,e,o,r){const[n,i]=e;if(o==="linear")return w(t,n,i);if(o==="log"){const m=Math.log10(r.base);return S(t,n,i,g=>Math.log10(g)/m)??S(t,n,i,Math.log)??w(t,n,i)}if(o==="pow"||o==="sqrt")return z(t,e,r.exponent);const s=x(n,r.constant),a=x(i,r.constant);if(s===a)return w(t,n,i);const c=w(x(t,r.constant),s,a);return Number.isFinite(c)?c:w(t,n,i)}function N(t,e,o={}){_(t,L.quantitativePosition);const r=I(e,"Transformed scale domain");if(r[0]===r[1])throw new RangeError("Transformed scale domain values must be distinct.");if(t==="log"&&(r.includes(0)||Math.sign(r[0])!==Math.sign(r[1])))throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return p(t,o),r}function A(t,e,o){const r=Number.isFinite(t)?t**e:Number.NaN;if(Number.isFinite(r)&&r>0)return r;const n=e*o;return n>Math.log(Number.MAX_VALUE)?Number.POSITIVE_INFINITY:n<Math.log(Number.MIN_VALUE)?0:Math.exp(n)}function O(t,e){const o=Math.min(...t),r=Math.max(...t),n=r<0;return[e<1?-Math.log(e):Math.log(e),e<1?1/e:e,t[0]>t[1],n,n?Math.abs(r):o,n?Math.abs(o):r]}function C(t,e){const[o,r,n,i,s,a]=O(t,e);let c=A(r,Math.floor(Math.log(s)/o),o),m=A(r,Math.ceil(Math.log(a)/o),o);(!Number.isFinite(c)||c<=0||c>s)&&(c=s),(!Number.isFinite(m)||m<a)&&(m=a);const f=i?[-m,-c]:[c,m];return M(n?f.reverse():f)}function U(t,e,o){const r=p(t,o);if(e===0){if(t==="log")throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return[-1,1]}const n=Math.abs(e);let i=n/2,s=n*2;if(t==="pow"||t==="sqrt")s=n*(2-2**-r.exponent)**(1/r.exponent);else if(t==="symlog"){const a=Math.abs(x(e,r.constant));if(a===0)return[-1,1];s=r.constant*Math.expm1(2*a-x(i,r.constant))}return i<n&&s>n&&Number.isFinite(s)||(i=n/2,s=Number.isFinite(n*2)?n*2:n),t==="log"&&i===0&&(i=n),e>0?[i,s]:[-s,i===0?0:-i]}function W({type:t,domain:e="auto",values:o,nice:r=!1,zero:n=!1,...i}){if(_(t,L.quantitativePosition),!Array.isArray(o)||!o.every(Number.isFinite))throw new TypeError("Transformed scale values must be finite numbers.");if(typeof r!="boolean"||typeof n!="boolean")throw new TypeError("Scale nice and zero must be booleans.");if(t==="log"&&n)throw new Error('Scale type "log" does not support zero.');if(e!=="auto")return N(t,e,i);if(o.length===0)throw new Error("Cannot infer an automatic scale domain from no values.");let a=q(o);return n&&(a=[Math.min(0,a[0]),Math.max(0,a[1])]),a[0]===a[1]&&(a=U(t,a[0],i)),N(t,a,i),r&&(a=t==="log"?C(a,p(t,i).base):j(a)),N(t,a,i)}function X(t,{reverse:e=!1}={}){const o=I(t,"Scale range");if(typeof e!="boolean")throw new TypeError("Scale reverse must be a boolean.");return e?M([...o].reverse()):o}function Z(t,e,o,{type:r="linear",clamp:n=!1,reverse:i=!1,...s}={}){if(!Array.isArray(t))throw new TypeError("Scale values must be an array.");if(typeof n!="boolean")throw new TypeError("Scale clamp must be a boolean.");const a=Object.hasOwn(s,"unknown"),{unknown:c,...m}=s,f=p(r,m),g=N(r,e,m),l=X(o,{reverse:i}),d=l[1]-l[0];return M(t.map(h=>{if(!(Number.isFinite(h)&&!(r==="log"&&(h===0||Math.sign(h)!==Math.sign(g[0]))))){if(a)return c;throw new TypeError(`Scale type "${r}" received an invalid value.`)}let u=V(h,g,r,f);n&&(u=Math.max(0,Math.min(1,u)));const b=l[0]+u*d;return Number.isFinite(d)&&Number.isFinite(b)&&(u!==1||Math.abs(b-l[1])<=Number.EPSILON*Math.abs(l[1]))?b:R(l[0],l[1],u)}))}function tt(t,e,o,r={}){const n=N(t,e,r);if(!Number.isInteger(o)||o<=0)throw new RangeError("Tick count must be a positive integer.");if(t!=="log")return $(n,o);const{base:i}=p(t,r),[s,a,c,m,f,g]=O(n,i),l=Math.ceil(Math.log(f)/s-1e-12),d=Math.floor(Math.log(g)/s+1e-12),h=d-l+1,E=h<=T?h:Math.min(o,T),u=[];for(let b=0;b<E;b+=1){const k=h<=T?l+b:Math.round(l+(d-l)*b/Math.max(1,E-1)),F=A(a,k,s),v=(m?-1:1)*F;Number.isFinite(v)&&F>=f&&F<=g&&(u.length===0||v!==u.at(-1))&&u.push(v)}return m&&u.reverse(),c&&u.reverse(),M(u.length===0?[...n]:u)}function rt(t,e,o={}){const r=N("symlog",t,o);if(!Number.isInteger(e)||e<2||e>T)throw new RangeError(`Sample count must be an integer from 2 to ${T}.`);const{constant:n}=p("symlog",o),i=r.map(m=>x(m,n)),s=Math.min(...r),a=Math.max(...r),c=Array.from({length:e},(m,f)=>{if(f===0)return r[0];if(f===e-1)return r[1];const g=f/(e-1);if(i[0]===i[1])return R(...r,g);const l=R(...i,g),d=Math.abs(l),h=n*Math.expm1(d),E=Math.sign(l)*(Number.isFinite(h)?h:Math.exp(Math.log(n)+d+Math.log1p(-Math.exp(-d))));return Math.max(s,Math.min(a,E))});if(s<0&&a>0&&e>2){const m=Math.round(w(0,...i)*(e-1));m>0&&m<e-1&&(c[m]=0)}return M(c.filter((m,f)=>f===0||m!==c[f-1]))}function nt(t,e){if(t!=="log")return String(e);const o=Math.abs(e),n=[[1e9,"B"],[1e6,"M"],[1e3,"K"]].find(([s])=>o>=s);if(n===void 0)return String(e);const i=e/n[0];return`${Number(i.toPrecision(3))}${n[1]}`}export{nt as formatTransformedTick,Z as mapTransformedValues,p as normalizeTransformParameters,X as resolveMappingRange,W as resolveTransformedDomain,rt as sampleSymlogValues,tt as transformedTicks,N as validateTransformedDomain};
@@ -1,6 +1,6 @@
1
1
  import { validateGeneratedItemLimit } from "../../core/validation.js";
2
2
  import { validateFontStyle } from "../../core/font.js";
3
- import { isPlainObject } from "../../core/immutable.js";
3
+ import { isOwned, isPlainObject } from "../../core/immutable.js";
4
4
  import { validatePathCommands } from "../pathCommands.js";
5
5
  import { validateFillPaint } from "../paint.js";
6
6
  import {
@@ -25,6 +25,7 @@ const TEXT_ALIGNS = new Set(["left", "right", "center", "start", "end"]);
25
25
  const TEXT_BASELINES = new Set([
26
26
  "top", "hanging", "middle", "alphabetic", "ideographic", "bottom"
27
27
  ]);
28
+ const validatedProperties = new WeakMap();
28
29
 
29
30
  export function validateConcreteGraphicValue(type, property, value) {
30
31
  if (property === "lines") {
@@ -98,6 +99,8 @@ export function validateConcreteGraphicValue(type, property, value) {
98
99
  }
99
100
 
100
101
  export function validateConcreteGraphicProperties(type, properties) {
102
+ const validatedTypes = validatedProperties.get(properties);
103
+ if (validatedTypes?.has(type)) return properties;
101
104
  if (!isPlainObject(properties)) {
102
105
  throw new TypeError(`${type}.properties must be a plain object.`);
103
106
  }
@@ -105,5 +108,9 @@ export function validateConcreteGraphicProperties(type, properties) {
105
108
  validateGraphicProperty(type, property);
106
109
  validateConcreteGraphicValue(type, property, value);
107
110
  }
111
+ if (isOwned(properties)) {
112
+ if (validatedTypes) validatedTypes.add(type);
113
+ else validatedProperties.set(properties, new Set([type]));
114
+ }
108
115
  return properties;
109
116
  }
@@ -3,6 +3,7 @@ import {
3
3
  resolveTextBounds
4
4
  } from "../../core/textMetrics.js";
5
5
  import { interpolateNumber } from "../numeric.js";
6
+ import { isOwned } from "../../core/immutable.js";
6
7
  import { graphicClipBounds, intersectClipBounds } from "./graphicClip.js";
7
8
  import { resolveStrokeDetails } from "../strokeStyle.js";
8
9
  import {
@@ -12,6 +13,7 @@ import {
12
13
  } from "./graphicTree.js";
13
14
 
14
15
  const EPSILON = Number.EPSILON * 64;
16
+ const pathBounds = new WeakMap();
15
17
 
16
18
  function box(left, right, top, bottom, extent = 0) {
17
19
  return {
@@ -111,6 +113,10 @@ function cubicExtrema(start, control1, control2, end) {
111
113
  }
112
114
 
113
115
  export function resolvePathCommandBounds(commands) {
116
+ if (pathBounds.has(commands)) {
117
+ const cached = pathBounds.get(commands);
118
+ return cached === undefined ? undefined : { ...cached };
119
+ }
114
120
  let bounds;
115
121
  let current;
116
122
  for (const command of commands) {
@@ -145,7 +151,8 @@ export function resolvePathCommandBounds(commands) {
145
151
  }
146
152
  current = { x: command.x, y: command.y };
147
153
  }
148
- return bounds;
154
+ if (isOwned(commands)) pathBounds.set(commands, bounds);
155
+ return bounds === undefined ? undefined : { ...bounds };
149
156
  }
150
157
 
151
158
  function endpointDirection(start, command, end, reverse = false) {
@@ -1,4 +1,4 @@
1
- import { isPlainObject } from "../../core/immutable.js";
1
+ import { isOwned, isPlainObject } from "../../core/immutable.js";
2
2
  import { isGraphicContainerType } from "./graphic.js";
3
3
 
4
4
  export function resolveGraphicChildOrder(graphicSpec, target, value) {
@@ -34,7 +34,7 @@ export function findGraphic(graphicSpec, target) {
34
34
  return { id: target, object, kind: "object" };
35
35
  }
36
36
  for (const [ownerId, owner] of Object.entries(graphicSpec.objects)) {
37
- const itemIndex = owner.items?.findIndex(item => item.id === target);
37
+ const itemIndex = findItemIndex(owner.items, target);
38
38
  if (itemIndex !== undefined && itemIndex !== -1) {
39
39
  return {
40
40
  id: target,
@@ -49,6 +49,22 @@ export function findGraphic(graphicSpec, target) {
49
49
  return undefined;
50
50
  }
51
51
 
52
+ // Collections are immutable across unrelated edits. Index their items once,
53
+ // instead of scanning every point/path again for each label or inspection.
54
+ // Caller-owned arrays remain uncached, including shallow-frozen inputs.
55
+ const itemIndexes = new WeakMap();
56
+ function findItemIndex(items, target) {
57
+ if (!items) return undefined;
58
+ if (!isOwned(items)) return items.findIndex(item => item.id === target);
59
+ let index = itemIndexes.get(items);
60
+ if (!index) {
61
+ index = new Map();
62
+ items.forEach((item, i) => { if (!index.has(item.id)) index.set(item.id, i); });
63
+ itemIndexes.set(items, index);
64
+ }
65
+ return index.get(target) ?? -1;
66
+ }
67
+
52
68
  export function requireGraphic(graphicSpec, target) {
53
69
  const found = findGraphic(graphicSpec, target);
54
70
  if (found === undefined) {
@@ -68,6 +68,7 @@ const ENTITY_PATHS = Object.freeze({
68
68
  "mark.orientation",
69
69
  "mark.blend",
70
70
  "derivedBindings.regression",
71
+ "derivedBindings.overlay",
71
72
  "layout.mode",
72
73
  ...ENCODING_PATHS
73
74
  ])
@@ -106,6 +107,8 @@ const GUIDE_PATHS = new Set([
106
107
  "legend.colorSize.colorScale",
107
108
  "legend.colorSize.sizeScale",
108
109
  "legend.colorSize.title",
110
+ "legend.colorSize.shapeScale",
111
+ "legend.colorSize.colorChannel",
109
112
  "legend.color.scale",
110
113
  "legend.color.title",
111
114
  "legend.color.order",
@@ -11,6 +11,11 @@ export const STROKE_STYLE_PROPERTIES = Object.freeze([
11
11
  "lineJoin",
12
12
  "miterLimit"
13
13
  ]);
14
+ // Bounds and collision checks resolve this small vocabulary for every mark.
15
+ // Share the nine common immutable results rather than allocating per item;
16
+ // nondefault miter limits remain uncached so arbitrary user values stay bounded.
17
+ const commonStrokeDetails = new Map(LINE_CAPS.flatMap(lineCap => LINE_JOINS.map(lineJoin =>
18
+ [`${lineCap}:${lineJoin}`, freezeOwned({ lineCap, lineJoin, miterLimit: DEFAULT_MITER_LIMIT })])));
14
19
 
15
20
  function validateEnum(value, values, property, label) {
16
21
  if (typeof value !== "string") {
@@ -46,17 +51,18 @@ export function resolveStrokeDetails(request = {}) {
46
51
  if (!isPlainObject(request)) {
47
52
  throw new TypeError("Stroke style details must be a plain object.");
48
53
  }
49
- return freezeOwned({
50
- lineCap: request.lineCap === undefined
54
+ const lineCap = request.lineCap === undefined
51
55
  ? DEFAULT_LINE_CAP
52
- : validateLineCap(request.lineCap),
53
- lineJoin: request.lineJoin === undefined
56
+ : validateLineCap(request.lineCap);
57
+ const lineJoin = request.lineJoin === undefined
54
58
  ? DEFAULT_LINE_JOIN
55
- : validateLineJoin(request.lineJoin),
56
- miterLimit: request.miterLimit === undefined
59
+ : validateLineJoin(request.lineJoin);
60
+ const miterLimit = request.miterLimit === undefined
57
61
  ? DEFAULT_MITER_LIMIT
58
- : validateMiterLimit(request.miterLimit)
59
- });
62
+ : validateMiterLimit(request.miterLimit);
63
+ return miterLimit === DEFAULT_MITER_LIMIT
64
+ ? commonStrokeDetails.get(`${lineCap}:${lineJoin}`)
65
+ : freezeOwned({ lineCap, lineJoin, miterLimit });
60
66
  }
61
67
 
62
68
  export function requestedStrokeDetails(request = {}, label = "Stroke style") {
@@ -118,6 +118,8 @@ export function formatTextValue(value, format = "auto") {
118
118
 
119
119
  export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
120
120
  const config = { ...base };
121
+ if (options.textOverrides === "auto") delete config.textOverrides;
122
+ else if (Object.hasOwn(options, "textOverrides")) config.textOverrides = normalizeTextOverrides(options.textOverrides);
121
123
  if (Object.hasOwn(options, "lineHeight")) {
122
124
  if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
123
125
  config.lineHeight = options.lineHeight;
@@ -158,3 +160,73 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
158
160
  }
159
161
 
160
162
  export { resolveTextLines } from "../core/textMetrics.js";
163
+
164
+
165
+ export const isTextIdentityValue = value => value === null || typeof value === "string" ||
166
+ typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
167
+ export const textIdentity = (fields = {}, channels = {}) => Object.fromEntries(
168
+ Object.entries({ fields, channels }).map(([part, values]) => [part,
169
+ Object.fromEntries(Object.entries(values).filter(([, value]) => isTextIdentityValue(value)))]));
170
+ export const textIdentityKey = match => JSON.stringify(["fields", "channels"].map(part =>
171
+ Object.entries(match[part] ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
172
+
173
+ export function normalizeTextOverrides(value) {
174
+ if (!Array.isArray(value)) throw new TypeError("textOverrides must be an array or auto.");
175
+ const seen = new Set();
176
+ for (const entry of value) {
177
+ if (!isPlainObject(entry) || Object.keys(entry).length !== 3 ||
178
+ !isPlainObject(entry.match) || typeof entry.text !== "string" ||
179
+ !Number.isInteger(entry.occurrence) || entry.occurrence < 0) {
180
+ throw new TypeError("Each text override requires match, occurrence and text.");
181
+ }
182
+ if (!Object.keys(entry.match).length || Object.keys(entry.match).some(part =>
183
+ !["fields", "channels"].includes(part) || !isPlainObject(entry.match[part]) ||
184
+ !Object.keys(entry.match[part]).length || !Object.values(entry.match[part]).every(isTextIdentityValue))) {
185
+ throw new TypeError("Text match requires nonempty fields or channels containing scalar values.");
186
+ }
187
+ const key = `${textIdentityKey(entry.match)}/${entry.occurrence}`;
188
+ if (seen.has(key)) throw new Error("Duplicate text override selector.");
189
+ seen.add(key);
190
+ }
191
+ return cloneAndFreeze(value);
192
+ }
193
+
194
+ // Count semantic matches before visibility/placement filtering. Never address a graphic index.
195
+ export function resolveTextOverrides(items, overrides) {
196
+ const result = new Map();
197
+ if (!overrides?.length) return result;
198
+ const groups = new Map(), priorities = new Map();
199
+ for (const [priority, { match, occurrence, text }] of overrides.entries()) {
200
+ const keys = ["fields", "channels"].flatMap(part => Object.keys(match[part] ?? {}).sort().map(key => [part, key]));
201
+ const shape = JSON.stringify(keys);
202
+ if (!groups.has(shape)) groups.set(shape, { keys, values: new Map() });
203
+ const values = groups.get(shape).values, key = JSON.stringify(keys.map(([part, field]) => match[part][field]));
204
+ if (!values.has(key)) values.set(key, new Map());
205
+ values.get(key).set(occurrence, { text, priority });
206
+ }
207
+ // Complete identities from the editor share one shape: O(items + overrides),
208
+ // rather than scanning all items once for every previously edited label.
209
+ for (const { keys, values } of groups.values()) {
210
+ const counts = new Map();
211
+ for (const item of items) {
212
+ if (!keys.every(([part, field]) => Object.hasOwn(item[part] ?? {}, field) && isTextIdentityValue(item[part][field]))) continue;
213
+ const key = JSON.stringify(keys.map(([part, field]) => item[part][field]));
214
+ const candidates = values.get(key); if (!candidates) continue;
215
+ const occurrence = counts.get(key) ?? 0; counts.set(key, occurrence + 1);
216
+ const chosen = candidates.get(occurrence);
217
+ if (chosen && chosen.priority > (priorities.get(item) ?? -1)) {
218
+ result.set(item, chosen.text); priorities.set(item, chosen.priority);
219
+ }
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+
225
+ export function textIdentityOccurrences(items) {
226
+ const counts = new Map();
227
+ return items.map(item => {
228
+ const identity = textIdentity(item.fields, item.channels), key = textIdentityKey(identity);
229
+ const occurrence = counts.get(key) ?? 0; counts.set(key, occurrence + 1);
230
+ return { identity, occurrence };
231
+ });
232
+ }
package/src/inspection.js CHANGED
@@ -1 +1 @@
1
- import{resolveGraphicClipBounds as B,resolveGraphicParentOffset as W}from"./grammar/schemas/graphicBounds.js";import{resolveFacetDefinition as K,resolveFacetGridDefinition as L}from"./grammar/facets/index.js";import{APPEARANCE_MARK_TYPES as V}from"./core/vocabulary.js";import{ChartProgram as E}from"./ChartProgram.js";import{BasicChartProgram as C}from"./BasicChartProgram.js";import{getWrappedActionMetadata as J}from"./core/action.js";import{cloneAndFreeze as b,isOwned as _,isPlainObject as S}from"./core/immutable.js";import{encodeValue as Y}from"./persistence/codec.js";import{findDataset as z,hasDatasetOwner as Z,resolveDatasetReference as F}from"./selectors/datasets.js";import{findLayer as G}from"./selectors/layers.js";import{findSemanticScale as H}from"./selectors/scales.js";import{findCoordinate as Q}from"./selectors/coordinates.js";import{inferDatasetSchema as X}from"./grammar/datasetSchema.js";import{BUILTIN_ACTION_DESCRIPTOR_RECORDS as ee}from"./inspectionDescriptors.js";const ne=new Set(["data","mark","scale","coordinate","guide"]),ie=new Map(ee.map(([e,n,i,t,c])=>[e,{options:n.map(([m,l,k,d,a])=>({path:m,required:l,kind:k,...d===null?{}:{choices:d},...a===null?{}:{unit:a}})),idOptions:i,prerequisites:t,entryPoints:c}]));function P(e,n="Program"){if(!(e instanceof E||e instanceof C))throw new TypeError(`${n} must be a ggaction program.`);if(e.actionStack.length!==0)throw new Error(`${n} must have a closed action stack.`);return e}function $(e,n=[]){if(!Array.isArray(n))throw new TypeError("childPath must be an array.");let i=e;for(const t of n){if(typeof t!="string"||!Object.hasOwn(i.children,t))throw new Error(`Unknown program child "${String(t)}".`);i=i.children[t]}return i}function R(e){if(!S(e)||!ne.has(e.kind)||typeof e.id!="string"||e.id.length===0)throw new TypeError("Inspection target requires a supported kind and non-empty id.");return e}function I(e,n){const i=$(e,n.childPath);return n.kind==="data"?Z(i,n.id)?F(i,n.id,"Inspection target"):z(i,n.id):n.kind==="mark"?G(i,n.id):n.kind==="scale"?H(i,n.id):n.kind==="coordinate"?Q(i,n.id):i.semanticSpec.guides[n.id]}function g(e,n,i){return{kind:e,id:n,...i?.length?{childPath:[...i]}:{}}}function te(e,{data:n,childPath:i}={}){P(e);const t=$(e,i);if(typeof n!="string"||n.length===0)throw new TypeError("getDatasetSchema requires data.");const c=F(t,n,"Dataset schema data"),m=c.schema??X(c.values??[]);return b({data:c.id,schema:m})}function be(e,{action:n,target:i,options:t={}}={}){if(P(e),typeof n!="string"||n.length===0)throw new TypeError("describeAction requires action.");if(!S(t))throw new TypeError("describeAction options must be a plain object.");let c;if(i!==void 0){if(R(i),c=I(e,i),c===void 0)return b({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:[],findings:[]}],findings:[{severity:"error",code:"missing-resource",reason:"target-unavailable",message:`Unknown ${i.kind} "${i.id}".`,resource:g(i.kind,i.id,i.childPath)}]});if(t.target!==void 0&&t.target!==i.id)return b({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:["target"],findings:[]}],findings:[{severity:"error",code:"incompatible-resource",reason:"target-mismatch",message:`Inspection target "${i.id}" conflicts with options.target "${t.target}".`,resource:g(i.kind,i.id,i.childPath),optionPath:"target"}]});const f=V[n];if(f&&(i.kind!=="mark"||!f.includes(c.mark?.type)))return b({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"not_applicable",evidencePaths:["target"],findings:[]}],findings:[{severity:"info",code:"unsupported-combination",reason:"mark-type",message:`${n} supports ${f.join(", ")} marks.`,resource:g(i.kind,i.id,i.childPath)}]})}const m=e[n],l=typeof m=="function"?J(m):void 0;if(l===void 0)return b({version:1,action:n,applicability:typeof m=="function"?"unverified":"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-format",reason:typeof m=="function"?"extension-unverified":"action-unavailable",message:`Action "${n}" has no built-in inspection descriptor.`}]});const k=ie.get(n),d=l.options??k?.options.map(f=>f.path);if(d===void 0)return b({version:1,action:n,applicability:"unverified",parameterDefinitions:[],requirements:[],checks:[{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:[{severity:"info",code:"unsupported-format",reason:"descriptor-unavailable",message:`Action "${n}" is registered but its static option descriptor is unavailable.`}]});const a=Object.keys(t).find(f=>!d.includes(f)),o=(k?.options.filter(f=>f.required).map(f=>f.path)??[]).filter(f=>t[f]===void 0);let p=[];if(c!==void 0&&i.kind==="mark"){const f=$(e,i.childPath),O=n.startsWith("edit")&&n.endsWith("Plot")?Object.entries(f.markConfigs[i.id]??{}).find(([r])=>r.toLowerCase()===n.slice(4).toLowerCase())?.[1]:void 0,x=O?.source===void 0?c.data:t.data??O.source,y=te(e,{data:x,childPath:i.childPath}).schema;if(y.completeness==="known"){const r=new Set(y.fields.map(v=>v.name));p=(k?.options??[]).filter(v=>v.kind==="field"&&typeof t[v.path]=="string"&&!r.has(t[v.path])).map(v=>({severity:"error",code:"missing-resource",reason:"field-unavailable",message:`Dataset does not contain field "${t[v.path]}".`,resource:g("data",x,i.childPath),optionPath:v.path}))}}if(a===void 0&&o.length===0&&(n==="facet"&&t.field||n==="facetGrid"&&t.rows&&t.columns))try{(n==="facet"?K:L)(e.semanticSpec,t)}catch(f){p.push({severity:"error",code:"unsupported-combination",reason:"facet-partition",message:f.message,optionPath:"data"})}const u=a!==void 0?[{severity:"error",code:"invalid-option",reason:"unknown-option",message:`Unknown ${n} option "${a}".`,optionPath:a}]:[...o.map(f=>({severity:"info",code:"missing-input",reason:"required-option",message:`${n} requires option "${f}".`,optionPath:f})),...p],h=a!==void 0||p.length?"incompatible":o.length?"needs-input":"supported";return b({version:1,action:n,applicability:h,parameterDefinitions:k?.options.map(f=>({...f,active:!0}))??d.map(f=>({path:f,kind:"object",required:!1,active:!0})),requirements:o.map(f=>({reason:"required-option",optionPath:f})),checks:[{name:"option-shape",status:a===void 0?"passed":"failed",evidencePaths:Object.keys(t),findings:a===void 0?[]:u},{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:u})}function T(e){return Array.isArray(e)?e.map(T):S(e)?Object.fromEntries(Object.keys(e).sort().map(n=>[n,T(e[n])])):e}function j(e){return JSON.stringify(Y(T(e)))}const D=new WeakMap;function M(e,n){if(Object.is(e,n))return!0;if(e===null||n===null||typeof e!="object"||typeof n!="object"||Array.isArray(e)!==Array.isArray(n))return!1;if(!Array.isArray(e)&&(!S(e)||!S(n)))return j(e)===j(n);const i=_(e)&&_(n),t=i?D.get(e)?.get(n):void 0;if(t!==void 0)return t;const c=Object.keys(e),m=(!Array.isArray(e)||e.length===n.length)&&c.length===Object.keys(n).length&&c.every(l=>Object.hasOwn(n,l)&&M(e[l],n[l]));return i&&(D.has(e)||D.set(e,new WeakMap),D.get(e).set(n,m)),m}function N(e,n=[],i=[]){for(const t of e.semanticSpec.datasets)i.push({ref:g("data",t.id,n),value:t});for(const[t,c]of e.semanticSpec.layers.entries())i.push({ref:g("mark",c.id,n),value:{semantic:c,graphic:e.graphicSpec.objects[c.id],config:e.markConfigs[c.id],orderIndex:t}});for(const t of e.semanticSpec.scales)i.push({ref:g("scale",t.id,n),value:{semantic:t,resolved:e.resolvedScales[t.id]}});for(const t of e.semanticSpec.coordinates)i.push({ref:g("coordinate",t.id,n),value:t});for(const[t,c]of Object.entries(e.semanticSpec.guides))i.push({ref:g("guide",t,n),value:{semantic:c,graphic:e.graphicSpec.objects[t],config:e.guideConfigs[t]}});Object.keys(e.semanticSpec.title).length&&i.push({ref:g("guide","title",n),value:{semantic:e.semanticSpec.title,config:e.titleConfig}}),e.graphicSpec.objects.canvas!==void 0&&i.push({ref:g("canvas","canvas",n),value:e.graphicSpec.objects.canvas}),Object.keys(e.compositionSpec??{}).length>0&&i.push({ref:g("composition","layout",n),value:e.compositionSpec});for(const[t,c]of Object.entries(e.children))N(c,[...n,t],i);return i}function A(e){return`${e.childPath?.join("/")??""}|${e.kind}|${e.id}`}function se(e){return e==="data"?"data":e==="scale"?"scale":e==="guide"?"guide":["coordinate","canvas","composition"].includes(e)?"structure":"style"}function re(e,n,i){return i!=="mark"||e===void 0||n===void 0?se(i):e.value.orderIndex!==n.value.orderIndex?"structure":!M(e.value.semantic?.encoding,n.value.semantic?.encoding)||e.value.semantic?.data!==n.value.semantic?.data?"binding":M(e.value.semantic,n.value.semantic)?"style":"structure"}function Se(e,n,{target:i}={}){P(e,"Before program"),P(n,"After program"),i!==void 0&&R(i);const t=new Map(N(e).map(a=>[A(a.ref),a])),c=new Map(N(n).map(a=>[A(a.ref),a]));let l=[...new Set([...t.keys(),...c.keys()])].sort().flatMap(a=>{const s=t.get(a),o=c.get(a),p=s??o;if(s!==void 0&&o!==void 0&&M(s.value,o.value))return[];const u=p.ref.kind==="mark"&&s!==void 0&&o!==void 0?[...new Set([...Object.keys(s.value.semantic?.encoding??{}),...Object.keys(o.value.semantic?.encoding??{})])].filter(h=>!M(s.value.semantic?.encoding?.[h],o.value.semantic?.encoding?.[h])):[];return[{resource:p.ref,operation:s===void 0?"add":o===void 0?"remove":"modify",effect:re(s,o,p.ref.kind),roles:u}]});if(i!==void 0){const a=[e,n].flatMap(o=>{const p=I(o,i);return p===void 0?[]:[A(g(i.kind,i.kind==="data"?p.id:i.id,i.childPath))]});if(a.length===0)throw new Error(`Unknown ${i.kind} "${i.id}" in both programs.`);const s=new Set(a);for(const o of[t,c]){let p=!0;for(;p;){p=!1;for(const[u,h]of o){if(s.has(u))continue;const f=j(h.ref.childPath??[]);[...s].some(x=>{const y=o.get(x);return y===void 0||j(y.ref.childPath??[])!==f?!1:y.ref.kind==="data"?h.ref.kind==="data"?h.value.source===y.ref.id:h.ref.kind==="mark"&&h.value.semantic?.data===y.ref.id:y.ref.kind==="mark"?h.ref.kind==="mark"&&h.value.semantic?.source===y.ref.id||h.ref.kind==="guide"&&(h.value.semantic?.target===y.ref.id||h.value.config?.target===y.ref.id):y.ref.kind==="scale"?h.ref.kind==="mark"&&Object.values(h.value.semantic?.encoding??{}).some(r=>r?.scale===y.ref.id||r?.dimensions?.some(v=>v.scale===y.ref.id))||h.ref.kind==="guide"&&(h.value.semantic?.scale===y.ref.id||h.value.config?.scale===y.ref.id):y.ref.kind==="coordinate"&&h.ref.kind==="mark"&&h.value.semantic?.coordinate===y.ref.id})&&(s.add(u),p=!0)}}}l=l.filter(o=>s.has(A(o.resource)))}const k=![E,C].includes(e.constructor)||![E,C].includes(n.constructor),d=k?[{severity:"info",code:"unsupported-format",reason:"extension-unverified",message:"Extension-owned semantic equivalence could not be fully verified."}]:[];return b({version:1,equivalence:l.length?"different":k?"unknown":"equal",completeProgramComparison:i===void 0,changes:l,affected:l.map(a=>a.resource),findings:d})}function q(e,n){if(!S(e))return null;if(n==="circle")return!Number.isFinite(e.x)||!Number.isFinite(e.y)||!Number.isFinite(e.radius)||e.radius<0?null:{x:e.x-e.radius,y:e.y-e.radius,width:e.radius*2,height:e.radius*2};if(n==="rect")return[e.x,e.y,e.width,e.height].every(Number.isFinite)?{x:Math.min(e.x,e.x+e.width),y:Math.min(e.y,e.y+e.height),width:Math.abs(e.width),height:Math.abs(e.height)}:null;if(n==="path"&&Array.isArray(e.commands)){const i=e.commands.flatMap(m=>Number.isFinite(m.x)&&Number.isFinite(m.y)?[[m.x,m.y]]:[]);if(i.length===0)return null;const t=i.map(m=>m[0]),c=i.map(m=>m[1]);return{x:Math.min(...t),y:Math.min(...c),width:Math.max(...t)-Math.min(...t),height:Math.max(...c)-Math.min(...c)}}return[e.x1,e.y1,e.x2,e.y2].every(Number.isFinite)?{x:Math.min(e.x1,e.x2),y:Math.min(e.y1,e.y2),width:Math.abs(e.x2-e.x1),height:Math.abs(e.y2-e.y1)}:null}function ce(e,n,i){const t=e.graphicSpec.objects[n.id],c=z(e,n.data),m=new Set(["circle","rect","line","path"]),l=t?.items??(t?.properties?[{properties:t.properties}]:[]),k=l.map(r=>r.type??t?.type),d=t!==void 0&&k.every(r=>m.has(r)||r==="text"),a=d&&k.every(r=>m.has(r)),s=l.filter(r=>r.properties.opacity===0).length,o=l.filter(r=>{const v=q(r.properties,r.type??t?.type);return v!==null&&v.width===0&&v.height===0}).length,p=l.map(r=>q(r.properties,r.type??t?.type)).filter(Boolean),u=t===void 0?void 0:B(e.graphicSpec,n.id,{rootViewport:!0}),h=t===void 0?[0,0]:W(e.graphicSpec,n.id),f=u===void 0?null:l.flatMap((r,v)=>{const w=q(r.properties,r.type??t.type);return w!==null&&(u.right<=u.left||u.bottom<=u.top||w.x+h[0]+w.width<u.left||w.y+h[1]+w.height<u.top||w.x+h[0]>u.right||w.y+h[1]>u.bottom)?[v]:[]}),O=f?.length??null,x=p.length===0?null:{x:Math.min(...p.map(r=>r.x)),y:Math.min(...p.map(r=>r.y)),width:Math.max(...p.map(r=>r.x+r.width))-Math.min(...p.map(r=>r.x)),height:Math.max(...p.map(r=>r.y+r.height))-Math.min(...p.map(r=>r.y))},y=["line","area"].includes(n.mark?.type)?"series":"item";return{owner:g("mark",n.id,i),grain:y,rawRows:c?.values?.length??null,logicalDataItems:d?l.length:null,drawablePrimitives:d?l.length:null,visibleCandidates:a?Math.max(0,l.length-new Set([...l.map((r,v)=>r.properties.opacity===0?v:-1),...l.map((r,v)=>{const w=q(r.properties,r.type??t?.type);return w!==null&&w.width===0&&w.height===0?v:-1}),...f??[]].filter(r=>r>=0)).size):null,flags:[{reason:"opacity-zero",count:s},{reason:"zero-size",count:o},{reason:"outside-clip",count:O},{reason:"missing-skipped",count:n.mark?.missing==="skip"&&c?.values?Math.max(0,c.values.length-l.length):null}],bounds:x,sourceRefs:c===void 0?[]:[g("data",c.id,i)],checks:[{name:"concrete-structure",status:t===void 0&&!e.compositionSpec?.facet?"failed":d?"passed":"not_applicable",evidencePaths:[n.id],findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function U(e,n,i){const t=new Set(e.semanticSpec.layers.map(s=>s.id)),c=n==="axis"?/axis/iu:n==="grid"?/grid/iu:n==="legend"?/legend/iu:n==="title"?/title|subtitle/iu:new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"iu"),m=Object.keys(e.graphicSpec.objects).filter(s=>s!=="canvas"&&!s.startsWith("plot-")&&!t.has(s)&&c.test(s)),l=m.flatMap(s=>{const o=e.graphicSpec.objects[s];return Array.isArray(o?.items)?o.items:o?.properties===void 0?[]:[{properties:o.properties}]}),k=n==="title"?e.semanticSpec.title:e.semanticSpec.guides[n],d=[];function a(s){if(S(s)){typeof s.scale=="string"&&d.push(g("scale",s.scale,i)),typeof s.coordinate=="string"&&d.push(g("coordinate",s.coordinate,i));for(const o of Object.values(s))a(o)}}return a(k),{owner:g("guide",n,i),grain:"unknown",rawRows:null,logicalDataItems:l.length,drawablePrimitives:l.length,visibleCandidates:null,flags:[{reason:"opacity-zero",count:null},{reason:"zero-size",count:null},{reason:"outside-clip",count:null},{reason:"missing-skipped",count:null}],bounds:null,sourceRefs:[...new Map(d.map(s=>[A(s),s])).values()],checks:[{name:"concrete-structure",status:m.length>0?"passed":"not_applicable",evidencePaths:m,findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function xe(e,{target:n}={}){P(e),n!==void 0&&R(n);const i=[];function t(d,a=[]){const s=new Set,o=new Set;function p(u){if(o.has(u))throw new Error(`Graphic hierarchy contains a cycle at "${u}".`);if(s.has(u))throw new Error(`Graphic "${u}" is referenced more than once.`);const h=d.graphicSpec.objects[u];if(h===void 0)throw new Error(`Graphic hierarchy references missing object "${u}".`);s.add(u),o.add(u);for(const f of h.children??[])p(f);o.delete(u)}for(const u of d.graphicSpec.order)p(u);for(const u of d.semanticSpec.layers)i.push(ce(d,u,a));for(const u of Object.keys(d.semanticSpec.guides))i.push(U(d,u,a));Object.keys(d.semanticSpec.title).length>0&&i.push(U(d,"title",a));for(const[u,h]of Object.entries(d.children))t(h,[...a,u])}t(e);let c=i;if(n!==void 0){const d=$(e,n.childPath),a=I(e,n);if(a===void 0)throw new Error(`Unknown ${n.kind} "${n.id}".`);c=i.filter(s=>{if(j(s.owner.childPath??[])!==j(n.childPath??[]))return!1;if(n.kind==="mark")return s.owner.id===n.id;const o=G(d,s.owner.id);return n.kind==="data"?o?.data===a.id:n.kind==="coordinate"?o?.coordinate===n.id:n.kind==="scale"?Object.values(o?.encoding??{}).some(p=>p?.scale===n.id||p?.dimensions?.some(u=>u.scale===n.id)):n.kind==="guide"?s.owner.kind==="guide"&&s.owner.id===n.id:!1})}const m=[];function l(d,a=[]){for(const s of Object.values(d.materializationConfigs.calculations??{}))if(S(s))for(const o of Object.values(s))m.push(a.length===0?o:{...o,childPath:a});for(const[s,o]of Object.entries(d.children))l(o,[...a,s])}l(e);const k=c.filter(d=>d.logicalDataItems===null).map(d=>d.owner);return b({version:1,views:c,calculations:m,checks:[{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}],findings:[],coverage:{partial:c.some(d=>d.checks.some(a=>a.status!=="passed")),unsupported:k}})}export{Se as comparePrograms,be as describeAction,te as getDatasetSchema,xe as inspectProgram};
1
+ import{resolveMarkItems as Q}from"./materialization/selection/policies/index.js";import{highlightTargets as ee}from"./selectors/selectionTargets.js";import{resolvePathCommandBounds as ne,unionConcreteGraphicBounds as ie}from"./grammar/schemas/graphicBounds.js";import{facetUnitTemplate as U}from"./actions/facets/actions.js";import{plotComposition as te}from"./selectors/plotComposition.js";import{overlaySourceReason as L}from"./grammar/overlay.js";import{findUpstreamTransform as W}from"./materialization/dataProvenance.js";import{planLayerPlot as se}from"./actions/composition/layerPlan.js";import{resolveAdjacentSource as re}from"./materialization/adjacentSource.js";import{normalizeActionTargets as oe}from"./grammar/actionTargets.js";import{resolveGraphicClipBounds as ce,resolveGraphicParentOffset as ae}from"./grammar/schemas/graphicBounds.js";import{resolveFacetDefinition as de,resolveFacetGridDefinition as ue}from"./grammar/facets/index.js";import{APPEARANCE_MARK_TYPES as D,FIXED_APPEARANCE_MARK_TYPES as K}from"./core/vocabulary.js";import{ChartProgram as I}from"./ChartProgram.js";import{BasicChartProgram as R}from"./BasicChartProgram.js";import{getWrappedActionMetadata as fe,supportsScopedActionTargets as le}from"./core/action.js";import{cloneAndFreeze as S,isOwned as V,isPlainObject as M}from"./core/immutable.js";import{encodeValue as pe}from"./persistence/codec.js";import{findDataset as C,hasDatasetOwner as me,resolveDatasetReference as Y}from"./selectors/datasets.js";import{findLayer as _,resolveEligibleLayer as he}from"./selectors/layers.js";import"./selectors/markOwners.js";import{normalizeCompositionShares as ye}from"./core/compositionScales.js";import{findSemanticScale as ve}from"./selectors/scales.js";import{findCoordinate as F}from"./selectors/coordinates.js";import{inferDatasetSchema as ge}from"./grammar/datasetSchema.js";import{BUILTIN_ACTION_DESCRIPTOR_RECORDS as ke}from"./inspectionDescriptors.js";const we=new Set(["data","mark","scale","coordinate","guide"]),be=new Map(ke.map(([e,n,i,t,a])=>[e,{options:n.map(([h,m,g,l,d])=>({path:h,required:m,kind:g,...l===null?{}:{choices:l},...d===null?{}:{unit:d}})),idOptions:i,prerequisites:t,entryPoints:a}]));function O(e,n="Program"){if(!(e instanceof I||e instanceof R))throw new TypeError(`${n} must be a ggaction program.`);if(e.actionStack.length!==0)throw new Error(`${n} must have a closed action stack.`);return e}function P(e,n=[]){if(!Array.isArray(n))throw new TypeError("childPath must be an array.");let i=e;for(const t of n){if(typeof t!="string"||!Object.hasOwn(i.children,t))throw new Error(`Unknown program child "${String(t)}".`);i=i.children[t]}return i}function B(e){if(!M(e)||!we.has(e.kind)||typeof e.id!="string"||e.id.length===0)throw new TypeError("Inspection target requires a supported kind and non-empty id.");return e}function N(e,n){const i=P(e,n.childPath);return n.kind==="data"?me(i,n.id)?Y(i,n.id,"Inspection target"):C(i,n.id):n.kind==="mark"?_(i,n.id):n.kind==="scale"?ve(i,n.id):n.kind==="coordinate"?F(i,n.id):i.semanticSpec.guides[n.id]}function w(e,n,i){return{kind:e,id:n,...i?.length?{childPath:[...i]}:{}}}function en(e,{target:n,childPath:i}={}){return O(e),M(n)&&(i=n.childPath,n=n.id),S(te(P(e,i),n))}function Se(e,{data:n,childPath:i}={}){O(e);const t=P(e,i);if(typeof n!="string"||n.length===0)throw new TypeError("getDatasetSchema requires data.");const a=Y(t,n,"Dataset schema data"),h=a.schema??ge(a.values??[]);return S({data:a.id,schema:h})}function xe(e,{action:n,target:i,options:t={}}={}){if(O(e),!M(t))throw new TypeError("describeAction options must be a plain object.");if(M(t.target)||Array.isArray(t.target)){if(O(e),i!==void 0)throw new Error("Use one inspection target or explicit action target selectors, not both.");if(!le(e.constructor,n))return S({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-combination",reason:"entry-point",message:"Scoped action targets require a Full domain action."}]});const{target:s,...x}=t,y=oe(s).map(v=>{let j=e;for(const H of v.childPath){if(j.compositionSpec?.type==="facet")throw new Error("Facet cells follow their source; use editFacetSource for persistent edits.");j=P(j,[H])}if(v.facetSource){if(j.compositionSpec?.type!=="facet")throw new Error("facetSource requires a facet view.");j=U(j,!0)}if(j.compositionSpec?.type==="facet"&&v.id!==void 0)throw new Error("Target a facet source through editFacetSource.");const Z=v.id===void 0||n.includes("CompositionChild")?void 0:{kind:"mark",id:v.id};return xe(j,{action:n,target:Z,options:{...x,...v.id===void 0?{}:{target:v.id}}})}),k=["incompatible","unsupported","needs-input","unverified","supported"].find(v=>y.some(j=>j.applicability===v));return S({...y[0],applicability:k,requirements:y.flatMap(v=>v.requirements),findings:y.flatMap(v=>v.findings),checks:y.flatMap(v=>v.checks)})}if(O(e),typeof n!="string"||n.length===0)throw new TypeError("describeAction requires action.");if(!M(t))throw new TypeError("describeAction options must be a plain object.");let a;if(i!==void 0){if(B(i),a=N(e,i),a===void 0)return S({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:[],findings:[]}],findings:[{severity:"error",code:"missing-resource",reason:"target-unavailable",message:`Unknown ${i.kind} "${i.id}".`,resource:w(i.kind,i.id,i.childPath)}]});if(t.target!==void 0&&t.target!==i.id)return S({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:["target"],findings:[]}],findings:[{severity:"error",code:"incompatible-resource",reason:"target-mismatch",message:`Inspection target "${i.id}" conflicts with options.target "${t.target}".`,resource:w(i.kind,i.id,i.childPath),optionPath:"target"}]});const s=t.field!==void 0?D[n]:K[n]??D[n];if(s&&(i.kind!=="mark"||!s.includes(a.mark?.type)))return S({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"not_applicable",evidencePaths:["target"],findings:[]}],findings:[{severity:"info",code:"unsupported-combination",reason:"mark-type",message:`${n} supports ${s.join(", ")} marks.`,resource:w(i.kind,i.id,i.childPath)}]})}const h=e[n],m=typeof h=="function"?fe(h):void 0;if(m===void 0)return S({version:1,action:n,applicability:typeof h=="function"?"unverified":"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-format",reason:typeof h=="function"?"extension-unverified":"action-unavailable",message:`Action "${n}" has no built-in inspection descriptor.`}]});let g=be.get(n);a?.mark&&K[n]?.includes(a.mark.type)&&!D[n].includes(a.mark.type)&&(g={...g,options:g.options.filter(s=>!["field","fieldType","scale"].includes(s.path))});const l=m.options??g?.options.map(s=>s.path);if(l===void 0)return S({version:1,action:n,applicability:"unverified",parameterDefinitions:[],requirements:[],checks:[{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:[{severity:"info",code:"unsupported-format",reason:"descriptor-unavailable",message:`Action "${n}" is registered but its static option descriptor is unavailable.`}]});const d=Object.keys(t).find(s=>!l.includes(s)),r=(g?.options.filter(s=>s.required).map(s=>s.path)??[]).filter(s=>t[s]===void 0);let p=[];if(n==="createLayerPlot")try{const s=P(e,i?.childPath);se(s.compositionSpec?.type==="facet"?U(s,!0):s,{...t,source:t.source??i?.id})}catch(s){p.push({severity:"error",code:"unsupported-combination",reason:"layer-compatibility",message:s.message})}if(n==="createAdjacentPlot"){const s=P(e,i?.childPath);try{re(s,t.source??i?.id)}catch(x){t.source||i?.id||s.compositionSpec&&s.compositionSpec.type!=="facet"?p.push({severity:"error",code:"missing-resource",reason:"adjacent-source",message:x.message}):r.push("source")}}if(n==="editCompositionScales"&&((!e.compositionSpec||e.compositionSpec.type==="facet")&&p.push({severity:"error",code:"unsupported-combination",reason:"composition-scope",message:"editCompositionScales requires a concat parent."}),t.share!==void 0))try{if(!Object.keys(ye(t.share)).length)throw new Error("At least one shared-channel policy is required.")}catch(s){p.push({severity:"error",code:"invalid-option",reason:"share-policy",message:s.message,optionPath:"share"})}if(["createOverlay","editOverlay"].includes(n)){const s=P(e,i?.childPath),x=n==="createOverlay"?t.source??i?.id??s.context.currentMark:t.target??i?.id??s.context.currentMark,b=_(s,x);b===void 0?r.push(n==="createOverlay"?"source":"target"):(n==="editOverlay"?!b.derivedBindings?.overlay:L(b,F(s,b.coordinate)?.type,W(s,C(s,b.data),"density")))?p.push({severity:"error",code:"unsupported-combination",reason:"overlay-source",message:n==="createOverlay"?L(b,F(s,b.coordinate)?.type,W(s,C(s,b.data),"density")):"editOverlay requires a linked overlay target."}):n==="createOverlay"&&b.mark.type==="point"&&t.type!=="point"&&!t.orderBy&&r.push("orderBy")}if(a!==void 0&&i.kind==="mark"){const s=P(e,i.childPath),x=n.startsWith("edit")&&n.endsWith("Plot")?Object.entries(s.markConfigs[i.id]??{}).find(([u])=>u.toLowerCase()===n.slice(4).toLowerCase())?.[1]:void 0,b=x?.source===void 0?a.data:t.data??x.source,y=Se(e,{data:b,childPath:i.childPath}).schema;if(y.completeness==="known"){const u=new Set(y.fields.map(k=>k.name));p.push(...(g?.options??[]).filter(k=>k.kind==="field"&&typeof t[k.path]=="string"&&!u.has(t[k.path])).map(k=>({severity:"error",code:"missing-resource",reason:"field-unavailable",message:`Dataset does not contain field "${t[k.path]}".`,resource:w("data",b,i.childPath),optionPath:k.path})))}}if(d===void 0&&r.length===0&&(n==="facet"&&t.field||n==="facetGrid"&&t.rows&&t.columns))try{(n==="facet"?de:ue)(e.semanticSpec,t)}catch(s){p.push({severity:"error",code:"unsupported-combination",reason:"facet-partition",message:s.message,optionPath:"data"})}const f=d!==void 0?[{severity:"error",code:"invalid-option",reason:"unknown-option",message:`Unknown ${n} option "${d}".`,optionPath:d}]:[...r.map(s=>({severity:"info",code:"missing-input",reason:"required-option",message:`${n} requires option "${s}".`,optionPath:s})),...p],c=d!==void 0||p.length?"incompatible":r.length?"needs-input":"supported";return S({version:1,action:n,applicability:c,parameterDefinitions:g?.options.map(s=>({...s,active:!0}))??l.map(s=>({path:s,kind:"object",required:!1,active:!0})),requirements:r.map(s=>({reason:"required-option",optionPath:s})),checks:[{name:"option-shape",status:d===void 0?"passed":"failed",evidencePaths:Object.keys(t),findings:d===void 0?[]:f},{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:f})}function z(e){return Array.isArray(e)?e.map(z):M(e)?Object.fromEntries(Object.keys(e).sort().map(n=>[n,z(e[n])])):e}function A(e){return JSON.stringify(pe(z(e)))}const $=new WeakMap;function E(e,n){if(Object.is(e,n))return!0;if(e===null||n===null||typeof e!="object"||typeof n!="object"||Array.isArray(e)!==Array.isArray(n))return!1;if(!Array.isArray(e)&&(!M(e)||!M(n)))return A(e)===A(n);const i=V(e)&&V(n),t=i?$.get(e)?.get(n):void 0;if(t!==void 0)return t;const a=Object.keys(e),h=(!Array.isArray(e)||e.length===n.length)&&a.length===Object.keys(n).length&&a.every(m=>Object.hasOwn(n,m)&&E(e[m],n[m]));return i&&($.has(e)||$.set(e,new WeakMap),$.get(e).set(n,h)),h}function G(e,n=[],i=[]){for(const t of e.semanticSpec.datasets)i.push({ref:w("data",t.id,n),value:t});for(const[t,a]of e.semanticSpec.layers.entries())i.push({ref:w("mark",a.id,n),value:{semantic:a,graphic:e.graphicSpec.objects[a.id],config:e.markConfigs[a.id],orderIndex:t}});for(const t of e.semanticSpec.scales)i.push({ref:w("scale",t.id,n),value:{semantic:t,resolved:e.resolvedScales[t.id]}});for(const t of e.semanticSpec.coordinates)i.push({ref:w("coordinate",t.id,n),value:t});for(const[t,a]of Object.entries(e.semanticSpec.guides))i.push({ref:w("guide",t,n),value:{semantic:a,graphic:e.graphicSpec.objects[t],config:e.guideConfigs[t]}});Object.keys(e.semanticSpec.title).length&&i.push({ref:w("guide","title",n),value:{semantic:e.semanticSpec.title,config:e.titleConfig}}),e.graphicSpec.objects.canvas!==void 0&&i.push({ref:w("canvas","canvas",n),value:e.graphicSpec.objects.canvas}),Object.keys(e.compositionSpec??{}).length>0&&i.push({ref:w("composition","layout",n),value:e.compositionSpec});for(const[t,a]of Object.entries(e.children))G(a,[...n,t],i);return i}function q(e){return`${e.childPath?.join("/")??""}|${e.kind}|${e.id}`}function Me(e){return e==="data"?"data":e==="scale"?"scale":e==="guide"?"guide":["coordinate","canvas","composition"].includes(e)?"structure":"style"}function je(e,n,i){return i!=="mark"||e===void 0||n===void 0?Me(i):e.value.orderIndex!==n.value.orderIndex?"structure":!E(e.value.semantic?.encoding,n.value.semantic?.encoding)||e.value.semantic?.data!==n.value.semantic?.data?"binding":E(e.value.semantic,n.value.semantic)?"style":"structure"}function nn(e,n,{target:i}={}){O(e,"Before program"),O(n,"After program"),i!==void 0&&B(i);const t=new Map(G(e).map(d=>[q(d.ref),d])),a=new Map(G(n).map(d=>[q(d.ref),d]));let m=[...new Set([...t.keys(),...a.keys()])].sort().flatMap(d=>{const o=t.get(d),r=a.get(d),p=o??r;if(o!==void 0&&r!==void 0&&E(o.value,r.value))return[];const f=p.ref.kind==="mark"&&o!==void 0&&r!==void 0?[...new Set([...Object.keys(o.value.semantic?.encoding??{}),...Object.keys(r.value.semantic?.encoding??{})])].filter(c=>!E(o.value.semantic?.encoding?.[c],r.value.semantic?.encoding?.[c])):[];return[{resource:p.ref,operation:o===void 0?"add":r===void 0?"remove":"modify",effect:je(o,r,p.ref.kind),roles:f}]});if(i!==void 0){const d=[e,n].flatMap(r=>{const p=N(r,i);return p===void 0?[]:[q(w(i.kind,i.kind==="data"?p.id:i.id,i.childPath))]});if(d.length===0)throw new Error(`Unknown ${i.kind} "${i.id}" in both programs.`);const o=new Set(d);for(const r of[t,a]){let p=!0;for(;p;){p=!1;for(const[f,c]of r){if(o.has(f))continue;const s=A(c.ref.childPath??[]);[...o].some(b=>{const y=r.get(b);return y===void 0||A(y.ref.childPath??[])!==s?!1:y.ref.kind==="data"?c.ref.kind==="data"?c.value.source===y.ref.id:c.ref.kind==="mark"&&c.value.semantic?.data===y.ref.id:y.ref.kind==="mark"?c.ref.kind==="mark"&&c.value.semantic?.source===y.ref.id||c.ref.kind==="guide"&&(c.value.semantic?.target===y.ref.id||c.value.config?.target===y.ref.id):y.ref.kind==="scale"?c.ref.kind==="mark"&&Object.values(c.value.semantic?.encoding??{}).some(u=>u?.scale===y.ref.id||u?.dimensions?.some(k=>k.scale===y.ref.id))||c.ref.kind==="guide"&&(c.value.semantic?.scale===y.ref.id||c.value.config?.scale===y.ref.id):y.ref.kind==="coordinate"&&c.ref.kind==="mark"&&c.value.semantic?.coordinate===y.ref.id})&&(o.add(f),p=!0)}}}m=m.filter(r=>o.has(q(r.resource)))}const g=![I,R].includes(e.constructor)||![I,R].includes(n.constructor),l=g?[{severity:"info",code:"unsupported-format",reason:"extension-unverified",message:"Extension-owned semantic equivalence could not be fully verified."}]:[];return S({version:1,equivalence:m.length?"different":g?"unknown":"equal",completeProgramComparison:i===void 0,changes:m,affected:m.map(d=>d.resource),findings:l})}function T(e,n){if(!M(e))return null;if(n==="circle")return!Number.isFinite(e.x)||!Number.isFinite(e.y)||!Number.isFinite(e.radius)||e.radius<0?null:{x:e.x-e.radius,y:e.y-e.radius,width:e.radius*2,height:e.radius*2};if(n==="rect")return[e.x,e.y,e.width,e.height].every(Number.isFinite)?{x:Math.min(e.x,e.x+e.width),y:Math.min(e.y,e.y+e.height),width:Math.abs(e.width),height:Math.abs(e.height)}:null;if(n==="path"&&Array.isArray(e.commands)){const i=e.commands.flatMap(h=>Number.isFinite(h.x)&&Number.isFinite(h.y)?[[h.x,h.y]]:[]);if(i.length===0)return null;const t=i.map(h=>h[0]),a=i.map(h=>h[1]);return{x:Math.min(...t),y:Math.min(...a),width:Math.max(...t)-Math.min(...t),height:Math.max(...a)-Math.min(...a)}}return[e.x1,e.y1,e.x2,e.y2].every(Number.isFinite)?{x:Math.min(e.x1,e.x2),y:Math.min(e.y1,e.y2),width:Math.abs(e.x2-e.x1),height:Math.abs(e.y2-e.y1)}:null}function Pe(e,n,i){const t=e.graphicSpec.objects[n.id],a=C(e,n.data),h=new Set(["circle","rect","line","path"]),m=t?.items??(t?.properties?[{properties:t.properties}]:[]),g=m.map(u=>u.type??t?.type),l=t!==void 0&&g.every(u=>h.has(u)||u==="text"),d=l&&g.every(u=>h.has(u)),o=m.filter(u=>u.properties.opacity===0).length,r=m.filter(u=>{const k=T(u.properties,u.type??t?.type);return k!==null&&k.width===0&&k.height===0}).length,p=m.map(u=>T(u.properties,u.type??t?.type)).filter(Boolean),f=t===void 0?void 0:ce(e.graphicSpec,n.id,{rootViewport:!0}),c=t===void 0?[0,0]:ae(e.graphicSpec,n.id),s=f===void 0?null:m.flatMap((u,k)=>{const v=T(u.properties,u.type??t.type);return v!==null&&(f.right<=f.left||f.bottom<=f.top||v.x+c[0]+v.width<f.left||v.y+c[1]+v.height<f.top||v.x+c[0]>f.right||v.y+c[1]>f.bottom)?[k]:[]}),x=s?.length??null,b=p.length===0?null:{x:Math.min(...p.map(u=>u.x)),y:Math.min(...p.map(u=>u.y)),width:Math.max(...p.map(u=>u.x+u.width))-Math.min(...p.map(u=>u.x)),height:Math.max(...p.map(u=>u.y+u.height))-Math.min(...p.map(u=>u.y))},y=["line","area"].includes(n.mark?.type)?"series":"item";return{owner:w("mark",n.id,i),grain:y,rawRows:a?.values?.length??null,logicalDataItems:l?m.length:null,drawablePrimitives:l?m.length:null,visibleCandidates:d?Math.max(0,m.length-new Set([...m.map((u,k)=>u.properties.opacity===0?k:-1),...m.map((u,k)=>{const v=T(u.properties,u.type??t?.type);return v!==null&&v.width===0&&v.height===0?k:-1}),...s??[]].filter(u=>u>=0)).size):null,flags:[{reason:"opacity-zero",count:o},{reason:"zero-size",count:r},{reason:"outside-clip",count:x},{reason:"missing-skipped",count:n.mark?.missing==="skip"&&a?.values?Math.max(0,a.values.length-m.length):null}],bounds:b,sourceRefs:[...a===void 0?[]:[w("data",a.id,i)],...n.derivedBindings?.overlay?[w("mark",n.derivedBindings.overlay.source,i)]:[]],checks:[{name:"concrete-structure",status:t===void 0&&!e.compositionSpec?.facet?"failed":l?"passed":"not_applicable",evidencePaths:[n.id],findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function J(e,n,i){const t=new Set(e.semanticSpec.layers.map(o=>o.id)),a=n==="axis"?/axis/iu:n==="grid"?/grid/iu:n==="legend"?/legend/iu:n==="title"?/title|subtitle/iu:new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"iu"),h=Object.keys(e.graphicSpec.objects).filter(o=>o!=="canvas"&&!o.startsWith("plot-")&&!t.has(o)&&a.test(o)),m=h.flatMap(o=>{const r=e.graphicSpec.objects[o];return Array.isArray(r?.items)?r.items:r?.properties===void 0?[]:[{properties:r.properties}]}),g=n==="title"?e.semanticSpec.title:e.semanticSpec.guides[n],l=[];function d(o){if(M(o)){typeof o.scale=="string"&&l.push(w("scale",o.scale,i)),typeof o.coordinate=="string"&&l.push(w("coordinate",o.coordinate,i));for(const r of Object.values(o))d(r)}}return d(g),{owner:w("guide",n,i),grain:"unknown",rawRows:null,logicalDataItems:m.length,drawablePrimitives:m.length,visibleCandidates:null,flags:[{reason:"opacity-zero",count:null},{reason:"zero-size",count:null},{reason:"outside-clip",count:null},{reason:"missing-skipped",count:null}],bounds:null,sourceRefs:[...new Map(l.map(o=>[q(o),o])).values()],checks:[{name:"concrete-structure",status:h.length>0?"passed":"not_applicable",evidencePaths:h,findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function tn(e,{target:n}={}){O(e),n!==void 0&&B(n);const i=[];function t(l,d=[]){const o=new Set,r=new Set;function p(f){if(r.has(f))throw new Error(`Graphic hierarchy contains a cycle at "${f}".`);if(o.has(f))throw new Error(`Graphic "${f}" is referenced more than once.`);const c=l.graphicSpec.objects[f];if(c===void 0)throw new Error(`Graphic hierarchy references missing object "${f}".`);o.add(f),r.add(f);for(const s of c.children??[])p(s);r.delete(f)}for(const f of l.graphicSpec.order)p(f);for(const f of l.semanticSpec.layers)i.push(Pe(l,f,d));for(const f of Object.keys(l.semanticSpec.guides))i.push(J(l,f,d));Object.keys(l.semanticSpec.title).length>0&&i.push(J(l,"title",d));for(const[f,c]of Object.entries(l.children))t(c,[...d,f])}t(e);let a=i;if(n!==void 0){const l=P(e,n.childPath),d=N(e,n);if(d===void 0)throw new Error(`Unknown ${n.kind} "${n.id}".`);a=i.filter(o=>{if(A(o.owner.childPath??[])!==A(n.childPath??[]))return!1;if(n.kind==="mark")return o.owner.id===n.id;const r=_(l,o.owner.id);return n.kind==="data"?r?.data===d.id:n.kind==="coordinate"?r?.coordinate===n.id:n.kind==="scale"?Object.values(r?.encoding??{}).some(p=>p?.scale===n.id||p?.dimensions?.some(f=>f.scale===n.id)):n.kind==="guide"?o.owner.kind==="guide"&&o.owner.id===n.id:!1})}const h=[];function m(l,d=[]){for(const o of Object.values(l.materializationConfigs.calculations??{}))if(M(o))for(const r of Object.values(o))h.push(d.length===0?r:{...r,childPath:d});for(const[o,r]of Object.entries(l.children))m(r,[...d,o])}m(e);const g=a.filter(l=>l.logicalDataItems===null).map(l=>l.owner);return S({version:1,views:a,calculations:h,checks:[{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}],findings:[],coverage:{partial:a.some(l=>l.checks.some(d=>d.status!=="passed")),unsupported:g}})}const X=new WeakMap;function sn(e,{target:n,grain:i="item"}={}){O(e);const t=typeof n=="object"?P(e,n.childPath):e,h=(typeof n=="object"?n.id:n)??he(t,{label:"mark items",predicate:o=>o.data!==void 0}).id;let m=X.get(t);m||X.set(t,m=new Map);const g=`${h}:${i}`;if(m.has(g))return m.get(g);const l=new Map;for(const[o,r]of Object.entries(t.graphicSpec.objects)){l.set(o,r);for(const p of r.items??[])l.set(p.id,{type:p.type??r.type,properties:p.properties})}const d=S(ee(t,h).flatMap(o=>Q(t,o,i).map(r=>{const p=r.graphicIds.map(c=>l.get(c)).filter(Boolean).map(c=>({type:c.type,properties:c.properties})),f=p.map(c=>{const s=c.properties;return s.commands?ne(s.commands):c.type==="circle"?{left:s.x-s.radius,top:s.y-s.radius,right:s.x+s.radius,bottom:s.y+s.radius}:c.type==="line"?{left:Math.min(s.x1,s.x2),top:Math.min(s.y1,s.y2),right:Math.max(s.x1,s.x2),bottom:Math.max(s.y1,s.y2)}:c.type==="rect"?{left:s.x,top:s.y,right:s.x+s.width,bottom:s.y+s.height}:ie({objects:{item:c},order:["item"]},["item"],t.materializationConfigs.textMetrics)}).filter(Boolean);return{key:r.key,target:r.layer,markType:r.markType,grain:i,fields:r.fields,channels:r.channels,properties:r.properties,rows:r.rows,geometry:p,bounds:f.length?{left:Math.min(...f.map(c=>c.left)),top:Math.min(...f.map(c=>c.top)),right:Math.max(...f.map(c=>c.right)),bottom:Math.max(...f.map(c=>c.bottom))}:null}})));return m.set(g,d),d}export{nn as comparePrograms,xe as describeAction,Se as getDatasetSchema,sn as getMarkItems,en as getPlotComposition,tn as inspectProgram};