ggaction 0.0.20 → 0.0.22

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 (254) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +3 -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/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/interval.js +4 -3
  165. package/src/grammar/jitter.js +2 -2
  166. package/src/grammar/legendSpacing.js +45 -0
  167. package/src/grammar/lineMissing.js +90 -0
  168. package/src/grammar/lineSeries.js +1 -376
  169. package/src/grammar/lookup.js +97 -0
  170. package/src/grammar/markFilter.js +4 -2
  171. package/src/grammar/markLabels.js +8 -3
  172. package/src/grammar/numeric.js +1 -1
  173. package/src/grammar/pathCommands.js +1 -1
  174. package/src/grammar/pathSeries.js +1 -109
  175. package/src/grammar/pointPolygon.js +81 -0
  176. package/src/grammar/pointShapes.js +15 -15
  177. package/src/grammar/rects.js +1 -1
  178. package/src/grammar/regression/derive.js +1 -195
  179. package/src/grammar/scales/appearance.js +1 -1
  180. package/src/grammar/scales/color.js +1 -1
  181. package/src/grammar/scales/colorConsumers.js +1 -63
  182. package/src/grammar/scales/continuous.js +58 -1
  183. package/src/grammar/scales/definition.js +1 -1
  184. package/src/grammar/scales/ordinal.js +5 -2
  185. package/src/grammar/scales/policies.js +3 -3
  186. package/src/grammar/schemas/graphicBounds.js +40 -12
  187. package/src/grammar/schemas/graphicClip.js +23 -0
  188. package/src/grammar/schemas/graphicTree.js +16 -0
  189. package/src/grammar/statisticalReference.js +22 -0
  190. package/src/grammar/text.js +65 -1
  191. package/src/grammar/ticks.js +92 -1
  192. package/src/grammar/transformTopology.js +3 -1
  193. package/src/grammar/transforms.js +24 -0
  194. package/src/inspection.js +1 -495
  195. package/src/inspectionDescriptors.js +1 -1
  196. package/src/layout/canvas.js +1 -1
  197. package/src/layout/facetContentFlow.js +101 -0
  198. package/src/layout/facetHeaderTitles.js +1 -0
  199. package/src/layout/facetTopLane.js +29 -0
  200. package/src/layout/facets.js +1 -1
  201. package/src/layout/legendGroup.js +1 -1
  202. package/src/layout/legendItems.js +26 -10
  203. package/src/layout/legendSpacing.js +66 -0
  204. package/src/layout/plotDrawOrder.js +17 -0
  205. package/src/materialization/bars/histogram.js +8 -2
  206. package/src/materialization/bars/resolve.js +1 -1
  207. package/src/materialization/composition.js +1 -0
  208. package/src/materialization/compositionSnapshot.js +22 -2
  209. package/src/materialization/dataProvenance.js +1 -1
  210. package/src/materialization/facetGuides/layout.js +1 -1
  211. package/src/materialization/facetGuides/placement.js +23 -11
  212. package/src/materialization/facetGuides/preparation.js +43 -24
  213. package/src/materialization/facetHeaders.js +1 -1
  214. package/src/materialization/facetTopLane.js +25 -0
  215. package/src/materialization/facets.js +1 -1
  216. package/src/materialization/graphicHierarchy.js +5 -0
  217. package/src/materialization/graphicTranslation.js +2 -0
  218. package/src/materialization/guides/layout.js +1 -1
  219. package/src/materialization/guides/resources.js +1 -1
  220. package/src/materialization/legends.js +1 -1
  221. package/src/materialization/markClip.js +26 -0
  222. package/src/materialization/marks/capabilities.js +3 -2
  223. package/src/materialization/marks/grouping.js +7 -5
  224. package/src/materialization/marks/index.js +4 -2
  225. package/src/materialization/marks/policies.js +4 -0
  226. package/src/materialization/plotDrawOrder.js +44 -0
  227. package/src/materialization/rect.js +11 -3
  228. package/src/materialization/scaleGuideDependencies.js +13 -5
  229. package/src/materialization/selection/items/common.js +1 -1
  230. package/src/materialization/selection/items/path.js +1 -0
  231. package/src/materialization/selection/policies/index.js +2 -1
  232. package/src/materialization/text.js +10 -5
  233. package/src/mcp/cli.js +0 -0
  234. package/src/persistence/validation.js +108 -18
  235. package/src/persistence.js +4 -2
  236. package/src/renderers/canvas/index.js +16 -0
  237. package/src/renderers/canvas/isolated.js +11 -1
  238. package/src/renderers/canvas/native.js +7 -0
  239. package/src/renderers/canvas/scene.js +66 -0
  240. package/src/renderers/canvas/text.js +3 -2
  241. package/src/renderers/svg.js +1 -617
  242. package/src/renderers/svgSerialization.js +643 -0
  243. package/src/renderers/transitionScene.js +67 -0
  244. package/src/scene.js +89 -0
  245. package/src/selectors/axes.js +1 -1
  246. package/src/selectors/datasets.js +1 -0
  247. package/src/selectors/grids.js +9 -0
  248. package/src/selectors/markClip.js +28 -0
  249. package/src/selectors/raincloud.js +7 -0
  250. package/src/version.js +1 -1
  251. package/types/basic.d.ts +7 -0
  252. package/types/index.d.ts +8 -0
  253. package/types/program.d.ts +270 -60
  254. package/types/scene.d.ts +35 -0
@@ -2,7 +2,7 @@ import { localLegendPosition } from "../../grammar/legendGroups.js";
2
2
  import { prepareAuthoredFacetLegend, facetLegendIntent } from "./owner.js";
3
3
  import { isPlainObject } from "../../core/immutable.js";
4
4
  import { PLOT_LEGEND_POSITIONS } from "../../layout/legend.js";
5
- import { isContinuousColorScaleType } from "../../grammar/scales/types.js";
5
+ import { isContinuousColorScaleType, isDiscretizedColorScaleType } from "../../grammar/scales/types.js";
6
6
  import {
7
7
  unionConcreteGraphicBounds
8
8
  } from "../../grammar/schemas/graphicBounds.js";
@@ -83,8 +83,24 @@ function continuousValues(domain, count) {
83
83
  return sampleNumericRange(domain[0], domain[1], count, "Facet legend domain");
84
84
  }
85
85
 
