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,12 +1,15 @@
1
1
  import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
2
- import {isContinuousColorScaleType} from './scales/types.js';
2
+ import {isContinuousColorScaleType,isDiscretizedColorScaleType} from './scales/types.js';
3
3
  import {isContinuousSizeScaleType} from './scales/size.js';
4
4
 
5
- export function isColorSizeChannels(channels) {
6
- return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
5
+ export function isColorSizeChannels(channels,combine='color-size') {
6
+ return Array.isArray(channels)&&(combine==='color-size-shape'
7
+ ? channels.length===3&&channels.includes('shape')&&channels.includes('size')&&['color','stroke'].some(c=>channels.includes(c))
8
+ : channels.length===2&&channels.includes('color')&&channels.includes('size'));
7
9
  }
8
10
 
9
- export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
11
+ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate,combine='color-size',channels}) {
12
+ if(combine==='color-size-shape')return resolveColorSizeShapeMapping({layer,scales,coordinate,channels});
10
13
  const color=layer?.encoding?.color,size=layer?.encoding?.size;
11
14
  const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
12
15
  if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
@@ -26,7 +29,20 @@ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
26
29
  return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
27
30
  }
28
31
 
29
- export function normalizeColorSizeSymbol(value,previous) {
32
+ export function normalizeColorSizeSymbol(value,previous,mapping) {
33
+ if(mapping?.combine==='color-size-shape') {
34
+ if(value===undefined)return {...(previous??{type:'point'})};
35
+ if(value==='auto')return {type:'point'};
36
+ validateOptionObject(value,['type','fill','stroke','strokeWidth','opacity'],'Color-size-shape symbol');
37
+ if(value.type!==undefined&&value.type!=='point')throw new Error('Combined symbols require type point.');
38
+ const symbol={type:'point',...previous,...value};
39
+ const mapped=mapping.colorChannel==='stroke'?'stroke':'fill';
40
+ if(Object.hasOwn(symbol,mapped))throw new Error(`Combined symbols cannot override mapped ${mapped}.`);
41
+ for(const key of ['fill','stroke'])if(symbol[key]!==undefined)validateNonEmptyString(symbol[key],`Combined symbol ${key}`);
42
+ if(symbol.strokeWidth!==undefined)validateNonNegativeFinite(symbol.strokeWidth,'Combined symbol strokeWidth');
43
+ if(symbol.opacity!==undefined)validateUnitInterval(symbol.opacity,'Combined symbol opacity');
44
+ return symbol;
45
+ }
30
46
  if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
31
47
  if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
32
48
  validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
@@ -37,3 +53,38 @@ export function normalizeColorSizeSymbol(value,previous) {
37
53
  validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
38
54
  return symbol;
39
55
  }
56
+
57
+ function resolveColorSizeShapeMapping({layer,scales,coordinate,channels}) {
58
+ const hues=channels===undefined?['color','stroke'].filter(c=>layer?.encoding?.[c]?.scale!==undefined)
59
+ :channels.filter(c=>['color','stroke'].includes(c));
60
+ if(hues.length!==1)throw new Error('Color-size-shape requires one color or stroke channel; select channels explicitly.');
61
+ const colorChannel=hues[0],hue=layer?.encoding?.[colorChannel],size=layer?.encoding?.size,shape=layer?.encoding?.shape;
62
+ const colorScale=scales[hue?.scale],sizeScale=scales[size?.scale],shapeScale=scales[shape?.scale];
63
+ if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
64
+ hue?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
65
+ !['nominal','ordinal'].includes(shape?.fieldType)||typeof hue.field!=='string'||
66
+ [size,shape].some(e=>e.field!==hue.field||e.data!==hue.data||e.aggregate!==hue.aggregate)||
67
+ !isDiscretizedColorScaleType(colorScale?.type)||!isDiscretizedColorScaleType(sizeScale?.type)||shapeScale?.type!=='ordinal') {
68
+ throw new Error('Color-size-shape requires one Cartesian point owner with the same field and grain on interval hue/size and ordinal shape.');
69
+ }
70
+ if(!sameOrderedValues(colorScale.thresholds,sizeScale.thresholds))throw new Error('Combined interval scales require identical threshold boundaries.');
71
+ return {target:layer.id,field:hue.field,fieldType:'quantitative',colorScale:hue.scale,sizeScale:size.scale,
72
+ shapeScale:shape.scale,colorChannel,domain:[...colorScale.domain]};
73
+ }
74
+
75
+ export function validateColorSizeShapeSamples(sampling,scale) {
76
+ const cuts=scale.thresholds;
77
+ if(sampling.mode!=='values'||sampling.values.length!==cuts.length+1||sampling.values.some((value,index)=>
78
+ !Number.isFinite(value)||index>0&&value<cuts[index-1]||index<cuts.length&&value>=cuts[index])) {
79
+ throw new Error('Color-size-shape requires one explicit finite value in each ordered threshold interval.');
80
+ }
81
+ return [...sampling.values];
82
+ }
83
+
84
+ export function colorSizeScaleIds(config) {
85
+ return [config.colorScale,config.sizeScale,...(config.shapeScale===undefined?[]:[config.shapeScale])];
86
+ }
87
+
88
+ export function colorSizeChannels(config) {
89
+ return [config.colorChannel??'color','size',...(config.shapeScale===undefined?[]:['shape'])];
90
+ }
@@ -91,3 +91,13 @@ export function resolveDisplayLabelColor(value, map, fallback) {
91
91
  const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
92
92
  return entry?.color ?? fallback;
93
93
  }
94
+
95
+ export function normalizeLegendDisplayLabelMap(descriptor, map, scaleType) {
96
+ if (["size", "colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
97
+ return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
98
+ }
99
+ if (descriptor.family !== "categorical" && !(descriptor.family === "discrete-size" && scaleType === "ordinal")) {
100
+ throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
101
+ }
102
+ return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
103
+ }
@@ -1 +1 @@
1
- import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\r\n\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title text must be one line.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
1
+ import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title line breaks must use LF, CR or CRLF.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
@@ -115,9 +115,16 @@ export function resolveFacetFamily(semanticSpec) {
115
115
  semanticSpec.layers.length === 0) {
116
116
  throw new Error("facet requires at least one materializable layer.");
117
117
  }
118
- const primary = semanticSpec.layers.filter(layer =>
119
- !(layer.mark?.type === "text" && layer.source !== undefined)
120
- );
118
+ // Literal positioned notes decorate a chart rather than define its data
119
+ // coordinate family. Keep validating/materializing them, but do not confuse
120
+ // a plot-space Cartesian annotation with a second Cartesian chart in a polar
121
+ // or parallel facet. A text-only chart still supplies its own primary layer.
122
+ const annotations = semanticSpec.layers.filter(layer => layer.mark?.type === 'text' &&
123
+ ['x', 'y', 'text'].every(channel => Object.hasOwn(layer.encoding?.[channel] ?? {}, 'datum')));
124
+ const dataLayers = semanticSpec.layers.filter(layer =>
125
+ !(layer.mark?.type === 'text' && layer.source !== undefined) && !annotations.includes(layer));
126
+ const primary = dataLayers.length ? dataLayers : annotations;
127
+ for (const note of annotations) classifyPrimaryLayer(semanticSpec, note);
121
128
  if (primary.length === 0) {
122
129
  throw new Error("facet requires at least one primary materializable layer.");
123
130
  }
@@ -1,3 +1,4 @@
1
+ import {colorSizeScaleIds} from "../colorSizeLegend.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
3
 
3
4
 
@@ -167,7 +168,7 @@ function requireLegendChildren(children) {
167
168
  }
168
169
 
169
170
  function scaleIds(config, kind) {
170
- const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
171
+ const ids = kind === "colorSize" ? colorSizeScaleIds(config)
171
172
  : config.scales ?? (config.scale === undefined ? [] : [config.scale]);
172
173
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
173
174
  typeof id === "string" && id.length > 0
@@ -1,4 +1,5 @@
1
1
  import { isPlainObject } from "../../core/immutable.js";
2
+ import { patchLegendItemsOffset } from "../legendItemsOffset.js";
2
3
  import {validateLegendSpacing} from "../legendSpacing.js";
3
4
  import { validateOptionObject, validateNonEmptyString } from "../../core/validation.js";
4
5
  import { validateUserId } from "../../core/identifiers.js";
@@ -8,12 +9,12 @@ import { validateLegendGroupState } from "../legendGroups.js";
8
9
  import { resolveSharedFacetLegends, sameFacetGuideValue as same } from "./guides.js";
9
10
  import { normalizeDisplayLabelMap } from "../displayLabels.js";
10
11
 
11
- const CONFIG_KEYS = Object.freeze([
12
+ const CONFIG_KEYS = Object.freeze(["visibility",
12
13
  "kind", "target", "channels", "scales", "scale", "field", "fieldType", "domain", "title", "titleVisible",
13
14
  "inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "position", "align", "offset", "direction", "columns",
14
15
  "titlePosition", "symbol", "labels", "titleStyle", "itemGap", "rowPadding", "titlePadding", "layout", "overflow", "border", "gradient",
15
16
  "count", "sampling", "representation", "gradientDirection", "inheritAppearance", "inheritPosition", "blockOverrides",
16
- "combine", "sizeAreas", "clipHeight", "defaultLabelMap"
17
+ "combine", "sizeAreas", "clipHeight", "defaultLabelMap", "itemsOffset"
17
18
  ]);
18
19
 
19
20
  export function isAuthoredFacetLegend(value) {
@@ -31,9 +32,10 @@ export function validateAuthoredFacetLegend(program, intent) {
31
32
  for (const [kind, block] of Object.entries(intent.blocks)) {
32
33
  if (!LEGEND_CONFIG_KINDS.includes(kind)) throw new Error("Stored parent legend has an unknown block kind.");
33
34
  validateOptionObject(block, ["config", "guide"], "Stored parent legend block");
34
- validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale"] : CONFIG_KEYS, "Stored parent legend config");
35
+ validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale", "shapeScale", "colorChannel"] : CONFIG_KEYS, "Stored parent legend config");
35
36
  const config = block.config;
36
37
  validateLegendSpacing(kind,config);
38
+ if (config.itemsOffset !== undefined) patchLegendItemsOffset({}, config.itemsOffset);
37
39
  if (config.defaultLabelMap !== undefined) normalizeDisplayLabelMap(config.defaultLabelMap, "Stored inferred legend labels");
38
40
  validateUserId(config.target, "Parent legend target");
39
41
  if (!LEGEND_POSITIONS.includes(config.position)) throw new Error("Stored parent legend requires a native position.");
@@ -42,9 +44,9 @@ export function validateAuthoredFacetLegend(program, intent) {
42
44
  }
43
45
  const descriptor = describeLegendBlock(program, kind, config);
44
46
  if (block.guide !== undefined) {
45
- validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "title"]
47
+ validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "shapeScale", "colorChannel", "title"]
46
48
  : ["scale", "scales", "channels", "title", "order"], "Stored parent legend meaning");
47
- if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale)) {
49
+ if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale || block.guide.shapeScale !== config.shapeScale || block.guide.colorChannel !== config.colorChannel)) {
48
50
  throw new Error("Stored parent color-size meaning must match both block scale bindings.");
49
51
  }
50
52
  if (block.guide.title !== undefined) validateNonEmptyString(block.guide.title, "Parent legend title");
@@ -1 +1 @@
1
- import{LEGEND_CHANNELS as d,LEGEND_CONFIG_KINDS as h}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as u}from"./scales/size.js";function y(e,r){if(!Array.isArray(e))throw new TypeError(`${r} channels must be an array.`);if(e.length===0)throw new Error(`${r} channels must select at least one channel.`);const s=new Set;for(const t of e){if(!d.includes(t))throw new Error(`Unsupported legend channel "${t}".`);if(s.has(t))throw new Error(`${r} channels contains duplicate "${t}".`);s.add(t)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(e,r)=>e<r?-1:e>r?1:0;function w(e,r){const s=e==="colorSize"?["color","size"]:o.includes(e)?r?.channels:a[e]===void 0?void 0:[a[e]];if(!Array.isArray(s)||s.length===0)throw new Error(`Legend block ${e} requires at least one channel.`);return y(s,`Legend block ${e}`),Object.freeze([...s].sort(c))}function p(e){if(!Array.isArray(e)||e.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...e].sort(c))}function b(e,r,s){if(!h.includes(r))throw new Error(`Unknown legend block kind "${r}".`);const t=w(r,s),n=r==="colorSize"?[s.colorScale,s.sizeScale]:o.includes(r)?s?.scales:[s?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||e.resolvedScales[l]===void 0))throw new Error(`Legend block ${r} requires resolved scales.`);const i=o.includes(r)?"categorical":r==="colorSize"?"sampled":r==="size"?u(e.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(r)||r==="gradient"&&s.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(r)?"gradient":"interval";return Object.freeze({target:s.target,kind:r,family:i,key:p(t),channels:t,scaleIds:Object.freeze([...n]),config:s})}function g(e,r){return e.symbol.layers.length===1&&(e.kind==="series"&&r==="line"||["color","stroke"].includes(e.kind)&&r==="swatch")?`${e.kind}LegendSymbols`:`${e.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[r]}`}export{g as categoricalLegendSymbolId,b as describeLegendBlock,w as legendBlockChannels,p as legendBlockKey,y as validateLegendChannels};
1
+ import{colorSizeChannels as d,colorSizeScaleIds as h}from"./colorSizeLegend.js";import{LEGEND_CHANNELS as u,LEGEND_CONFIG_KINDS as y}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as p}from"./scales/size.js";function w(r,e){if(!Array.isArray(r))throw new TypeError(`${e} channels must be an array.`);if(r.length===0)throw new Error(`${e} channels must select at least one channel.`);const t=new Set;for(const s of r){if(!u.includes(s))throw new Error(`Unsupported legend channel "${s}".`);if(t.has(s))throw new Error(`${e} channels contains duplicate "${s}".`);t.add(s)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(r,e)=>r<e?-1:r>e?1:0;function S(r,e){const t=r==="colorSize"?d(e):o.includes(r)?e?.channels:a[r]===void 0?void 0:[a[r]];if(!Array.isArray(t)||t.length===0)throw new Error(`Legend block ${r} requires at least one channel.`);return w(t,`Legend block ${r}`),Object.freeze([...t].sort(c))}function m(r){if(!Array.isArray(r)||r.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...r].sort(c))}function E(r,e,t){if(!y.includes(e))throw new Error(`Unknown legend block kind "${e}".`);const s=S(e,t),n=e==="colorSize"?h(t):o.includes(e)?t?.scales:[t?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||r.resolvedScales[l]===void 0))throw new Error(`Legend block ${e} requires resolved scales.`);const i=o.includes(e)?"categorical":e==="colorSize"?"sampled":e==="size"?p(r.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(e)||e==="gradient"&&t.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(e)?"gradient":"interval";return Object.freeze({target:t.target,kind:e,family:i,key:m(s),channels:s,scaleIds:Object.freeze([...n]),config:t})}function L(r,e){return r.symbol.layers.length===1&&(r.kind==="series"&&e==="line"||["color","stroke"].includes(r.kind)&&e==="swatch")?`${r.kind}LegendSymbols`:`${r.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[e]}`}export{L as categoricalLegendSymbolId,E as describeLegendBlock,S as legendBlockChannels,m as legendBlockKey,w as validateLegendChannels};
@@ -9,7 +9,7 @@ export function isSizeShapeChannels(channels) {
9
9
  }
10
10
  export function validateLegendCombine(value) {
11
11
  if(value===undefined)return;
12
- if (!['separate','size-shape','color-size'].includes(value)) throw new Error('Legend combine must be separate, size-shape, or color-size.');
12
+ if (!['separate','size-shape','color-size','color-size-shape'].includes(value)) throw new Error('Legend combine must be separate, size-shape, color-size, or color-size-shape.');
13
13
  }
14
14
  export function validateMappedSizeShapeSymbol(symbol) {
15
15
  if(symbol===undefined||symbol==='auto')return;
@@ -1 +1 @@
1
- import{validateOptionObject as o,validateNonNegativeFinite as d}from"../core/validation.js";import{validateUserId as l}from"../core/identifiers.js";import{PLOT_LEGEND_POSITIONS as i,LEGEND_POSITIONS as a,LEGEND_BLOCK_LAYOUTS as f}from"../core/vocabulary.js";function c(n){o(n,void 0,"Legend groups");for(const[t,r]of Object.entries(n)){if(l(t,"Legend group target"),o(r,["placement","blockLayout","inferredOffset"],"Legend group"),!f.includes(r.blockLayout))throw new Error("Legend group blockLayout must be row, column or wrap.");if(r.inferredOffset!==void 0&&typeof r.inferredOffset!="boolean")throw new TypeError("Legend group inferredOffset must be boolean.");const e=r.placement;if(o(e,["at","position","align","offset"],"Legend group placement"),Object.hasOwn(e,"at")){if(Object.keys(e).length!==1)throw new Error("Legend group at cannot coexist with an edge anchor.");if(o(e.at,["x","y"],"Legend group at"),!Number.isFinite(e.at.x)||!Number.isFinite(e.at.y))throw new Error("Legend group at requires finite x and y.")}else{if(Object.keys(e).length!==3||!a.includes(e.position))throw new Error("Legend group requires a complete edge placement.");if(!["left","center","right"].includes(e.align))throw new Error("Unsupported legend group alignment.");if((i.includes(e.position)||["left","right"].includes(e.position))&&e.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");d(e.offset,"Legend group offset")}}return n}function s(n){const t=Object.values(n?.blockOverrides??{}).map(r=>r.position).filter(r=>r!==void 0);if(t.length>1)throw new Error("Legend family has ambiguous local positions.");if(t.length&&!a.includes(t[0]))throw new Error("Invalid local legend position.");return t[0]}function m(n){const t=s(n);if(t!==void 0)return{position:t,align:["top","bottom"].includes(t)?n.align??"center":"center",offset:i.includes(t)?8:n.offset??(n.channels===void 0?30:8),inferredOffset:n.inferredOffset??!0}}export{m as localLegendPlacement,s as localLegendPosition,c as validateLegendGroupState};
1
+ import{validateOptionObject as o,validateNonNegativeFinite as l}from"../core/validation.js";import{validateUserId as d}from"../core/identifiers.js";import{PLOT_LEGEND_POSITIONS as i,LEGEND_POSITIONS as f,LEGEND_BLOCK_LAYOUTS as a}from"../core/vocabulary.js";function c(e){o(e,void 0,"Legend groups");for(const[r,n]of Object.entries(e)){if(d(r,"Legend group target"),o(n,["placement","blockLayout","inferredOffset"],"Legend group"),!a.includes(n.blockLayout))throw new Error("Legend group blockLayout must be row, column or wrap.");if(n.inferredOffset!==void 0&&typeof n.inferredOffset!="boolean")throw new TypeError("Legend group inferredOffset must be boolean.");const t=n.placement;if(o(t,["at","position","align","offset","alongOffset"],"Legend group placement"),Object.hasOwn(t,"at")){if(Object.keys(t).length!==1)throw new Error("Legend group at cannot coexist with an edge anchor.");if(o(t.at,["x","y"],"Legend group at"),!Number.isFinite(t.at.x)||!Number.isFinite(t.at.y))throw new Error("Legend group at requires finite x and y.")}else{if(![3,4].includes(Object.keys(t).length)||!f.includes(t.position))throw new Error("Legend group requires a complete edge placement.");if(!["left","center","right"].includes(t.align))throw new Error("Unsupported legend group alignment.");if((i.includes(t.position)||["left","right"].includes(t.position))&&t.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");if(l(t.offset,"Legend group offset"),t.alongOffset!==void 0&&!Number.isFinite(t.alongOffset))throw new TypeError("Legend alongOffset must be finite.")}}return e}function s(e){for(const n of Object.values(e?.blockOverrides??{}))if(n.alongOffset!==void 0&&!Number.isFinite(n.alongOffset))throw new TypeError("Legend alongOffset must be finite.");const r=Object.values(e?.blockOverrides??{}).map(n=>n.position??(n.alongOffset===void 0?void 0:e.position)).filter(n=>n!==void 0);if(r.length>1)throw new Error("Legend family has ambiguous local positions.");if(r.length&&!f.includes(r[0]))throw new Error("Invalid local legend position.");return r[0]}function O(e){const r=s(e);if(r===void 0)return;const n=Object.values(e.blockOverrides??{}).flatMap(t=>t.alongOffset===void 0?[]:[t.alongOffset]);if(n.length>1||n.some(t=>!Number.isFinite(t)))throw new TypeError("Legend alongOffset requires one finite block value.");return{position:r,...n.length?{alongOffset:n[0]}:{},align:["top","bottom"].includes(r)?e.align??"center":"center",offset:i.includes(r)&&r!==e.position&&e.inferredOffset===!0?8:e.offset??(i.includes(r)?8:e.channels===void 0?30:8),inferredOffset:e.inferredOffset??!0}}function m(e,r=e.position){return s(e)===r&&Object.values(e.blockOverrides??{}).some(n=>n.alongOffset!==void 0)}export{O as localLegendPlacement,s as localLegendPosition,m as retainsLegendSlot,c as validateLegendGroupState};
@@ -0,0 +1,12 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+
3
+ export function patchLegendItemsOffset(config, value) {
4
+ const { itemsOffset: previous, ...rest } = config;
5
+ if (value === "auto") return rest;
6
+ if (!isPlainObject(value) || !Object.keys(value).length ||
7
+ Object.keys(value).some(key => !["x", "y"].includes(key) ||
8
+ typeof value[key] !== "number" || !Number.isFinite(value[key]))) {
9
+ throw new TypeError('Legend itemsOffset must be a nonempty object with finite x/y coordinates, or "auto".');
10
+ }
11
+ return { ...rest, itemsOffset: { x: 0, y: 0, ...previous, ...value } };
12
+ }
@@ -0,0 +1,25 @@
1
+ import { validateOptionObject } from "../core/validation.js";
2
+
3
+ export function normalizeLegendOverflow(value, { summarySymbol = false } = {}) {
4
+ if (value === undefined || value === false) return value;
5
+ validateOptionObject(value, ["maxItems", "summary", "summarySymbol"], "Legend overflow");
6
+ if (!Number.isInteger(value.maxItems) || value.maxItems < 1) {
7
+ throw new RangeError("Legend overflow.maxItems must be a positive integer.");
8
+ }
9
+ if (value.summary !== undefined && value.summary !== "ellipsis-count") {
10
+ throw new Error("Legend overflow.summary must be ellipsis-count.");
11
+ }
12
+ if (value.summarySymbol !== undefined && (!summarySymbol || value.summarySymbol !== "first-hidden")) {
13
+ throw new Error("Legend overflow.summarySymbol requires first-hidden on categorical line samples.");
14
+ }
15
+ return { maxItems: value.maxItems, summary: "ellipsis-count",
16
+ ...(value.summarySymbol === undefined ? {} : { summarySymbol: value.summarySymbol }) };
17
+ }
18
+
19
+ export function validateLegendOverflow(kind, config) {
20
+ normalizeLegendOverflow(config.overflow, { summarySymbol: ["series", "color", "stroke"].includes(kind) });
21
+ if (config.overflow?.summarySymbol !== undefined &&
22
+ (!config.symbol?.layers?.length || config.symbol.layers.some(layer => layer.type !== "line"))) {
23
+ throw new Error("Legend overflow.summarySymbol requires a categorical line-only sample recipe.");
24
+ }
25
+ }
@@ -1 +1 @@
1
- import{cloneAndFreeze as $}from"../core/immutable.js";import{readLinePosition as R,validateLineMissingScope as N,orderLineGapValues as X,splitLineGapValues as j}from"./lineMissing.js";import{readNominalField as U,readQuantitativeField as k,readTemporalField as P}from"./scales/index.js";import{aggregateRows as D,isAggregate as C,validateAggregateFieldType as Q,validateAggregateFieldValues as Y}from"./aggregate.js";import{stableOrderPathValues as _}from"./pathOrder.js";import{finiteMidpoint as J}from"./numeric.js";import{findHistogramBinIndex as W,resolveHistogramBins as Z}from"./histogram.js";import{validatePathSeriesAppearance as I,pathSeriesKey as H}from"./pathSeries.js";function ee(e){if(e?.mark?.type!=="line")throw new Error("Line series derivation requires a semantic line mark.");const n=e.encoding?.x,i=e.encoding?.y;if(n===void 0)throw new Error(`Line mark "${e.id}" requires an x encoding.`);const a=n?.fieldType==="quantitative"&&n.bin!==void 0&&C(i?.aggregate),r=["temporal","nominal","ordinal"].includes(n?.fieldType)&&C(i?.aggregate)||a,s=["quantitative","nominal","ordinal"].includes(n?.fieldType)&&n.bin===void 0&&i?.fieldType==="quantitative"&&i.aggregate===void 0,c=i?.aggregate===void 0&&(n?.fieldType==="temporal"&&i?.fieldType==="quantitative"||n?.fieldType==="quantitative"&&i?.fieldType==="temporal");if(!r&&!s&&!c)throw n?.fieldType==="temporal"?new Error(`Line mark "${e.id}" requires a supported aggregate y encoding.`):new Error(`Line mark "${e.id}" requires categorical/temporal aggregate or compatible direct x/y encodings.`);return r&&Q(i.aggregate,i.fieldType),{x:n,y:i,isAggregate:r,binnedAggregate:a,directRows:c||s,directTemporal:c}}function q(e,n,i){const a=H(i);let r=e.get(a);return r===void 0&&(r={key:Object.fromEntries(n.map((s,c)=>[s,i[c]])),values:[]},e.set(a,r)),r}function A(e,n){const i=e.flatMap(r=>r.values.map(s=>s.x)),a=new Set(i);if(n!==void 0){const r=new Set(n);if(i.some(s=>!r.has(s)))throw new Error("Line categorical x domain must contain every observed vertex.")}return $({xValues:n===void 0?i:n.filter(r=>a.has(r)),yValues:e.flatMap(r=>r.values.map(s=>s.y)),series:e})}function ne(e,n,i={}){const a=n?.encoding?.x;if(!(n?.mark?.type!=="line"||a?.fieldType!=="quantitative"||a.bin===void 0))return Z({values:k(e,a.field),bin:a.bin,domain:i.domain??"auto",nice:i.nice??!0,zero:i.zero??!1})}function ie(e,n,i={}){const{x:a,y:r,isAggregate:s,binnedAggregate:c,directRows:M,directTemporal:V}=ee(n);if(n.mark.missing==="break"){const t=R(e,a),d=R(e,r),f=I(e,n,i.scales),o=n.encoding?.pathOrder,g=o===void 0?V&&r.fieldType==="temporal"?d:t:k(e,o.field),m=new Map;for(let l=0;l<e.length;l+=1){const L=q(m,f,f.map(O=>e[l][O]));L.values.push({x:t[l],y:d[l],sourceIndex:l}),(L.orderValues??=[]).push(g[l])}const v=[...m.values()].flatMap(l=>{const L=X(l.values,l.orderValues,o?.order);return j(L).map(z=>({key:l.key,values:z.map(({sourceIndex:G,...K})=>K),sourceIndices:z.map(G=>G.sourceIndex)}))});return $({xValues:t.filter(l=>l!==null),yValues:d.filter(l=>l!==null),series:v})}s&&Y(e,r.field,r.fieldType);const y=["nominal","ordinal"].includes(a.fieldType),h=y?U(e,a.field):a.fieldType==="temporal"?P(e,a.field,a.temporalUnit):k(e,a.field),u=s?void 0:r.fieldType==="temporal"?P(e,r.field,r.temporalUnit):k(e,r.field),p=y?i.xDomain??[...new Set(h)]:void 0,w=new Map(p?.map((t,d)=>[t,d])),x=y?(t,d)=>w.get(t.x)-w.get(d.x):(t,d)=>t.x-d.x,T=I(e,n,i.scales),b=c?i.xBinBoundaries??ne(e,n).boundaries:void 0,S=n.encoding?.pathOrder;if(S!==void 0){if(s)throw new Error(`Line mark "${n.id}" cannot combine aggregate y and path order.`);const t=k(e,S.field),d=new Map;for(let o=0;o<e.length;o+=1){const g=T.map(v=>e[o][v]),m=q(d,T,g);m.values.push({x:h[o],y:u[o]}),m.orderValues??=[],m.orderValues.push(t[o])}const f=[...d.values()].flatMap(o=>{const g=_(o.values,o.orderValues,S.order);return[{key:o.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two ordered points.`);return A(f,p)}if(M){const t=new Map;for(let o=0;o<e.length;o+=1){const g=T.map(v=>e[o][v]);q(t,T,g).values.push({x:h[o],y:u[o]})}const d=V&&r.fieldType==="temporal"?"y":"x",f=[...t.values()].flatMap(o=>{const g=o.values.sort(d==="x"?x:(m,v)=>m.y-v.y);return[{key:o.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two direct points.`);return A(f,p)}const E=new Map;for(let t=0;t<e.length;t+=1){const d=T.map(v=>e[t][v]),f=c?W(h[t],b):void 0;if(f===-1)continue;const o=c?J(b[f],b[f+1]):h[t],g=H([c?f:o,...d]),m=E.get(g)??{x:o,dimensions:d,rows:[]};m.rows.push(e[t]),E.set(g,m)}if(E.size===0)throw new Error(`Line mark "${n.id}" has no values to aggregate.`);const F=new Map;for(const t of E.values()){const d=D(t.rows,r.field,r.aggregate);if(d===void 0)continue;q(F,T,t.dimensions).values.push({x:t.x,y:d})}const B=[...F.values()].flatMap(t=>{const d=t.values.sort(x);return d.length<(i.allowSingleton===!0?1:2)?[]:[{key:t.key,values:d}]});if(B.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two aggregate points.`);return A(B,p)}function re(e){if(e?.mark?.type!=="line")throw new Error("Polar line series derivation requires a semantic line mark.");const n=e.encoding?.theta,i=e.encoding?.radius;if(n===void 0||i===void 0)throw new Error(`Polar line mark "${e.id}" requires theta and radius encodings.`);if(!["quantitative","temporal","ordinal","nominal"].includes(n.fieldType))throw new Error(`Polar line mark "${e.id}" has unsupported theta field type.`);if(i.fieldType!=="quantitative")throw new Error(`Polar line mark "${e.id}" requires a quantitative radius encoding.`);return{theta:n,radius:i}}function te(e,n){return["nominal","ordinal"].includes(n.fieldType)?U(e,n.field):n.fieldType==="temporal"?P(e,n.field,n.temporalUnit):k(e,n.field)}function ae(e,n,i){if(!["nominal","ordinal"].includes(n))return e;const a=i??[...new Set(e)],r=new Map(a.map((s,c)=>[s,c]));for(const s of e)if(!r.has(s))throw new Error(`Polar line theta domain does not contain "${s}".`);return e.map(s=>r.get(s))}function oe(e,n,{thetaDomain:i}={}){if(n.encoding?.pathOrder!==void 0)throw new Error(`Polar line mark "${n.id}" does not support path order.`);const{theta:a,radius:r}=re(n),s=te(e,a),c=k(e,r.field),M=ae(s,a.fieldType,i),V=I(e,n),y=new Map;for(let u=0;u<e.length;u+=1){const p=V.map(x=>e[u][x]);q(y,V,p).values.push({theta:s[u],radius:c[u],order:M[u],sourceIndex:u})}const h=[...y.values()].flatMap(u=>{const p=u.values.sort((w,x)=>w.order-x.order||w.sourceIndex-x.sourceIndex);return p.length<2?[]:[{key:u.key,values:p}]});if(h.length===0)throw new Error(`Polar line series on mark "${n.id}" requires at least two points.`);return $({thetaFieldType:a.fieldType,thetaValues:h.flatMap(u=>u.values.map(p=>p.theta)),radiusValues:h.flatMap(u=>u.values.map(p=>p.radius)),series:h})}function me(e,n,i){return N(n),n?.encoding?.theta!==void 0||n?.encoding?.radius!==void 0?oe(e,n,i):ie(e,n,i)}export{me as deriveLineSeries,oe as derivePolarLineSeries,ne as resolveLineBins};
1
+ import{cloneAndFreeze as M}from"../core/immutable.js";import{readLinePosition as D,validateLineMissingScope as N,orderLineGapValues as X,splitLineGapValues as j}from"./lineMissing.js";import{readNominalField as H,readQuantitativeField as k,readTemporalField as P}from"./scales/index.js";import{aggregateRows as Q,isAggregate as R,validateAggregateFieldType as Y,validateAggregateFieldValues as _}from"./aggregate.js";import{stableOrderPathValues as J}from"./pathOrder.js";import{finiteMidpoint as W}from"./numeric.js";import{findHistogramBinIndex as Z,resolveHistogramBins as ee}from"./histogram.js";import{validatePathSeriesAppearance as A,pathSeriesKey as U}from"./pathSeries.js";function ne(e){return["quantitative","temporal","nominal","ordinal"].includes(e?.fieldType)}function ie(e){if(e?.mark?.type!=="line")throw new Error("Line series derivation requires a semantic line mark.");const n=e.encoding?.x,i=e.encoding?.y;if(n===void 0)throw new Error(`Line mark "${e.id}" requires an x encoding.`);if(n.aggregate!==void 0)throw new Error("A Line requires one independent position; both axes cannot aggregate.");const o=n?.fieldType==="quantitative"&&n.bin!==void 0&&R(i?.aggregate),t=ne(n)&&R(i?.aggregate),r=["quantitative","nominal","ordinal"].includes(n?.fieldType)&&n.bin===void 0&&i?.fieldType==="quantitative"&&i.aggregate===void 0,u=i?.aggregate===void 0&&(n?.fieldType==="temporal"&&i?.fieldType==="quantitative"||n?.fieldType==="quantitative"&&i?.fieldType==="temporal");if(!t&&!r&&!u)throw n?.fieldType==="temporal"?new Error(`Line mark "${e.id}" requires a supported aggregate y encoding.`):new Error(`Line mark "${e.id}" requires an aggregate value at compatible x positions or compatible direct x/y encodings.`);return t&&Y(i.aggregate,i.fieldType),{x:n,y:i,isAggregate:t,binnedAggregate:o,directRows:u||r,directTemporal:u}}function L(e,n,i){const o=U(i);let t=e.get(o);return t===void 0&&(t={key:Object.fromEntries(n.map((r,u)=>[r,i[u]])),values:[]},e.set(o,t)),t}function B(e,n){const i=e.flatMap(t=>t.values.map(r=>r.x)),o=new Set(i);if(n!==void 0){const t=new Set(n);if(i.some(r=>!t.has(r)))throw new Error("Line categorical x domain must contain every observed vertex.")}return M({xValues:n===void 0?i:n.filter(t=>o.has(t)),yValues:e.flatMap(t=>t.values.map(r=>r.y)),series:e})}function te(e,n,i={}){const o=n?.encoding?.x;if(!(n?.mark?.type!=="line"||o?.fieldType!=="quantitative"||o.bin===void 0))return ee({values:k(e,o.field),bin:o.bin,domain:i.domain??"auto",nice:i.nice??!0,zero:i.zero??!1})}function C(e,n,i={}){const{x:o,y:t,isAggregate:r,binnedAggregate:u,directRows:v,directTemporal:q}=ie(n);if(n.mark.missing==="break"){const a=D(e,o),d=D(e,t),f=A(e,n,i.scales),s=n.encoding?.pathOrder,g=s===void 0?q&&t.fieldType==="temporal"?d:a:k(e,s.field),m=new Map;for(let c=0;c<e.length;c+=1){const b=L(m,f,f.map(z=>e[c][z]));b.values.push({x:a[c],y:d[c],sourceIndex:c}),(b.orderValues??=[]).push(g[c])}const x=[...m.values()].flatMap(c=>{const b=X(c.values,c.orderValues,s?.order);return j(b).map(O=>({key:c.key,values:O.map(({sourceIndex:G,...K})=>K),sourceIndices:O.map(G=>G.sourceIndex)}))});return M({xValues:a.filter(c=>c!==null),yValues:d.filter(c=>c!==null),series:x})}r&&_(e,t.field,t.fieldType);const w=["nominal","ordinal"].includes(o.fieldType),h=w?H(e,o.field):o.fieldType==="temporal"?P(e,o.field,o.temporalUnit):k(e,o.field),l=r?void 0:t.fieldType==="temporal"?P(e,t.field,t.temporalUnit):k(e,t.field),p=w?i.xDomain??[...new Set(h)]:void 0,T=new Map(p?.map((a,d)=>[a,d])),y=w?(a,d)=>T.get(a.x)-T.get(d.x):(a,d)=>a.x-d.x,V=A(e,n,i.scales),S=u?i.xBinBoundaries??te(e,n).boundaries:void 0,$=n.encoding?.pathOrder;if($!==void 0){if(r)throw new Error(`Line mark "${n.id}" cannot combine aggregate y and path order.`);const a=k(e,$.field),d=new Map;for(let s=0;s<e.length;s+=1){const g=V.map(x=>e[s][x]),m=L(d,V,g);m.values.push({x:h[s],y:l[s]}),m.orderValues??=[],m.orderValues.push(a[s])}const f=[...d.values()].flatMap(s=>{const g=J(s.values,s.orderValues,$.order);return[{key:s.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two ordered points.`);return B(f,p)}if(v){const a=new Map;for(let s=0;s<e.length;s+=1){const g=V.map(x=>e[s][x]);L(a,V,g).values.push({x:h[s],y:l[s]})}const d=q&&t.fieldType==="temporal"?"y":"x",f=[...a.values()].flatMap(s=>{const g=s.values.sort(d==="x"?y:(m,x)=>m.y-x.y);return[{key:s.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two direct points.`);return B(f,p)}const E=new Map;for(let a=0;a<e.length;a+=1){const d=V.map(x=>e[a][x]),f=u?Z(h[a],S):void 0;if(f===-1)continue;const s=u?W(S[f],S[f+1]):h[a],g=U([u?f:s,...d]),m=E.get(g)??{x:s,dimensions:d,rows:[]};m.rows.push(e[a]),E.set(g,m)}if(E.size===0)throw new Error(`Line mark "${n.id}" has no values to aggregate.`);const I=new Map;for(const a of E.values()){const d=Q(a.rows,t.field,t.aggregate);if(d===void 0)continue;L(I,V,a.dimensions).values.push({x:a.x,y:d})}const F=[...I.values()].flatMap(a=>{const d=a.values.sort(y);return d.length<(i.allowSingleton===!0?1:2)?[]:[{key:a.key,values:d}]});if(F.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two aggregate points.`);return B(F,p)}function re(e){if(e?.mark?.type!=="line")throw new Error("Polar line series derivation requires a semantic line mark.");const n=e.encoding?.theta,i=e.encoding?.radius;if(n===void 0||i===void 0)throw new Error(`Polar line mark "${e.id}" requires theta and radius encodings.`);if(!["quantitative","temporal","ordinal","nominal"].includes(n.fieldType))throw new Error(`Polar line mark "${e.id}" has unsupported theta field type.`);if(i.fieldType!=="quantitative")throw new Error(`Polar line mark "${e.id}" requires a quantitative radius encoding.`);return{theta:n,radius:i}}function ae(e,n){return["nominal","ordinal"].includes(n.fieldType)?H(e,n.field):n.fieldType==="temporal"?P(e,n.field,n.temporalUnit):k(e,n.field)}function oe(e,n,i){if(!["nominal","ordinal"].includes(n))return e;const o=i??[...new Set(e)],t=new Map(o.map((r,u)=>[r,u]));for(const r of e)if(!t.has(r))throw new Error(`Polar line theta domain does not contain "${r}".`);return e.map(r=>t.get(r))}function se(e,n,{thetaDomain:i}={}){if(n.encoding?.pathOrder!==void 0)throw new Error(`Polar line mark "${n.id}" does not support path order.`);const{theta:o,radius:t}=re(n),r=ae(e,o),u=k(e,t.field),v=oe(r,o.fieldType,i),q=A(e,n),w=new Map;for(let l=0;l<e.length;l+=1){const p=q.map(y=>e[l][y]);L(w,q,p).values.push({theta:r[l],radius:u[l],order:v[l],sourceIndex:l})}const h=[...w.values()].flatMap(l=>{const p=l.values.sort((T,y)=>T.order-y.order||T.sourceIndex-y.sourceIndex);return p.length<2?[]:[{key:l.key,values:p}]});if(h.length===0)throw new Error(`Polar line series on mark "${n.id}" requires at least two points.`);return M({thetaFieldType:o.fieldType,thetaValues:h.flatMap(l=>l.values.map(p=>p.theta)),radiusValues:h.flatMap(l=>l.values.map(p=>p.radius)),series:h})}function de(e){return e?.encoding?.x?.aggregate!==void 0||e?.encoding?.x?.fieldType==="quantitative"&&["nominal","ordinal"].includes(e?.encoding?.y?.fieldType)}function xe(e,n,i={}){if(N(n),n?.encoding?.theta!==void 0||n?.encoding?.radius!==void 0)return se(e,n,i);if(!de(n))return C(e,n,i);const t={...n,encoding:{...n.encoding,x:n.encoding.y,y:n.encoding.x}},r=C(e,t,{...i,xDomain:i.yDomain,xBinBoundaries:i.yBinBoundaries});return M({...r,xValues:r.yValues,yValues:r.xValues,series:r.series.map(u=>({...u,values:u.values.map(v=>({...v,x:v.y,y:v.x}))}))})}export{xe as deriveLineSeries,se as derivePolarLineSeries,ne as isAggregateLinePosition,de as isHorizontalLine,te as resolveLineBins};
@@ -43,6 +43,7 @@ const COMPARISON_OPERATORS = new Set([
43
43
  const ORDERED_COMPARISON_OPERATORS = new Set(["gt", "gte", "lt", "lte"]);
44
44
  const RANK_OPERATORS = new Set(["min", "max"]);
45
45
  const SELECTOR_KEYS = Object.freeze([
46
+ "rows",
46
47
  "grain",
47
48
  "field",
48
49
  "channel",
@@ -98,6 +99,16 @@ export function normalizeMarkSelector(selector) {
98
99
  throw new Error(`Unknown mark selector option "${unknown}".`);
99
100
  }
100
101
 
102
+ if (selector.rows !== undefined) {
103
+ if (Object.keys(selector).some(key => !["rows", "grain"].includes(key)) || !isPlainObject(selector.rows) ||
104
+ Object.keys(selector.rows).some(key => !["data", "indices"].includes(key)) ||
105
+ typeof selector.rows.data !== "string" || !selector.rows.data || !Array.isArray(selector.rows.indices) ||
106
+ !Array.from(selector.rows.indices).every(i => Number.isInteger(i) && i >= 0)) throw new TypeError("Row selection requires data and non-negative integer indices.");
107
+ const grain = selector.grain ?? "item";
108
+ if (!MARK_SELECTOR_GRAINS.includes(grain)) throw new Error(`Unknown mark selector grain "${grain}".`);
109
+ return cloneAndFreeze({ grain, rows: { data: selector.rows.data, indices: [...new Set(selector.rows.indices)] } });
110
+ }
111
+
101
112
  const sources = ["field", "channel", "property"].filter(key =>
102
113
  Object.hasOwn(selector, key)
103
114
  );
@@ -298,6 +309,10 @@ function rankGroup(items, selector) {
298
309
  export function selectMarkItemKeys(items, selector) {
299
310
  validateItems(items);
300
311
  const normalized = normalizeMarkSelector(selector);
312
+ if (normalized.rows) {
313
+ const indices = new Set(normalized.rows.indices);
314
+ return cloneAndFreeze(items.filter(item => item.rows?.some(ref => ref.data === normalized.rows.data && indices.has(ref.index))).map(item => item.key));
315
+ }
301
316
  if (!RANK_OPERATORS.has(normalized.op)) {
302
317
  return cloneAndFreeze(
303
318
  items
@@ -324,3 +339,11 @@ export function selectMarkItemKeys(items, selector) {
324
339
  )
325
340
  );
326
341
  }
342
+
343
+ // Stored selectors and live materialization share dataset identity and bounds checks.
344
+ export function validateRowSelectionDataset(rows, dataset) {
345
+ if (!dataset) throw new Error(`Unknown row dataset "${rows.data}".`);
346
+ if (rows.indices.some(index => index >= dataset.values.length)) {
347
+ throw new RangeError("Selected row index exceeds the dataset length.");
348
+ }
349
+ }
@@ -0,0 +1,51 @@
1
+ import { isPlainObject, cloneAndFreeze } from "../core/immutable.js";
2
+ import { validateUserId } from "../core/identifiers.js";
3
+
4
+ // Inspection and materialization share the same topology capability. A
5
+ // categorical density is an entire silhouette, not one endpoint per category.
6
+ export function overlaySourceReason(layer, coordinateType, density) {
7
+ if (coordinateType !== 'cartesian' || !['bar', 'point', 'line', 'area'].includes(layer?.mark?.type))
8
+ return 'Overlay source requires a Cartesian bar, point, line, or area mark.';
9
+ if (layer.derivedBindings?.overlay) return 'Overlay source must be an independent mark.';
10
+ if (['x', 'y'].some(channel => !layer.encoding?.[channel]?.scale)) return 'Overlay source requires complete x and y positions.';
11
+ if (density?.placement?.type === 'category') return 'Categorically placed density areas do not have a single result endpoint per category.';
12
+ return undefined;
13
+ }
14
+
15
+ export function normalizeOverlayBinding(input) {
16
+ if (!isPlainObject(input) || Object.keys(input).some(key =>
17
+ !["source", "basis", "mode", "series", "missing", "orderBy", "inheritColor"].includes(key))) {
18
+ throw new TypeError("Overlay binding requires source, basis, mode, series, missing, and optional orderBy.");
19
+ }
20
+ validateUserId(input.source, "Overlay source");
21
+ if (input.basis !== "visibleItems" || input.mode !== "follow") {
22
+ throw new Error('Overlay binding requires basis "visibleItems" and mode "follow".');
23
+ }
24
+ if (!["source", "total"].includes(input.series)) throw new Error("Overlay series must be source or total.");
25
+ if (!["break", "connect"].includes(input.missing)) throw new Error("Overlay missing must be break or connect.");
26
+ if (typeof input.inheritColor !== "boolean") throw new TypeError("Overlay inheritColor must be boolean.");
27
+ if (input.orderBy !== undefined && !["x", "y"].includes(input.orderBy)) {
28
+ throw new Error("Overlay orderBy must be x or y.");
29
+ }
30
+ return cloneAndFreeze(input);
31
+ }
32
+
33
+ export function overlayGroupFields(layer) {
34
+ const group = layer.encoding?.group;
35
+ if (group?.fields) return group.fields;
36
+ if (group?.field) return [group.field];
37
+ const color = layer.encoding?.color;
38
+ return ["nominal", "ordinal"].includes(color?.fieldType) &&
39
+ ![layer.encoding?.x?.field, layer.encoding?.y?.field].includes(color.field)
40
+ ? [color.field] : [];
41
+ }
42
+
43
+ export function overlayPositionEncoding(source, channel) {
44
+ const encoding = source.encoding[channel];
45
+ return encoding?.field === undefined && source.encoding[`${channel}2`]?.field !== undefined
46
+ ? source.encoding[`${channel}2`] : encoding;
47
+ }
48
+
49
+ export function assertIndependentOverlayGeometry(layer, operation) {
50
+ if (layer.derivedBindings?.overlay) throw new Error(`${operation}: overlay geometry follows its source; edit the source or detach the overlay first.`);
51
+ }