86
+ function legendRequests(program) {
87
+ const request = legendCandidates(program);
88
+ const layer = findLayer(program, request.target);
89
+ const separate = request.channels.filter(channel => {
90
+ if (!["color", "stroke"].includes(channel)) return false;
91
+ const type = findSemanticScale(program, layer.encoding[channel].scale)?.type;
92
+ return isContinuousColorScaleType(type) || isDiscretizedColorScaleType(type);
93
+ });
94
+ if (separate.length === 0) return [request];
95
+ const categorical = request.channels.filter(channel => !separate.includes(channel));
96
+ return [
97
+ ...(categorical.length === 0 ? [] : [{ ...request, channels: categorical }]),
98
+ ...separate.map(channel => ({ ...request, channels: [channel] }))
99
+ ];
100
+ }
101
+
86
102
  function prepareAutoLegendSource(child) {
87
- const request = legendCandidates(child);
103
+ const requests = legendRequests(child);
88
104
  const canvas = child.graphicSpec.objects.canvas;
89
105
  let source = child
90
106
  ._withCanvasConfig({
@@ -99,7 +115,7 @@ function prepareAutoLegendSource(child) {
99
115
  .editGraphics({
100
116
  target: "canvas",
101
117
  property: "height",
102
- value: LEGEND_SOURCE_CANVAS.height
118
+ value: LEGEND_SOURCE_CANVAS.height * requests.length
103
119
  });
104
120
  if (canvas?.properties.background !== undefined) {
105
121
  source = source.editGraphics({
@@ -108,27 +124,30 @@ function prepareAutoLegendSource(child) {
108
124
  value: canvas.properties.background
109
125
  });
110
126
  }
111
- const scale = findSemanticScale(
112
- source,
113
- findLayer(source, request.target)?.encoding?.[
114
- request.channels.includes("color") ? "color" : "stroke"
115
- ]?.scale
116
- );
117
- const gradient = isContinuousColorScaleType(scale?.type);
118
- const gradientPrefix = request.channels.includes("color")
119
- ? "colorGradient"
120
- : "strokeGradient";
121
- source = source.createLegend({
122
- target: request.target,
123
- channels: request.channels,
124
- ...(gradient ? {
125
- count: 5,
126
- gradient: { length: 180, thickness: 12 },
127
- labels: { offset: 9, fontSize: 10 },
128
- titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
129
- } : {})
130
- });
131
- if (gradient && scale.type === "sequential") {
127
+ const gradients = [];
128
+ for (const request of requests) {
129
+ const colorChannel = request.channels.find(channel => ["color", "stroke"].includes(channel));
130
+ const scale = findSemanticScale(
131
+ source,
132
+ findLayer(source, request.target)?.encoding?.[colorChannel]?.scale
133
+ );
134
+ const gradient = isContinuousColorScaleType(scale?.type);
135
+ const gradientPrefix = colorChannel === "color"
136
+ ? "colorGradient"
137
+ : "strokeGradient";
138
+ source = source.createLegend({
139
+ target: request.target,
140
+ channels: request.channels,
141
+ ...(gradient ? {
142
+ count: 5,
143
+ gradient: { length: 180, thickness: 12 },
144
+ labels: { offset: 9, fontSize: 10 },
145
+ titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
146
+ } : {})
147
+ });
148
+ if (gradient && scale.type === "sequential") gradients.push({ scale, gradientPrefix });
149
+ }
150
+ for (const { scale, gradientPrefix } of gradients) {
132
151
  const resolved = source.resolvedScales[scale.id];
133
152
  source = source
134
153
  .editGraphics({
@@ -1 +1 @@
1
- import{isPlainObject as x}from"../core/immutable.js";import{formatVisibleText as E,measureTextWidth as S,resolveTextBounds as b,textBoundsIntersect as p}from"../core/textMetrics.js";import{resolveDisplayLabel as T}from"../grammar/displayLabels.js";import{DEFAULT_COLORS as I,DEFAULT_FONT_FAMILY as B}from"../theme/defaults.js";const K=Object.freeze(["top","right","bottom","left"]),D=Object.freeze({top:0,right:0,bottom:0,left:0});function F(){return{mode:"legacy",common:{fontSize:12,fontFamily:B,fontWeight:600,color:I.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function M(e){if(!x(e))throw new TypeError("Facet headers config must be a plain object.");if(e.common===void 0&&e.mode===void 0)return{...F(),common:{...F().common,...e}};if(!["legacy","roles"].includes(e.mode)||!x(e.common)||!x(e.row)||!x(e.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:e.mode,common:{...e.common},row:{...e.row},column:{...e.column}}}function g(e,t){const n=M(e);return t==="all"?{...n.common}:{...n.common,...n[t]}}function u(e,t){return T(e,t.labelMap,E)}function H(e){return new Map((e.facet.grid?.cells??[]).map(t=>[t.id,t]))}function O(e,t){const n=e.compositionSpec,s=g(t,"all"),c=H(n);return n.children.map((i,o)=>{const r=c.get(i),f=r===void 0?u(n.facet.values[o],s):`${u(r.rowValue,s)} \xB7 ${u(r.columnValue,s)}`;return{topology:"legacy",role:"column",id:i,text:f,config:s}})}function z(e,t){const n=t==="column"?"column":"row",s=t==="column"?"columnValue":"rowValue",c=new Map;for(const i of e.facet.grid.cells){const o=c.get(i[n]);if(o===void 0)c.set(i[n],{coordinate:i[n],value:i[s],ids:[i.id]});else{if(o.value!==i[s])throw new Error(`Facet ${t} header values conflict at coordinate ${i[n]}.`);o.ids.push(i.id)}}return[...c.values()].sort((i,o)=>i.coordinate-o.coordinate)}function L(e,t){const n=e.compositionSpec,s=g(t,"column");if(n.facet.grid===void 0)return n.children.map((r,f)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(f/n.columns),column:f%n.columns,text:u(n.facet.values[f],s),config:s}));const c=g(t,"row"),i=z(n,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:u(r.value,s),config:s})),o=z(n,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:u(r.value,c),config:c}));return[...i,...o]}function v(e,t,n){const s=e.filter(c=>c.role===t&&c.text!=="");return s.length===0?0:Math.max(...t==="column"?s.map(c=>c.config.fontSize+c.config.offset):s.map(c=>S(c.text,c.config,n)+c.config.offset))}function N(e,t){const n=M(t),s=n.mode==="legacy"?O(e,n):L(e,n),c=e.compositionSpec.facet.grid===void 0?Math.ceil(e.compositionSpec.children.length/e.compositionSpec.columns):Math.max(...e.compositionSpec.facet.grid.cells.map(o=>o.row))+1,i={outer:{...D},cellRows:{top:Array(c).fill(0),bottom:Array(c).fill(0)}};if(n.mode==="legacy"&&e.compositionSpec.spacing==="plot")for(const o of s){if(o.text==="")continue;const r=e.compositionSpec.children.indexOf(o.id),f=e.compositionSpec.facet.grid?.cells[r]?.row??Math.floor(r/e.compositionSpec.columns);i.cellRows.top[f]=Math.max(i.cellRows.top[f],o.config.offset+o.config.fontSize)}if(n.mode==="roles"){const o=g(n,"column"),r=v(s,"column",e.materializationConfigs.textMetrics);if(e.compositionSpec.facet.grid===void 0){const f=["left","right"].includes(o.side);f&&(i.cellColumns={left:Array(e.compositionSpec.columns).fill(0),right:Array(e.compositionSpec.columns).fill(0)});for(const l of s)if(l.text!==""){const a=f?i.cellColumns:i.cellRows,m=f?l.column:l.row,d=f?S(l.text,l.config,e.materializationConfigs.textMetrics):l.config.fontSize;a[o.side][m]=Math.max(a[o.side][m],d+l.config.offset)}}else{i.outer[o.side]=r;const f=g(n,"row");i.outer[f.side]=v(s,"row",e.materializationConfigs.textMetrics)}}return{headers:n,descriptors:s,headerLayout:i}}function A(e,t){return t==="start"?{x:e.left,textAlign:"left"}:t==="end"?{x:e.right,textAlign:"right"}:{x:(e.left+e.right)/2,textAlign:"center"}}function R(e,t){return t==="start"?{y:e.top,textBaseline:"top"}:t==="end"?{y:e.bottom,textBaseline:"bottom"}:{y:(e.top+e.bottom)/2,textBaseline:"middle"}}function C(e){return{left:Math.min(...e.map(t=>t.left)),right:Math.max(...e.map(t=>t.right)),top:Math.min(...e.map(t=>t.top)),bottom:Math.max(...e.map(t=>t.bottom))}}function h(e,t){return{left:e.x+t.x,right:e.x+t.x+t.width,top:e.y+t.y,bottom:e.y+t.y+t.height}}function y(e){return{left:e.x,right:e.x+e.width,top:e.y,bottom:e.y+e.height}}function $(e,t,n,s){const c=e.ids.map(d=>t.get(d)),i=e.ids.find((d,w)=>c[w]===void 0),o=e.ids.find(d=>n.get(d)===void 0);if(i!==void 0||o!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${i??o}".`);const r=e.ids.map((d,w)=>h(c[w],n.get(d))),f=C(r),l=s==="plot"?f:C(c.map(y)),a=e.config;let m;if(["top","bottom"].includes(a.side)){m=A(f,a.align);const d=a.side==="top";m.y=d?l.top-a.offset-a.fontSize/2:l.bottom+a.offset+a.fontSize/2,m.textBaseline="middle"}else{m=R(f,a.align);const d=a.side==="left";m.x=d?l.left-a.offset:l.right+a.offset,m.textAlign=d?"right":"left"}return{descriptor:e,snapshots:l,properties:m}}function j(e,t,n,s){if(t===void 0||n===void 0)throw new Error(`Facet header requires cell and plot bounds for "${e.id}".`);return{descriptor:e,snapshots:y(t),properties:{...A(h(t,n),e.config.align),y:s==="plot"?t.y+n.y-e.config.offset-e.config.fontSize/2:t.y+e.config.offset,textBaseline:"middle"}}}function q(e){const{config:t}=e.descriptor;return{type:"text",properties:{...e.properties,text:e.descriptor.text,fill:t.color,fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight}}}function V(e,t,n,s,c){const i=t.map((o,r)=>e[r].descriptor.text===""?void 0:b(o.properties,s));for(const[o,r]of i.entries())if(r!==void 0&&(r.left<-1e-9||r.right>n.width+1e-9||r.top<-1e-9||r.bottom>n.height+1e-9||i.some((f,l)=>l<o&&f!==void 0&&p(f,r))||n.children.some(f=>p(r,n.spacing==="plot"?h(f,c.get(f.id)):y(f)))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function G(e,t,n,s){const c=[];for(let i=0;i<e.length;i+=1){if(e[i].properties.text==="")continue;const o=b(e[i].properties,s),r=t.children[i],f=n.get(r.id);if(o.left<0||o.right>t.width||o.top<0||o.bottom>t.height||c.some(l=>p(l,o))||(t.spacing==="plot"?t.children.some(l=>p(o,h(l,n.get(l.id)))):p(o,h(r,f))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");c.push(o)}}function P(e,t,n){const s=new Map(e.children.map(o=>[o.id,o])),c=new Map(t.map(o=>[o.id,o])),i=n.descriptors.map(o=>o.topology==="legacy"?j(o,s.get(o.id),c.get(o.id),e.spacing):$(o,s,c,e.spacing));return{records:i,items:i.map(q),plotById:c}}function Y(e,t,n,s){const c=e.materializationConfigs.textMetrics,{records:i,items:o,plotById:r}=P(t,n,s);s.headers.mode==="legacy"?G(o,t,r,c):V(i,o,t,c,r);const f=`${e.compositionSpec.id}-headers`;return e.createGraphics({id:f,type:"collection",parent:"canvas"}).editGraphics({target:f,property:"items",value:o})}export{F as createDefaultFacetHeaders,Y as materializeFacetHeaders,M as normalizeFacetHeadersConfig,N as prepareFacetHeaders,g as resolveFacetHeaderConfig,P as resolveFacetHeaderItems};
1
+ import{isPlainObject as F}from"../core/immutable.js";import{formatVisibleText as V,measureTextWidth as P,resolveTextBounds as z,textBoundsIntersect as S}from"../core/textMetrics.js";import{validateFacetHeaderRotations as W}from"../grammar/facetHeaders.js";import{resolveDisplayLabel as _}from"../grammar/displayLabels.js";import{resolveGraphicBounds as B}from"../layout/canvas.js";import{unionConcreteGraphicBounds as U}from"../grammar/schemas/graphicBounds.js";import{intersectClipBounds as K}from"../grammar/schemas/graphicClip.js";import{DEFAULT_COLORS as N,DEFAULT_FONT_FAMILY as Y}from"../theme/defaults.js";import{prepareFacetHeaderTitles as Z}from"../layout/facetHeaderTitles.js";import{cartesianAxisOwners as J,axisGraphicIds as E}from"../selectors/axes.js";import{resolveFacetOuterSides as Q}from"../grammar/facets/guides.js";import{resolveConcreteGraphicBounds as X}from"../grammar/schemas/graphicBounds.js";import{allLegendGraphicIds as tt}from"./guides/resources.js";const et=Object.freeze(["top","right","bottom","left"]),v=Object.freeze({top:0,right:0,bottom:0,left:0});function ot(t){return`${t}-headers`}function T(){return{mode:"legacy",common:{fontSize:12,fontFamily:Y,fontWeight:600,color:N.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function j(t){if(!F(t))throw new TypeError("Facet headers config must be a plain object.");if(W(t),t.common===void 0&&t.mode===void 0)return{...T(),common:{...T().common,...t}};if(!["legacy","roles"].includes(t.mode)||!F(t.common)||!F(t.row)||!F(t.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:t.mode,common:{...t.common},row:{...t.row},column:{...t.column}}}function y(t,e){const o=j(t);return e==="all"?{...o.common}:{...o.common,...o[e]}}function b(t,e){return _(t,e.labelMap,V)}function nt(t){return new Map((t.facet.grid?.cells??[]).map(e=>[e.id,e]))}function it(t,e){const o=t.compositionSpec,c=y(e,"all"),i=nt(o);return o.children.map((n,s)=>{const r=i.get(n),a=r===void 0?b(o.facet.values[s],c):`${b(r.rowValue,c)} \xB7 ${b(r.columnValue,c)}`;return{topology:"legacy",role:"column",id:n,text:a,config:c}})}function G(t,e){const o=e==="column"?"column":"row",c=e==="column"?"columnValue":"rowValue",i=new Map;for(const n of t.facet.grid.cells){const s=i.get(n[o]);if(s===void 0)i.set(n[o],{coordinate:n[o],value:n[c],ids:[n.id]});else{if(s.value!==n[c])throw new Error(`Facet ${e} header values conflict at coordinate ${n[o]}.`);s.ids.push(n.id)}}return[...i.values()].sort((n,s)=>n.coordinate-s.coordinate)}function ct(t,e){const o=t.compositionSpec,c=y(e,"column");if(o.facet.grid===void 0)return o.children.map((r,a)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(a/o.columns),column:a%o.columns,text:b(o.facet.values[a],c),config:c}));const i=y(e,"row"),n=G(o,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:b(r.value,c),config:c})),s=G(o,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:b(r.value,i),config:i}));return[...n,...s]}function x(t){return t.rotation!==void 0&&t.rotation!==0}function H(t,e){return z({x:0,y:0,text:t.text,...t.config,textAlign:"center",textBaseline:"middle"},e)}function st(t,e,o){if(x(t.config)){const c=H(t,o);return["left","right"].includes(e)?c.right-c.left:c.bottom-c.top}return["left","right"].includes(e)?P(t.text,t.config,o):t.config.fontSize}function rt(t){const e={...v};if(t.compositionSpec.spacing!=="plot")return e;for(const o of t.compositionSpec.children){const c=t.children[o],i=B(c),n=c.graphicSpec.objects.canvas.properties;e.left=Math.max(e.left,i.x),e.top=Math.max(e.top,i.y),e.right=Math.max(e.right,n.width-i.x-i.width),e.bottom=Math.max(e.bottom,n.height-i.y-i.height)}return e}function O(t,e,o,c,i){const n=st(t,e,o)+t.config.offset+(t.clearance?.[e]??0);return x(t.config)&&i?Math.max(0,n-c[e]):n}function ft(t,e,o){if(!e.some(i=>i.config.anchor==="content"))return;if(t.compositionSpec.spacing!=="plot")throw new Error("Facet header content anchor requires plot spacing.");const c=new Map;for(const i of t.compositionSpec.children){const n=t.children[i],s=B(n),r={left:s.x,right:s.x+s.width,top:s.y,bottom:s.y+s.height},a=n.semanticSpec.layers.map(f=>f.id).filter(f=>n.graphicSpec.objects[f]!==void 0),u=U(n.graphicSpec,a,o),l=n.graphicSpec.objects.canvas.properties,h=u===void 0?void 0:K(u,{left:0,right:l.width,top:0,bottom:l.height});c.set(i,h===void 0||h.left>h.right||h.top>h.bottom?r:C([r,h]))}for(const i of e)if(i.config.anchor==="content"){i.clearance={...v};for(const n of i.ids??[i.id]){const s=B(t.children[n]),r=c.get(n);for(const[a,u]of Object.entries({left:s.x-r.left,right:r.right-s.x-s.width,top:s.y-r.top,bottom:r.bottom-s.y-s.height}))i.clearance[a]=Math.max(i.clearance[a],u)}}return c}function R(t,e,o,c){const i=t.filter(n=>n.role===e&&n.text!=="");return i.length===0?0:Math.max(...i.map(n=>O(n,n.config.side,o,c,!0)))}function at(t,e){const o=t.compositionSpec,c=t.children[e],i=o.facet.guides.axes==="outer"?Q({placements:o.facet.grid?.cells??o.children.map((s,r)=>({id:s,row:Math.floor(r/o.columns),column:r%o.columns}))}):void 0,n=J(c).flatMap(s=>E(s.channel,s.id).filter((r,a)=>a>=2&&(i===void 0||i[s.config?.[["line","ticks","labels","title"][a]]?.position]?.includes(e))));for(const s of["theta","radius","parallel"])c.guideConfigs.axis?.[s]!==void 0&&n.push(...E(s).slice(2));return o.facet.guides.legend===!1&&n.push(...tt(Object.keys(c.guideConfigs.legend??{}))),n.filter(s=>c.graphicSpec.objects[s]!==void 0)}function lt(t,e){const o=t.children[e],c=B(o),i=at(t,e).map(n=>X(o.graphicSpec,n,o.materializationConfigs.textMetrics)).filter(Boolean);return{top:Math.max(0,...i.map(n=>c.y-n.top)),bottom:Math.max(0,...i.map(n=>n.bottom-c.y-c.height)),left:Math.max(0,...i.map(n=>c.x-n.left)),right:Math.max(0,...i.map(n=>n.right-c.x-c.width))}}function Et(t,e){const o=j(e),c=o.mode==="legacy"?it(t,o):ct(t,o),i=t.compositionSpec.facet.grid===void 0?Math.ceil(t.compositionSpec.children.length/t.compositionSpec.columns):Math.max(...t.compositionSpec.facet.grid.cells.map(f=>f.row))+1,n=t.materializationConfigs.textMetrics,s=["column",...t.compositionSpec.facet.grid===void 0?[]:["row"]].map(f=>[f,y(o,f)]),r=s.some(([,f])=>f.title!==void 0);if(t.compositionSpec.spacing==="plot"){const f=new Map(t.compositionSpec.children.map(p=>[p,lt(t,p)]));for(const p of c)p.clearance=Object.fromEntries(et.map(m=>{const d=Math.max(0,...(p.ids??[p.id]).map(g=>f.get(g)[m]));return[m,d>p.config.offset?d:0]}))}const a=ft(t,c,n),u=r||c.some(f=>x(f.config))?rt(t):v,l={outer:{...v},cellRows:{top:Array(i).fill(0),bottom:Array(i).fill(0)}};if(o.mode==="legacy"&&t.compositionSpec.spacing==="plot")for(const f of c){if(f.text==="")continue;const p=t.compositionSpec.children.indexOf(f.id),m=t.compositionSpec.facet.grid?.cells[p]?.row??Math.floor(p/t.compositionSpec.columns);l.cellRows.top[m]=Math.max(l.cellRows.top[m],O(f,"top",n,u,m===0))}if(o.mode==="roles"){const f=y(o,"column"),p=R(c,"column",n,u);if(t.compositionSpec.facet.grid===void 0){const m=["left","right"].includes(f.side);m&&(l.cellColumns={left:Array(t.compositionSpec.columns).fill(0),right:Array(t.compositionSpec.columns).fill(0)});for(const d of c)if(d.text!==""){const g=m?l.cellColumns:l.cellRows,M=m?d.column:d.row,$=m?t.compositionSpec.columns-1:i-1,q=["top","left"].includes(f.side)?M===0:M===$;g[f.side][M]=Math.max(g[f.side][M],O(d,f.side,n,u,q))}}else{l.outer[f.side]=p;const m=y(o,"row");l.outer[m.side]=R(c,"row",n,u)}}const h=Z({roles:s,descriptors:c,space:u,headerLayout:l,profile:n,ids:t.compositionSpec.children});return{headers:o,descriptors:c,titles:h,headerLayout:l,...a===void 0?{}:{contentById:a},...(r||c.some(f=>x(f.config)))&&n!==void 0?{profile:n}:{}}}function D(t,e){return e==="start"?{x:t.left,textAlign:"left"}:e==="end"?{x:t.right,textAlign:"right"}:{x:(t.left+t.right)/2,textAlign:"center"}}function dt(t,e){return e==="start"?{y:t.top,textBaseline:"top"}:e==="end"?{y:t.bottom,textBaseline:"bottom"}:{y:(t.top+t.bottom)/2,textBaseline:"middle"}}function C(t){return{left:Math.min(...t.map(e=>e.left)),right:Math.max(...t.map(e=>e.right)),top:Math.min(...t.map(e=>e.top)),bottom:Math.max(...t.map(e=>e.bottom))}}function w(t,e){return{left:t.x+e.x,right:t.x+e.x+e.width,top:t.y+e.y,bottom:t.y+e.y+e.height}}function A(t){return{left:t.x,right:t.x+t.width,top:t.y,bottom:t.y+t.height}}function I(t,e){return{left:t.x+e.left,right:t.x+e.right,top:t.y+e.top,bottom:t.y+e.bottom}}function L(t,e,o,c,i){return i==="start"?t-o:i==="end"?e-c:(t+e-o-c)/2}function k(t,e,o,c){const{config:i}=t,n=H(t,c),s={textAlign:"center",textBaseline:"middle",rotation:i.rotation};return["top","bottom"].includes(i.side)?{...s,x:L(e.left,e.right,n.left,n.right,i.align),y:i.side==="top"?o.top-i.offset-n.bottom:o.bottom+i.offset-n.top}:{...s,x:i.side==="left"?o.left-i.offset-n.right:o.right+i.offset-n.left,y:L(e.top,e.bottom,n.top,n.bottom,i.align)}}function ut(t,e,o,c,i,n){const s=t.ids.map(d=>e.get(d)),r=t.ids.find((d,g)=>s[g]===void 0),a=t.ids.find(d=>o.get(d)===void 0);if(r!==void 0||a!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${r??a}".`);const u=t.ids.map((d,g)=>w(s[g],o.get(d))),l=C(u),h=t.config.anchor==="content"?C(t.ids.map((d,g)=>I(s[g],n.get(d)))):c==="plot"?l:C(s.map(A)),f=t.config;if(x(f)||t.topology==="role-title"){const d=k(t,l,h,i),g=["left","right"].includes(f.side)?"y":"x";return f.alongOffset!==void 0&&(d[g]+=f.alongOffset),{descriptor:t,snapshots:h,properties:d}}const p=f.anchor==="content"?0:t.clearance?.[f.side]??0;let m;if(["top","bottom"].includes(f.side)){m=D(l,f.align);const d=f.side==="top";m.y=d?h.top-p-f.offset-f.fontSize/2:h.bottom+p+f.offset+f.fontSize/2,m.textBaseline="middle"}else{m=dt(l,f.align);const d=f.side==="left";m.x=d?h.left-p-f.offset:h.right+p+f.offset,m.textAlign=d?"right":"left"}return{descriptor:t,snapshots:h,properties:m}}function mt(t,e,o,c,i,n){if(e===void 0||o===void 0)throw new Error(`Facet header requires cell and plot bounds for "${t.id}".`);const s=t.config.anchor==="content"?I(e,n.get(t.id)):w(e,o);if(x(t.config)&&c==="plot"){const r=w(e,o);return{descriptor:t,snapshots:A(e),properties:k({...t,config:{...t.config,side:"top"}},r,s,i)}}return{descriptor:t,snapshots:A(e),properties:{...D(w(e,o),t.config.align),y:c==="plot"?e.y+o.y-(t.clearance?.top??0)-t.config.offset-t.config.fontSize/2:e.y+t.config.offset,textBaseline:"middle",...x(t.config)?{rotation:t.config.rotation}:{}}}}function pt(t){const{config:e}=t.descriptor;return{type:"text",properties:{...t.properties,text:t.descriptor.text,fill:e.color,fontSize:e.fontSize,fontFamily:e.fontFamily,fontWeight:e.fontWeight}}}function ht(t,e,o,c,i,n){const s=e.map((r,a)=>t[a].descriptor.text===""?void 0:z(r.properties,c));for(const[r,a]of s.entries())if(a!==void 0&&(a.left<-1e-9||a.right>o.width+1e-9||a.top<-1e-9||a.bottom>o.height+1e-9||s.some((u,l)=>l<r&&u!==void 0&&S(u,a))||o.children.some(u=>S(a,n===void 0?o.spacing==="plot"?w(u,i.get(u.id)):A(u):I(u,n.get(u.id))))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function gt(t,e,o,c,i){const n=[];for(let s=0;s<t.length;s+=1){if(t[s].properties.text==="")continue;const r=z(t[s].properties,c),a=e.children[s],u=o.get(a.id);if(r.left<0||r.right>e.width||r.top<0||r.bottom>e.height||n.some(l=>S(l,r))||(e.spacing==="plot"?e.children.some(l=>S(r,i===void 0?w(l,o.get(l.id)):I(l,i.get(l.id)))):S(r,w(a,u))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");n.push(r)}}function xt(t,e,o){const c=new Map(t.children.map(s=>[s.id,s])),i=new Map(e.map(s=>[s.id,s])),n=[...o.descriptors,...o.titles??[]].map(s=>s.topology==="legacy"?mt(s,c.get(s.id),i.get(s.id),t.spacing,o.profile,o.contentById):ut(s,c,i,t.spacing,o.profile,o.contentById));return{records:n,items:n.map(pt),plotById:i}}function Tt(t,e,o,c){const i=t.materializationConfigs.textMetrics,{records:n,items:s,plotById:r}=xt(e,o,c);c.headers.mode==="legacy"?gt(s,e,r,i,c.contentById):ht(n,s,e,i,r,c.contentById);const a=ot(t.compositionSpec.id);return t.createGraphics({id:a,type:"collection",parent:"canvas"}).editGraphics({target:a,property:"items",value:s})}export{T as createDefaultFacetHeaders,ot as facetHeaderGraphicId,Tt as materializeFacetHeaders,j as normalizeFacetHeadersConfig,Et as prepareFacetHeaders,y as resolveFacetHeaderConfig,xt as resolveFacetHeaderItems,at as retainedFacetGuideIds};
@@ -0,0 +1,25 @@
1
+ import { walkGraphicTreeEvents } from "../grammar/schemas/graphicTree.js";
2
+ import { resolveConcreteGraphicBounds } from "../grammar/schemas/graphicBounds.js";
3
+ import { assertFacetTopLaneBounds } from "../layout/facetTopLane.js";
4
+
5
+ export function assertFacetTopLane(program, { layout, plots, headerId, titleIds }) {
6
+ if (program.compositionSpec.topLane === undefined) return;
7
+ const titles = [], headers = [], retained = [];
8
+ const graphic = program.graphicSpec, profile = program.materializationConfigs.textMetrics;
9
+ const collect = entry => {
10
+ const type = entry.object.type ?? entry.owner?.type;
11
+ const properties = entry.object.properties;
12
+ if (type !== "text" || properties === undefined || properties.text === "" || properties.opacity === 0) return;
13
+ const bounds = resolveConcreteGraphicBounds(graphic, entry.id, profile);
14
+ if (bounds === undefined) return;
15
+ const owner = entry.ownerId ?? entry.id;
16
+ if (titleIds.includes(owner)) titles.push(bounds);
17
+ else if (owner === headerId) headers.push(bounds);
18
+ else retained.push(bounds);
19
+ };
20
+ walkGraphicTreeEvents(graphic, { enter: collect, item: collect });
21
+ assertFacetTopLaneBounds({ canvas: graphic.objects.canvas.properties, titles, headers, retained,
22
+ plots: layout.children.map((cell, index) => ({ left: cell.x + plots[index].x,
23
+ right: cell.x + plots[index].x + plots[index].width, top: cell.y + plots[index].y,
24
+ bottom: cell.y + plots[index].y + plots[index].height })) });
25
+ }
@@ -1 +1 @@
1
- import{cartesianAxisOwners as C}from"../selectors/axes.js";import{resolveConcreteGraphicBounds as G}from"../grammar/schemas/graphicBounds.js";import{textBoundsFitCanvas as F,textBoundsIntersect as m,resolveTextBounds as k}from"../core/textMetrics.js";import{axisGraphicIds as L,allLegendGraphicIds as j}from"./guides/resources.js";import{resolveGraphicBounds as u}from"../layout/canvas.js";import{resolvePlacedPlotBounds as P}from"../layout/composition.js";import{alignedTextAnchor as z,buildTitleReadingBlock as T,paintedTitleComponent as B,resolveTitleComponentBounds as E,titleTextProperties as O}from"../layout/title.js";import{resolveFacetLayout as $}from"../layout/facets.js";import{namespaceGraphicId as b,namespaceGraphicSnapshot as M}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as v}from"./facetGuides/index.js";import{resolvePromotedFacetLegendLayout as H}from"./facetGuides/layout.js";import{attachSnapshotObject as I,clearCompositionChildren as A,compositionChildDescriptor as q}from"./composition.js";import{materializeFacetHeaders as R,prepareFacetHeaders as _}from"./facetHeaders.js";const W={top:0,right:0,bottom:0,left:0};function D(e){const c=e.materializationConfigs.facets?.[e.compositionSpec.id];if(c===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return c}function N(e){if(e.semanticSpec.title.text===void 0)return{height:0};const c=e.titleConfig;if(c===void 0)throw new Error("Facet title requires chart title configuration.");if(c.position!=="top")throw new Error("Facet parent title currently supports only top position.");const i=T({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},c),o=8+c.offset;if(o<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:c,block:i,top:o,height:Math.ceil(o+i.height)}}function w(e,c,i,o,l,d,s){if(i.length===0)return e;const r=z(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),t=B({x:r,y:o.map(h=>s+h),lines:i,textAlign:e.titleConfig.align}),n=E(t,l,e.materializationConfigs.textMetrics);if(!F(n,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const a=t.lines.length;let p=e.createGraphics({id:c,type:"text",...a>1?{length:a}:{},parent:"canvas"});for(const[h,f]of Object.entries(O(t,l)))p=p.editGraphics({target:c,property:h,value:f});return p}function Z(e,c,i){let o=w(e,"chartTitle",i.block.titleLines,i.block.titleCenters,i.config.titleStyle,c,i.top);return i.block.subtitleLines.length>0&&(o=w(o,"chartSubtitle",i.block.subtitleLines,i.block.subtitleCenters,i.config.subtitleStyle,c,i.top)),o}function J(e,c){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");c??=v(e);const i=N(e),o=e.compositionSpec,l=_(e,D(e).headers),d=new Map((o.facet.grid?.cells??[]).map(n=>[n.id,n]));let s=$({children:o.children.map((n,a)=>({...q(n,e.children[n]),value:o.facet.values[a],...d.has(n)?{row:d.get(n).row,column:d.get(n).column}:{}})),spacing:o.spacing,plots:o.children.map(n=>({id:n,...u(e.children[n])})),columns:o.columns,gap:o.gap,align:o.align,padding:o.padding,titleHeight:i.height,headerLayout:l.headerLayout,sharedLegend:o.facet.guides.legend==="shared"&&c?.mode!=="promoted",...c===void 0?{}:{sharedLegendGap:c.reservation.gap,sharedLegendWidth:c.reservation.width,sharedLegendHeight:c.reservation.height,sharedLegendPosition:c.reservation.position}});const r=o.children.map(n=>({id:n,...u(e.children[n])}));let t=P({placements:s.children,plots:r});return{layout:s,plot:t}=H(e,{layout:s,plot:t,plots:r,title:i,preparedHeaders:l,prepared:c}),{layout:s,title:i,plot:t,plots:r,preparedHeaders:l}}function K(e,c,i){const o=[],l=c.children.map((s,r)=>({id:s.id,left:s.x+i[r].x,right:s.x+i[r].x+i[r].width,top:s.y+i[r].y,bottom:s.y+i[r].y+i[r].height}));for(const s of c.children){const r=e.children[s.id],t=[...C(r).flatMap(n=>L(n.channel,n.id).slice(2)),...j(Object.keys(r.guideConfigs.legend??{}))];for(const n of t){const a=b(`${e.compositionSpec.id}-${s.id}`,n);if(e.graphicSpec.objects[a]===void 0)continue;const p=G(e.graphicSpec,a,e.materializationConfigs.textMetrics);p!==void 0&&o.push({owner:s.id,bounds:p})}}const d=e.graphicSpec.objects[`${e.compositionSpec.id}-headers`];for(const s of d?.items??[])s.properties.text!==""&&o.push({owner:"headers",bounds:k(s.properties,e.materializationConfigs.textMetrics)});for(let s=0;s<o.length;s+=1){const r=o[s];if(l.some(t=>t.id!==r.owner&&m(r.bounds,t))||o.slice(0,s).some(t=>t.owner!==r.owner&&m(r.bounds,t.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function ae(e){const c=v(e),{layout:i,title:o,plot:l,plots:d,preparedHeaders:s}=J(e,c),r=e.graphicSpec.objects.canvas?.properties?.background??"white";let t=A(e);t.graphicSpec.objects.canvas===void 0&&(t=t.createGraphics({id:"canvas",type:"canvas"}));for(const[n,a]of Object.entries({width:i.width,height:i.height,background:r}))t=t.editGraphics({target:"canvas",property:n,value:a});for(const n of i.children){const a=e.children[n.id],p=M(a.graphicSpec,{namespace:`${e.compositionSpec.id}-${n.id}`,x:n.x,y:n.y});if(t=I(t,p,p.order[0],"canvas"),i.spacing==="plot"){const h=p.order[0],f=b(`${e.compositionSpec.id}-${n.id}`,"plot-background"),y=u(a),g=t.graphicSpec.objects[h].children?.[0];t=t.editGraphics({target:h,property:"background",value:"transparent"}).createGraphics({id:f,type:"rect",parent:h,...g===void 0?{}:{before:g}});for(const[x,S]of Object.entries({...y,stroke:"none",strokeWidth:0,fill:a.graphicSpec.objects.canvas.properties.background??"transparent"}))t=t.editGraphics({target:f,property:x,value:S})}}return t=R(t,i,d,s),t=t.composeFacetGuides({layout:i,plot:l}),i.spacing==="plot"&&K(t,i,d),o.height>0&&(t=Z(t,l,o)),t._withCanvasConfig({margin:W,size:{width:"auto",height:"auto"}})}export{ae as materializeFacetGraphics,J as resolveFacetProgramLayout};
1
+ import{cartesianAxisOwners as W}from"../selectors/axes.js";import{resolveConcreteGraphicBounds as q,resolvePrimitiveGraphicBounds as N,resolveGraphicParentOffset as V,resolveGraphicClipBounds as Z}from"../grammar/schemas/graphicBounds.js";import{intersectClipBounds as J}from"../grammar/schemas/graphicClip.js";import{cloneAndFreeze as K,isPlainObject as Q}from"../core/immutable.js";import{walkGraphicTreeEvents as _}from"../grammar/schemas/graphicTree.js";import{textBoundsFitCanvas as U,textBoundsIntersect as L,resolveTextBounds as M,requireFiniteBounds as X}from"../core/textMetrics.js";import{axisGraphicIds as Y,allLegendGraphicIds as ee}from"./guides/resources.js";import{resolveGraphicBounds as G}from"../layout/canvas.js";import{resolvePlacedPlotBounds as te}from"../layout/composition.js";import{alignedTextAnchor as oe,buildTitleReadingBlock as ie,paintedTitleComponent as ne,resolveTitleComponentBounds as re,titleTextProperties as ce}from"../layout/title.js";import{resolveFacetLayout as se}from"../layout/facets.js";import{assertFacetTopLane as ae}from"./facetTopLane.js";import{namespaceGraphicId as R,namespaceGraphicSnapshot as pe}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as I}from"./facetGuides/index.js";import{prepareFacetGuideOwnership as de}from"./facetGuides/placement.js";import{resolvePromotedFacetLegendLayout as le}from"./facetGuides/layout.js";import{attachSnapshotObject as fe,clearCompositionChildren as he,compositionChildDescriptor as ue}from"./composition.js";import{materializeFacetHeaders as me,prepareFacetHeaders as ge,facetHeaderGraphicId as B,retainedFacetGuideIds as we}from"./facetHeaders.js";const be={top:0,right:0,bottom:0,left:0},T=Object.freeze(["chartTitle","chartSubtitle"]);function z(e,i){return i===void 0?e:e===void 0?{...i}:{left:Math.min(e.left,i.left),right:Math.max(e.right,i.right),top:Math.min(e.top,i.top),bottom:Math.max(e.bottom,i.bottom)}}function De(e,{headerItems:i=[]}={}){if(e?.compositionSpec?.type!=="facet")throw new Error("Facet content measurement requires a facet composition.");const t=e.compositionSpec,r=new Map((t.facet.grid?.cells??[]).map(c=>[c.id,c])),l=t.children.map((c,f)=>({id:c,row:r.get(c)?.row??Math.floor(f/t.columns),column:r.get(c)?.column??f%t.columns})),d=I(e),n=de(e,l,d),p=new Set(t.children),o=new Map;if(!Array.isArray(i)||i.some(c=>!p.has(c?.ownerId)||c.type!=="text"||!Q(c.properties)))throw new TypeError("Facet content headers require concrete text with one observed child owner.");for(const c of i)o.has(c.ownerId)||o.set(c.ownerId,[]),o.get(c.ownerId).push(c);const s=["line","ticks","labels","title"];return K(l.map(c=>{const f=e.children[c.id],m=f.graphicSpec,b=f.materializationConfigs.textMetrics,h=G(f),x=n.children[c.id],y=new Map;for(const a of W(f)){const g=Y(a.channel,a.id);for(const[S,w]of s.entries())x.removeAxes.includes(a.id)||x.removeAxisComponents?.[a.id]?.includes(w)?y.set(g[S],"removed"):t.facet.guides.axes==="outer"&&a.channel==="x"&&a.config?.[w]?.position==="bottom"&&y.set(g[S],"footer")}for(const a of ee(x.removeLegends))y.set(a,"removed");const v=new Map,F=[],j=new Map,P=a=>{const g=F.at(-1);if(g==="removed")return;const S=a.object.type??a.owner?.type,w=a.object.properties;if(!w||S==="canvas"||w.opacity===0||S==="text"&&(w.text===""||w.fill==="none"||w.fill==="transparent"))return;const C=a.ownerId??a.id;j.has(C)||j.set(C,{offset:V(m,C),clip:Z(m,C,{rootViewport:!0})});const{offset:[O,A],clip:H}=j.get(C);let u=N(S,w,b);if(u===void 0||(u=X({left:u.left+O,right:u.right+O,top:u.top+A,bottom:u.bottom+A}),H!==void 0&&(u=J(u,H)),u.left>u.right||u.top>u.bottom))return;const $=g??"body";v.set($,z(v.get($),u))};_(m,{enter(a){F.push(F.at(-1)==="removed"?"removed":y.get(a.id)??F.at(-1)),a.object.items===void 0&&!a.object.children?.length&&P(a)},item:P,exit(){F.pop()}});let k=z({left:h.x,right:h.x+h.width,top:h.y,bottom:h.y+h.height},v.get("body"));for(const a of o.get(c.id)??[]){const g=a.properties;g.text!==""&&g.opacity!==0&&g.fill!=="none"&&g.fill!=="transparent"&&(k=z(k,M(g,b)))}const E=a=>a===void 0?void 0:{left:a.left-h.x,right:a.right-h.x,top:a.top-h.y,bottom:a.bottom-h.y};return{...c,plot:h,bounds:E(k),...v.has("footer")?{footerBounds:E(v.get("footer"))}:{}}}))}function ve(e){const i=e.materializationConfigs.facets?.[e.compositionSpec.id];if(i===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return i}function xe(e){if(e.semanticSpec.title.text===void 0)return{height:0};const i=e.titleConfig;if(i===void 0)throw new Error("Facet title requires chart title configuration.");if(i.position!=="top")throw new Error("Facet parent title currently supports only top position.");const t=ie({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},i),r=8+i.offset;if(r<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:i,block:t,top:r,height:Math.ceil(r+t.height)}}function D(e,i,t,r,l,d,n){if(t.length===0)return e;const p=oe(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),o=ne({x:p,y:r.map(m=>n+m),lines:t,textAlign:e.titleConfig.align}),s=re(o,l,e.materializationConfigs.textMetrics);if(!U(s,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const c=o.lines.length;let f=e.createGraphics({id:i,type:"text",...c>1?{length:c}:{},parent:"canvas"});for(const[m,b]of Object.entries(ce(o,l)))f=f.editGraphics({target:i,property:m,value:b});return f}function ye(e,i,t){let r=D(e,T[0],t.block.titleLines,t.block.titleCenters,t.config.titleStyle,i,t.top);return t.block.subtitleLines.length>0&&(r=D(r,T[1],t.block.subtitleLines,t.block.subtitleCenters,t.config.subtitleStyle,i,t.top)),r}function Se(e,i){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");i??=I(e);const t=xe(e),r=e.compositionSpec,l=ge(e,ve(e).headers),d=new Map((r.facet.grid?.cells??[]).map(s=>[s.id,s]));let n=se({children:r.children.map((s,c)=>({...ue(s,e.children[s]),value:r.facet.values[c],...d.has(s)?{row:d.get(s).row,column:d.get(s).column}:{}})),spacing:r.spacing,topLane:r.topLane,plots:r.children.map(s=>({id:s,...G(e.children[s])})),columns:r.columns,gap:r.gap,align:r.align,padding:r.padding,titleHeight:t.height,headerLayout:l.headerLayout,sharedLegend:r.facet.guides.legend==="shared"&&i?.mode!=="promoted",...i===void 0?{}:{sharedLegendGap:i.reservation.gap,sharedLegendWidth:i.reservation.width,sharedLegendHeight:i.reservation.height,sharedLegendPosition:i.reservation.position}});const p=r.children.map(s=>({id:s,...G(e.children[s])}));let o=te({placements:n.children,plots:p});return{layout:n,plot:o}=le(e,{layout:n,plot:o,plots:p,title:t,preparedHeaders:l,prepared:i}),{layout:n,title:t,plot:o,plots:p,preparedHeaders:l}}function Ce(e,i,t){const r=[],l=i.children.map((n,p)=>({id:n.id,left:n.x+t[p].x,right:n.x+t[p].x+t[p].width,top:n.y+t[p].y,bottom:n.y+t[p].y+t[p].height}));for(const n of i.children){const p=we(e,n.id);for(const o of p){const s=R(`${e.compositionSpec.id}-${n.id}`,o);if(e.graphicSpec.objects[s]===void 0)continue;const c=q(e.graphicSpec,s,e.materializationConfigs.textMetrics);c!==void 0&&r.push({owner:n.id,bounds:c})}}const d=e.graphicSpec.objects[B(e.compositionSpec.id)];for(const n of d?.items??[])n.properties.text!==""&&r.push({owner:"headers",bounds:M(n.properties,e.materializationConfigs.textMetrics)});for(let n=0;n<r.length;n+=1){const p=r[n];if(l.some(o=>o.id!==p.owner&&L(p.bounds,o))||r.slice(0,n).some(o=>o.owner!==p.owner&&L(p.bounds,o.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function Fe(e,i){if(i.titles.length===0)return;const t=B(e.compositionSpec.id),r=e.materializationConfigs.textMetrics,l=e.graphicSpec.objects[t].items.slice(i.descriptors.length).map(n=>M(n.properties,r)),d=n=>{if((n.ownerId??n.id)===t)return;const p=n.object.type??n.owner?.type,o=n.object.properties;if(p!=="text"||o?.text===""||o?.opacity===0)return;const s=q(e.graphicSpec,n.id,r);if(s!==void 0&&l.some(c=>L(c,s)))throw new Error("Facet field title overlaps retained chart text.")};_(e.graphicSpec,{enter:d,item:d})}function We(e){const i=I(e),{layout:t,title:r,plot:l,plots:d,preparedHeaders:n}=Se(e,i),p=e.graphicSpec.objects.canvas?.properties?.background??"white";let o=he(e);o.graphicSpec.objects.canvas===void 0&&(o=o.createGraphics({id:"canvas",type:"canvas"}));for(const[s,c]of Object.entries({width:t.width,height:t.height,background:p}))o=o.editGraphics({target:"canvas",property:s,value:c});for(const s of t.children){const c=e.children[s.id],f=pe(c.graphicSpec,{namespace:`${e.compositionSpec.id}-${s.id}`,x:s.x,y:s.y});if(o=fe(o,f,f.order[0],"canvas"),t.spacing==="plot"){const m=f.order[0],b=R(`${e.compositionSpec.id}-${s.id}`,"plot-background"),h=G(c),x=o.graphicSpec.objects[m].children?.[0];o=o.editGraphics({target:m,property:"background",value:"transparent"}).createGraphics({id:b,type:"rect",parent:m,...x===void 0?{}:{before:x}});for(const[y,v]of Object.entries({...h,stroke:"none",strokeWidth:0,fill:c.graphicSpec.objects.canvas.properties.background??"transparent"}))o=o.editGraphics({target:b,property:y,value:v})}}return o=me(o,t,d,n),o=o.composeFacetGuides({layout:t,plot:l}),t.spacing==="plot"&&Ce(o,t,d),r.height>0&&(o=ye(o,l,r)),Fe(o,n),ae(o,{layout:t,plots:d,headerId:B(e.compositionSpec.id),titleIds:T}),o._withCanvasConfig({margin:be,size:{width:"auto",height:"auto"}})}export{We as materializeFacetGraphics,De as measureFacetContentChildren,Se as resolveFacetProgramLayout};
@@ -5,6 +5,7 @@ import {
5
5
  graphicSiblings
6
6
  } from "../grammar/schemas/graphicTree.js";
7
7
  import { findDataset } from "../selectors/datasets.js";
8
+ import { hasPlotUnderlays, firstOverlayGuide } from "./plotDrawOrder.js";
8
9
 
9
10
  export const CANVAS_GRAPHIC_ID = "canvas";
10
11
  export const PLOT_GRAPHIC_ID = "plot-main";
@@ -57,6 +58,10 @@ export function resolveMarkGraphicPlacement(program, { data, markType }) {
57
58
  ? dataset.transform[0]
58
59
  : undefined;
59
60
  if (transform?.type !== "regression") {
61
+ if (hasPlotUnderlays(program)) {
62
+ const before = firstOverlayGuide(program);
63
+ return resolvePlotGraphicPlacement(program, before === undefined ? {} : { before });
64
+ }
60
65
  const named = new Set(cartesianAxisOwners(program).filter(owner => owner.named)
61
66
  .flatMap(owner => axisGraphicIds(owner.channel, owner.id)));
62
67
  const axis = program.graphicSpec.objects[PLOT_GRAPHIC_ID]?.children?.find(
@@ -35,6 +35,8 @@ export function translatedProperties(type, properties, dx, dy) {
35
35
  export function translateGraphic(program, id, dx, dy) {
36
36
  const graphic = program.graphicSpec.objects[id];
37
37
  if (dx === 0 && dy === 0) return program;
38
+ if (graphic.clip !== undefined) program = program.editGraphics({target: id, property: "clip",
39
+ value: {...graphic.clip, x: graphic.clip.x + dx, y: graphic.clip.y + dy}});
38
40
  if (graphic.items !== undefined) {
39
41
  let next = program.editGraphics({
40
42
  target: id,
@@ -1 +1 @@
1
- import{resetLegendClipGraphics as u,hasLegendClipGraphics as c}from"../legendClip.js";import{localLegendPosition as g}from"../legendGroups.js";import{needsLegendAxisReflow as m}from"./resources.js";import{getErrorDetails as p}from"../../core/diagnostics.js";import{planCanvasRematerialization as h}from"../layout.js";import{applyMaterializationPlan as C}from"../planner.js";import{assertGuideCollisionBlocks as L}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as v}from"./resources.js";import{resolveGuideCollisionBlocks as M}from"./resources.js";function G(i,d){if(i.context.deferGuideLayoutValidation===!0)return d(i);const a=i.materializationConfigs.canvas?.plot;let n=i.materializationConfigs.canvas?.margin,e;for(let s=0;;s+=1)try{let t=u(i)._withContext({deferGuideLayoutValidation:!0});s>0&&(t=t.editGraphics({target:"canvas",property:"width",value:a.width+n.left+n.right}).editGraphics({target:"canvas",property:"height",value:a.height+n.top+n.bottom})._withCanvasConfig({...t.materializationConfigs.canvas,margin:n}),t=C(t,h(t))),e=d(t),c(i)&&!c(e)&&Object.values(e.guideConfigs.legend??{}).some(o=>o.clipHeight!==void 0)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&Object.values(e.guideConfigs.legend??{}).some(o=>["left","right"].includes(o.position))&&(e=e.rematerializeSideLegendLane()),e.compositionSpec===void 0&&m(i,e)&&(e=e.rematerializeHorizontalLegendLane()),e.compositionSpec===void 0&&(Object.keys(e.guideConfigs.legendGroups??{}).length>0||Object.values(e.guideConfigs.legend??{}).some(o=>g(o)!==void 0))&&(e=e.rematerializeLegendGroups()),a!==void 0&&e.titleConfig!==void 0&&(e=e.rematerializeTitle());break}catch(t){const o=p(t)?.canvasOverflow;if(a===void 0||o===void 0||s>=16)throw t;n=Object.fromEntries(Object.keys(n).map(l=>[l,o[l]>0?Math.ceil((n[l]+o[l])*4)/4:n[l]]))}const{deferGuideLayoutValidation:y,...f}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(f.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const r=e._clone({context:f});return L(v(r.graphicSpec,r.guideConfigs,r.titleConfig,r.materializationConfigs.textMetrics)),r}function S(i){return function(d){return G(this,a=>i.call(a,d))}}export{M as resolveGuideCollisionBlocks,G as withGuideLayoutTransaction,S as withGuideLayoutValidation};
1
+ import{resetLegendClipGraphics as m,hasLegendClipGraphics as c}from"../legendClip.js";import{localLegendPosition as g}from"../legendGroups.js";import{needsLegendAxisReflow as p,needsSideLegendAxisReflow as h}from"./resources.js";import{getErrorDetails as C}from"../../core/diagnostics.js";import{planCanvasRematerialization as L}from"../layout.js";import{applyMaterializationPlan as v}from"../planner.js";import{assertGuideCollisionBlocks as G}from"../../layout/guideCollisions.js";import{resolveGuideCollisionBlocks as y}from"./resources.js";import{hasPlotUnderlays as u}from"../plotDrawOrder.js";import{resolveGuideCollisionBlocks as A}from"./resources.js";function z(i,l){if(i.context.deferGuideLayoutValidation===!0)return l(i);const a=i.materializationConfigs.canvas?.plot;let n=i.materializationConfigs.canvas?.margin,e;for(let s=0;;s+=1)try{let t=m(i)._withContext({deferGuideLayoutValidation:!0});s>0&&(t=t.editGraphics({target:"canvas",property:"width",value:a.width+n.left+n.right}).editGraphics({target:"canvas",property:"height",value:a.height+n.top+n.bottom})._withCanvasConfig({...t.materializationConfigs.canvas,margin:n}),t=v(t,L(t))),e=l(t),c(i)&&!c(e)&&Object.values(e.guideConfigs.legend??{}).some(o=>o.clipHeight!==void 0)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&h(i,e)&&(e=e.rematerializeLegend()),e.compositionSpec===void 0&&Object.values(e.guideConfigs.legend??{}).some(o=>["left","right"].includes(o.position))&&(e=e.rematerializeSideLegendLane()),e.compositionSpec===void 0&&p(i,e)&&(e=e.rematerializeHorizontalLegendLane()),e.compositionSpec===void 0&&(Object.keys(e.guideConfigs.legendGroups??{}).length>0||Object.values(e.guideConfigs.legend??{}).some(o=>g(o)!==void 0))&&(e=e.rematerializeLegendGroups()),a!==void 0&&e.titleConfig!==void 0&&(e=e.rematerializeTitle()),(u(i)||u(e))&&(e=e.rematerializePlotDrawOrder());break}catch(t){const o=C(t)?.canvasOverflow;if(a===void 0||o===void 0||s>=16)throw t;n=Object.fromEntries(Object.keys(n).map(d=>[d,o[d]>0?Math.ceil((n[d]+o[d])*4)/4:n[d]]))}const{deferGuideLayoutValidation:w,...f}=e.context;Object.hasOwn(i.context,"deferGuideLayoutValidation")&&(f.deferGuideLayoutValidation=i.context.deferGuideLayoutValidation);const r=e._clone({context:f});return G(y(r.graphicSpec,r.guideConfigs,r.titleConfig,r.materializationConfigs.textMetrics)),r}function B(i){return function(l){return z(this,a=>i.call(a,l))}}export{A as resolveGuideCollisionBlocks,z as withGuideLayoutTransaction,B as withGuideLayoutValidation};
@@ -1 +1 @@
1
- import{axisGraphicIds as h}from"../../selectors/axes.js";import{localLegendPosition as m}from"../legendGroups.js";import{resolveConcreteGraphicBounds as G,unionConcreteGraphicBounds as v}from"../../grammar/schemas/graphicBounds.js";function y(e,t,s,o){const n=[],r=(i,a,l,c)=>{if(l===void 0)return;const p=c.filter(b=>e.objects[b]!==void 0),g=v(e,p,o);g!==void 0&&n.push({id:i,kind:a,position:l,bounds:g})};r("chart title","title",s?.position,["chartTitle","chartSubtitle"]);for(const[i,a]of Object.entries(t.axis??{})){const l=["line","ticks","labels","title"].map(p=>a[p]?.position).find(Boolean),c=["x","y"].includes(i)?i:["top","bottom"].includes(l)?"x":["left","right"].includes(l)?"y":void 0;if(c!==void 0)for(const[p,g]of["line","ticks","labels","title"].entries()){const b=h(c,i)[p],I=n.length;if(r(`${i}-axis ${g}`,"axis",a[g]?.position,[b]),n.length===I)continue;const z=n.at(-1);if(z.axisId=i,z.namedAxis=i!==c,["labels","title"].includes(g)){const x=e.objects[b];z.textBounds=x.items?.map(S=>G(e,S.id,o))??[z.bounds]}}}const f=T(),d=t.legend??{},u=f.find(i=>i.family==="categorical"&&d[i.kind]!==void 0),k=u!==void 0&&d.size!==void 0&&d[u.kind].target===d.size.target&&m(d[u.kind])===void 0&&m(d.size)===void 0&&(d.size.inheritPosition!==!1||d[u.kind].position===d.size.position),L=t.legendGroups??{};for(const[i,a]of Object.entries(L)){const l=f.filter(c=>d[c.kind]?.target===i&&m(d[c.kind])===void 0).flatMap(c=>c.graphicIds);r(`${i} legend group`,"legend",a.placement.position??"free",l)}for(const i of f){const a=d[i.kind];if(L[a?.target]!==void 0&&m(a)===void 0||a===void 0||k&&i.kind==="size")continue;const l=k&&i===u?[...i.graphicIds,...f.find(c=>c.kind==="size").graphicIds]:i.graphicIds;r(`${i.kind}${k&&i===u?"+size":""} legend`,"legend",m(a)??a.position,l)}return n}const O=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),j=Object.freeze({colorSize:Object.freeze({semanticKind:"colorSize",family:"sampled",rematerializeOp:void 0,graphicIds:Object.freeze(["colorSizeLegendBackground","colorSizeLegendSymbols","colorSizeLegendLabels","colorSizeLegendTitle"])}),series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function B(e){return C(e).graphicIds}function C(e){const t=j[e];if(t===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return t}function T(){return Object.entries(j).map(([e,t])=>({kind:e,...t}))}function W(e){return[...new Set(e.flatMap(B))]}function w(e,t,s){if(t.inferredOffset!==!0||!["top","bottom"].includes(t.position)||t.layout==="legacy-bottom")return t.offset;const o=y(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(r=>r.kind==="axis"&&r.position===t.position),n=t.position==="bottom"?Math.max(0,...o.map(r=>r.bounds.bottom-s.y-s.height)):Math.max(0,...o.map(r=>s.y-r.bounds.top));return t.offset+n}function K(e,t,s){if(t!=="size"||s.inheritPosition===!1)return s;const o=["series","color","stroke"].map(n=>e.guideConfigs.legend?.[n]).find(n=>n?.target===s.target);return o===void 0?s:{...s,position:o.position,offset:o.offset,inferredOffset:o.inferredOffset}}function M(e,t){return[e,t].some(o=>Object.entries(o.guideConfigs.legend??{}).some(([n,r])=>{const f=K(o,n,r);return f.inferredOffset===!0&&["top","bottom"].includes(f.position)&&f.layout!=="legacy-bottom"})||Object.values(o.guideConfigs.legendGroups??{}).some(n=>n.inferredOffset===!0&&["top","bottom"].includes(n.placement.position)))&&[e,t].some(o=>y(o.graphicSpec,o.guideConfigs,void 0,o.materializationConfigs.textMetrics).some(n=>n.kind==="axis"&&["top","bottom"].includes(n.position)))}export{W as allLegendGraphicIds,h as axisGraphicIds,K as effectiveLegendOffsetConfig,B as legendGraphicIds,T as legendResourcePolicies,C as legendResourcePolicy,M as needsLegendAxisReflow,y as resolveGuideCollisionBlocks,w as resolveLegendAxisOffset};
1
+ import{axisGraphicIds as k}from"../../selectors/axes.js";import{localLegendPosition as z,localLegendPlacement as C}from"../legendGroups.js";import{resolveConcreteGraphicBounds as B,unionConcreteGraphicBounds as x}from"../../grammar/schemas/graphicBounds.js";function T(e,i,n,s){const t=[],r=(o,a,u,l)=>{if(u===void 0)return;const b=l.filter(O=>e.objects[O]!==void 0),m=x(e,b,s);m!==void 0&&t.push({id:o,kind:a,position:u,bounds:m})};r("chart title","title",n?.position,["chartTitle","chartSubtitle"]);for(const[o,a]of Object.entries(i.axis??{})){const u=["line","ticks","labels","title"].map(b=>a[b]?.position).find(Boolean),l=["x","y"].includes(o)?o:["top","bottom"].includes(u)?"x":["left","right"].includes(u)?"y":void 0;if(l!==void 0)for(const[b,m]of["line","ticks","labels","title"].entries()){const O=k(l,o)[b],I=t.length;if(r(`${o}-axis ${m}`,"axis",a[m]?.position,[O]),t.length===I)continue;const h=t.at(-1);if(h.axisId=o,h.namedAxis=o!==l,["labels","title"].includes(m)){const G=e.objects[O];h.textBounds=G.items?.map(v=>B(e,v.id,s))??[h.bounds]}}}const c=$(),d=i.legend??{},f=c.find(o=>o.family==="categorical"&&d[o.kind]!==void 0),g=f!==void 0&&d.size!==void 0&&d[f.kind].target===d.size.target&&z(d[f.kind])===void 0&&z(d.size)===void 0&&(d.size.inheritPosition!==!1||d[f.kind].position===d.size.position),p=i.legendGroups??{};for(const[o,a]of Object.entries(p)){const u=c.filter(l=>d[l.kind]?.target===o&&z(d[l.kind])===void 0).flatMap(l=>l.graphicIds);r(`${o} legend group`,"legend",a.placement.position??"free",u)}for(const o of c){const a=d[o.kind];if(p[a?.target]!==void 0&&z(a)===void 0||a===void 0||g&&o.kind==="size")continue;const u=g&&o===f?[...o.graphicIds,...c.find(l=>l.kind==="size").graphicIds]:o.graphicIds;r(`${o.kind}${g&&o===f?"+size":""} legend`,"legend",z(a)??a.position,u)}return t}const L=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),j=Object.freeze({colorSize:Object.freeze({semanticKind:"colorSize",family:"sampled",rematerializeOp:void 0,graphicIds:Object.freeze(["colorSizeLegendBackground","colorSizeLegendSymbols","colorSizeLegendLabels","colorSizeLegendTitle"])}),series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(L.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function K(e){return w(e).graphicIds}function w(e){const i=j[e];if(i===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return i}function $(){return Object.entries(j).map(([e,i])=>({kind:e,...i}))}function R(e){return[...new Set(e.flatMap(K))]}function y(e){const i=T(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(n=>n.kind==="axis");for(const n of["theta","radius"]){if(e.guideConfigs.axis?.[n]===void 0)continue;const s=k(n).slice(2).filter(r=>e.graphicSpec.objects[r]!==void 0),t=x(e.graphicSpec,s,e.materializationConfigs.textMetrics);if(t!==void 0)for(const r of["left","right","top","bottom"])i.push({id:`${n}-axis text`,kind:"axis",position:r,bounds:t})}return i}function A(e,i,n){if(i.inferredOffset!==!0||!["top","bottom","left","right"].includes(i.position)||i.layout==="legacy-bottom")return i.offset;const s=y(e).filter(r=>r.position===i.position),t=Math.max(0,...s.map(r=>({bottom:r.bounds.bottom-n.y-n.height,top:n.y-r.bounds.top,left:n.x-r.bounds.left,right:r.bounds.right-n.x-n.width})[i.position]));return i.offset+t}function S(e,i,n){if(i!=="size"||n.inheritPosition===!1)return n;const s=["series","color","stroke"].map(t=>e.guideConfigs.legend?.[t]).find(t=>t?.target===n.target);return s===void 0?n:{...n,position:s.position,offset:s.offset,inferredOffset:s.inferredOffset}}function M(e,i){return[e,i].some(s=>Object.entries(s.guideConfigs.legend??{}).some(([t,r])=>{const c=S(s,t,r);return c.inferredOffset===!0&&["top","bottom"].includes(c.position)&&c.layout!=="legacy-bottom"})||Object.values(s.guideConfigs.legendGroups??{}).some(t=>t.inferredOffset===!0&&["top","bottom"].includes(t.placement.position)))&&[e,i].some(s=>y(s).some(t=>["top","bottom"].includes(t.position)))}function N(e,i){const n=new Set;for(const t of[e,i])for(const[r,c]of Object.entries(t.guideConfigs.legend??{})){const d=C(c),f=d===void 0?t.guideConfigs.legendGroups?.[c.target]:void 0,g=S(t,r,c),p=d??f?.placement??g;(d?.inferredOffset??f?.inferredOffset??g.inferredOffset)===!0&&p.at===void 0&&["left","right"].includes(p.position)&&g.layout!=="legacy-bottom"&&n.add(p.position)}const s=t=>JSON.stringify(y(t).filter(r=>n.has(r.position)));return n.size>0&&s(e)!==s(i)}export{R as allLegendGraphicIds,k as axisGraphicIds,S as effectiveLegendOffsetConfig,K as legendGraphicIds,$ as legendResourcePolicies,w as legendResourcePolicy,M as needsLegendAxisReflow,N as needsSideLegendAxisReflow,T as resolveGuideCollisionBlocks,A as resolveLegendAxisOffset};
@@ -2,7 +2,7 @@ import { findLayer } from "../selectors/layers.js";
2
2
  import { LEGEND_CONFIG_KINDS } from "../core/vocabulary.js";
3
3
 
4
4
  export function isOpacityLegendLayer(layer) {
5
- return ["point", "line", "bar"].includes(layer.mark?.type) &&
5
+ return ["point", "line", "bar", "rect"].includes(layer.mark?.type) &&
6
6
  layer.encoding?.opacity?.scale !== undefined;
7
7
  }
8
8
 
@@ -0,0 +1,26 @@
1
+ import { findLayer } from "../selectors/layers.js";
2
+ import { resolvePrimitiveGraphicBounds } from "../grammar/schemas/graphicBounds.js";
3
+ import { resolveCoordinateBounds } from "./coordinateBounds.js";
4
+ import { markClipPositionOwner, validateMarkClipCoordinate } from "../selectors/markClip.js";
5
+
6
+ // Called explicitly by the owning mark materializer, using wrapped primitive edits.
7
+ export function materializeMarkClip(program, id) {
8
+ const intent = program.markConfigs[id]?.clip;
9
+ if (intent === undefined) return program;
10
+ const graphic = program.graphicSpec.objects[id];
11
+ if (intent === false) return graphic?.clip === undefined ? program
12
+ : program.editGraphics({target: id, property: "clip", value: false});
13
+ const layer = markClipPositionOwner(program, findLayer(program, id));
14
+ validateMarkClipCoordinate(program, layer, intent);
15
+ // Incomplete marks keep authoring intent until a coordinate and Canvas exist.
16
+ const bounds = layer?.coordinate === undefined ? undefined : resolveCoordinateBounds(program, layer.coordinate);
17
+ if (bounds === undefined) {
18
+ const drawable = (graphic?.items ?? (graphic ? [graphic] : [])).some(item =>
19
+ resolvePrimitiveGraphicBounds(item.type ?? graphic.type, item.properties) !== undefined);
20
+ if (drawable) throw new Error("Drawable mark clip requires a resolved Cartesian plot frame.");
21
+ return program;
22
+ }
23
+ const rect = {x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height};
24
+ if (graphic?.clip !== undefined && Object.keys(rect).every(key => rect[key] === graphic.clip[key])) return program;
25
+ return program.editGraphics({target: id, property: "clip", value: rect});
26
+ }
@@ -190,6 +190,7 @@ export function canMaterializeText(program, layer) {
190
190
  return hasCartesianPositionScales(layer);
191
191
  }
192
192
 
193
- export function canMaterializeRect(_program, layer) {
194
- return resolveRectMode(layer) !== undefined;
193
+ export function canMaterializeRect(program, layer) {
194
+ return program.markConfigs[layer.id]?.gradientPlot?.materialized === true ||
195
+ resolveRectMode(layer) !== undefined;
195
196
  }
@@ -1,13 +1,15 @@
1
1
  import { findUpstreamTransform } from "../dataProvenance.js";
2
- import { readPathSeriesFields, validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
2
+ import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
3
 
4
4
  export function validatePathGroupAppearance(program, layer, dataset) {
5
- if (layer.mark.type === "area" && findUpstreamTransform(program, dataset, "density")) {
5
+ const density = layer.mark.type === "area" &&
6
+ findUpstreamTransform(program, dataset, "density");
7
+ if (density) {
6
8
  // Density placement owns a category/split grain beyond the visible group field.
7
- readPathSeriesFields(dataset.values, layer);
8
- return;
9
+ return validatePathSeriesAppearance(dataset.values, layer, program.semanticSpec.scales,
10
+ [density.groupBy, density.placement?.split?.field].filter(Boolean));
9
11
  }
10
- validatePathSeriesAppearance(dataset.values, layer);
12
+ validatePathSeriesAppearance(dataset.values, layer, program.semanticSpec.scales);
11
13
  }
12
14
 
13
15
  export function assertPathGroupCompatible(program, layer, dataset, group) {
@@ -62,7 +62,8 @@ export function getSourceDependentMarkSteps(program, sourceId) {
62
62
  getMarkMaterializationPolicy(layer)?.sourceDependent === true;
63
63
  const statistical = program.markConfigs?.[layer.id]
64
64
  ?.statisticalReference?.source === sourceId;
65
- return sourceOwned || statistical
65
+ const filteredCompanion = program.markConfigs?.[layer.id]?.markFilter?.owner === sourceId;
66
+ return sourceOwned || statistical || filteredCompanion
66
67
  ? [getMarkMaterializationStep(program, layer) ??
67
68
  getExistingMarkRematerializationStep(program, layer)].filter(
68
69
  step => step !== undefined
@@ -166,7 +167,8 @@ export function reusePlannedMarkScales(program, steps, scaleIds) {
166
167
  const resolved = new Set(scaleIds);
167
168
  return steps.map(step => {
168
169
  const layer = findLayer(program, step.args?.id);
169
- if (layer === undefined || getMarkMaterializationPolicy(layer)?.acceptsResolvedScales !== true ||
170
+ const policy = layer === undefined ? undefined : getMarkMaterializationPolicy(layer);
171
+ if (policy?.acceptsResolvedScales !== true || step.op !== policy.op ||
170
172
  !getLayerScaleIds(layer).every(id => resolved.has(id))) return step;
171
173
  return { ...step, args: { ...step.args, scales: false } };
172
174
  });
@@ -48,6 +48,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
48
48
  area: Object.freeze({
49
49
  canMaterialize: canMaterializeArea,
50
50
  op: "rematerializeAreaMark",
51
+ acceptsResolvedScales: true,
51
52
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
52
53
  encoding: Object.freeze({
53
54
  sharedChannels: Object.freeze(["color", "stroke"]),
@@ -66,12 +67,14 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
66
67
  bar: Object.freeze({
67
68
  canMaterialize: canMaterializeBar,
68
69
  op: "rematerializeBarMark",
70
+ acceptsResolvedScales: true,
69
71
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
70
72
  scaleApplication: Object.freeze({ default: "defer" })
71
73
  }),
72
74
  rule: Object.freeze({
73
75
  canMaterialize: canMaterializeRule,
74
76
  op: "rematerializeRuleMark",
77
+ acceptsResolvedScales: true,
75
78
  positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: false }),
76
79
  scaleApplication: Object.freeze({ default: "defer" })
77
80
  }),
@@ -91,6 +94,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
91
94
  rect: Object.freeze({
92
95
  canMaterialize: canMaterializeRect,
93
96
  op: "rematerializeRectMark",
97
+ acceptsResolvedScales: true,
94
98
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
95
99
  encoding: Object.freeze({ scaleFirst: true }),
96
100
  scaleApplication: Object.freeze({
@@ -0,0 +1,44 @@
1
+ import { cartesianAxisOwners, axisGraphicIds, PARALLEL_AXIS_GRAPHICS } from "../selectors/axes.js";
2
+ import { gridNames } from "../selectors/grids.js";
3
+ import { resolvePlotDrawOrder } from "../layout/plotDrawOrder.js";
4
+
5
+ export function hasPlotUnderlays(program) {
6
+ return program.compositionSpec === undefined && (
7
+ program.materializationConfigs.canvas?.plotBorder?.underlay === true ||
8
+ cartesianAxisOwners(program).some(owner => owner.config?.frame?.underlay === true)
9
+ );
10
+ }
11
+
12
+ export function plotDrawOrderResources(program) {
13
+ const axes = cartesianAxisOwners(program).map(owner => ({
14
+ ids: axisGraphicIds(owner.channel, owner.id), underlay: owner.config?.frame?.underlay === true
15
+ }));
16
+ for (const channel of ["theta", "radius"]) if (program.semanticSpec.guides.axis?.[channel] !== undefined) {
17
+ axes.push({ ids: axisGraphicIds(channel), underlay: false });
18
+ }
19
+ if (program.semanticSpec.guides.axis?.parallel !== undefined) axes.push({ ids: Object.values(PARALLEL_AXIS_GRAPHICS), underlay: false });
20
+ return {
21
+ children: program.graphicSpec.objects["plot-main"]?.children ?? [],
22
+ grids: ["horizontal", "vertical"].flatMap((direction, index) => {
23
+ const config = program.guideConfigs.grid?.[direction];
24
+ return config === undefined ? [] : [{ id: gridNames(direction).graphic, zIndex: config.zIndex, index }];
25
+ }),
26
+ border: program.materializationConfigs.canvas?.plotBorder === undefined ? undefined : {
27
+ id: "plotBorder", underlay: program.materializationConfigs.canvas.plotBorder.underlay === true
28
+ }, axes
29
+ };
30
+ }
31
+
32
+ export function resolveProgramPlotDrawOrder(program) {
33
+ return resolvePlotDrawOrder(plotDrawOrderResources(program));
34
+ }
35
+
36
+ export function firstOverlayGuide(program) {
37
+ const { children, grids, border, axes } = plotDrawOrderResources(program);
38
+ const overlay = new Set([
39
+ ...grids.filter(grid => grid.zIndex > 0).map(grid => grid.id),
40
+ ...(border?.underlay === false ? [border.id] : []),
41
+ ...axes.filter(axis => !axis.underlay).flatMap(axis => axis.ids)
42
+ ]);
43
+ return children.find(id => overlay.has(id));
44
+ }
@@ -27,7 +27,7 @@ function requiredChannels(layer, mode) {
27
27
  return [
28
28
  ...position,
29
29
  ...(layer.encoding?.color ? ["color"] : []),
30
- ...(layer.encoding?.stroke ? ["stroke"] : [])
30
+ ...["stroke", "opacity", "strokeWidth"].filter(channel => layer.encoding?.[channel])
31
31
  ];
32
32
  }
33
33
 
@@ -96,6 +96,10 @@ export function resolveRectRows(program, layer, dataset) {
96
96
  const stroke = layer.encoding?.stroke === undefined
97
97
  ? undefined
98
98
  : mappedEncoding(program, layer, dataset, "stroke").values;
99
+ const opacity = layer.encoding?.opacity === undefined ? undefined
100
+ : mappedEncoding(program, layer, dataset, "opacity").values;
101
+ const strokeWidth = layer.encoding?.strokeWidth === undefined ? undefined
102
+ : mappedEncoding(program, layer, dataset, "strokeWidth").values;
99
103
  const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
100
104
 
101
105
  const rows = rectUsesFields(layer) ? dataset.values : [{}];
@@ -105,7 +109,9 @@ export function resolveRectRows(program, layer, dataset) {
105
109
  (x !== undefined && !Number.isFinite(x.values[index])) ||
106
110
  (y !== undefined && !Number.isFinite(y.values[index])) ||
107
111
  typeof fill !== "string" ||
108
- (stroke !== undefined && typeof stroke[index] !== "string")
112
+ (stroke !== undefined && typeof stroke[index] !== "string") ||
113
+ (opacity !== undefined && !Number.isFinite(opacity[index])) ||
114
+ (strokeWidth !== undefined && !Number.isFinite(strokeWidth[index]))
109
115
  ) return [];
110
116
 
111
117
  let geometry;
@@ -138,7 +144,9 @@ export function resolveRectRows(program, layer, dataset) {
138
144
  return [{
139
145
  row,
140
146
  sourceIndex: index,
141
- properties: { ...geometry, ...appearance(config, fill, stroke?.[index]) }
147
+ properties: { ...geometry, ...appearance(config, fill, stroke?.[index]),
148
+ ...(opacity === undefined ? {} : { opacity: opacity[index] }),
149
+ ...(strokeWidth === undefined ? {} : { strokeWidth: strokeWidth[index] }) }
142
150
  }];
143
151
  });
144
152
  }
@@ -16,14 +16,22 @@ const GRIDS = Object.freeze([
16
16
  Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
17
17
  ]);
18
18
 
19
- export function planAxisTickGridRematerialization(program, channel, axisId = channel) {
19
+ export function planAxisTickGridRematerialization(program, channel, axisId = channel, { syncLabels = false } = {}) {
20
20
  const ticks = program.guideConfigs.axis?.[axisId]?.ticks;
21
21
  const role = channel === "x" ? "vertical" : "horizontal";
22
22
  const config = program.guideConfigs.grid?.[role];
23
- return buildMaterializationPlan({ guides:
24
- config?.inferredValues === true && config.scale === ticks?.scale
25
- ? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : []
26
- });
23
+ const labels = syncLabels && program.guideConfigs.axis?.[axisId]?.labels
24
+ ? [{ op: `edit${channel.toUpperCase()}AxisLabels`, args: {
25
+ ...axisTargetArgs(axisId, channel),
26
+ ...(ticks.mode === "values" ? { values: ticks.values } : {
27
+ count: ticks.count,
28
+ ...(ticks.rounding === undefined ? {} : { rounding: ticks.rounding })
29
+ })
30
+ } }] : [];
31
+ return buildMaterializationPlan({ guides: [...labels,
32
+ ...(config?.inferredValues === true && config.scale === ticks?.scale
33
+ ? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : [])
34
+ ] });
27
35
  }
28
36
 
29
37
  function usesPositionalScale(program, id) {
@@ -102,7 +102,7 @@ function collectionBounds(program, items) {
102
102
  const bounds = unionConcreteGraphicBounds(
103
103
  program.graphicSpec,
104
104
  items.map(item => item.id)
105
- , program.materializationConfigs.textMetrics);
105
+ , program.materializationConfigs.textMetrics, {includeOwnerClips: false});
106
106
  if (bounds === undefined) return {};
107
107
  const { left, top, right, bottom } = bounds;
108
108
  return { x: left, y: top, width: right - left, height: bottom - top };
@@ -60,6 +60,7 @@ export function resolveLineItems(program, layer, dataset) {
60
60
  dataset.values,
61
61
  layer,
62
62
  {
63
+ scales: program.semanticSpec.scales,
63
64
  xDomain: program.resolvedScales[x?.scale]?.domain,
64
65
  ...(x?.bin === undefined ? {} : {
65
66
  xBinBoundaries: resolveLineBins(
@@ -8,6 +8,7 @@ import { pointSelectionPolicy } from "./point.js";
8
8
  import { ruleSelectionPolicy } from "./rule.js";
9
9
  import { tickSelectionPolicy } from "./tick.js";
10
10
  import { rectSelectionPolicy } from "./rect.js";
11
+ import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
11
12
 
12
13
  const POLICIES = Object.freeze({
13
14
  arc: arcSelectionPolicy,
@@ -42,5 +43,5 @@ export function resolveMarkItems(program, target, grain = "item") {
42
43
  if (!policy.supportedGrains.includes(grain)) {
43
44
  throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
44
45
  }
45
- return policy.resolveItems(program, layer, dataset, grain);
46
+ return policy.resolveItems(program, layer, applyItemMissingPolicy(layer, dataset), grain);
46
47
  }