ggaction 0.0.16 → 0.0.18

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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +10 -4
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -1,9 +1,31 @@
1
+ import { readScaleField } from "../../grammar/scales/index.js";
2
+ import { mapScaleConsumerValues } from "../scales/map.js";
3
+
4
+ // Appearance belongs to a final bar cell and never introduces another group.
5
+ export function resolveBarCellOpacities(program, layer, rowsByCell) {
6
+ const encoding = layer.encoding?.opacity;
7
+ if (encoding === undefined) return undefined;
8
+ const scale = program.resolvedScales[encoding.scale];
9
+ const values = rowsByCell.map(rows => {
10
+ const values = readScaleField(rows, encoding.field, encoding.fieldType, {
11
+ allowUnknown: Object.hasOwn(scale, "unknown")
12
+ });
13
+ if (new Set(values).size !== 1) {
14
+ throw new Error("Bar opacity requires one value within each final cell.");
15
+ }
16
+ return values[0];
17
+ });
18
+ return mapScaleConsumerValues(values, scale, "opacity");
19
+ }
20
+
21
+ import { hasRectRadius } from "../../grammar/roundedRect.js";
1
22
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
2
23
  import { findDataset } from "../../selectors/datasets.js";
3
24
  import { findLayer } from "../../selectors/layers.js";
4
25
  import { findSemanticScale } from "../../selectors/scales.js";
5
26
  import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
6
27
  import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
28
+ import { applyItemMissingPolicy } from "../../grammar/itemMissing.js";
7
29
 
8
30
  export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
9
31
  export const DEFAULT_BAR_STROKE = "white";
@@ -37,13 +59,14 @@ export function requireCompleteBar(program, id) {
37
59
  if (layer?.mark?.type !== "bar") {
38
60
  throw new Error(`Unknown bar mark "${id}".`);
39
61
  }
40
- const dataset = findDataset(program, layer.data);
41
- if (dataset === undefined) {
62
+ const sourceDataset = findDataset(program, layer.data);
63
+ if (sourceDataset === undefined) {
42
64
  throw new Error(`Bar mark "${id}" requires an existing dataset.`);
43
65
  }
66
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
44
67
  const graphic = program.graphicSpec.objects[id];
45
68
  const roundedCollection = graphic?.type === "collection" &&
46
- Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
69
+ hasRectRadius(program.markConfigs[id]?.barAppearance) &&
47
70
  graphic.items?.every(child => ["rect", "path"].includes(child.type));
48
71
  if (graphic?.type !== "rect" && !roundedCollection) {
49
72
  throw new Error(`Bar mark "${id}" requires rect graphics.`);
@@ -202,3 +202,11 @@ export function resolveGuideCoordinateBounds(program, {
202
202
  }
203
203
  return resolveCoordinateBounds(program, target, { resolvedScales });
204
204
  }
205
+
206
+ // Cartesian guide bindings are semantic scale consumers without data rows.
207
+ export function axisScaleBindings(program, id) {
208
+ return Object.entries(program.semanticSpec.guides.axis ?? {}).flatMap(([channel, guide]) =>
209
+ ["x", "y"].includes(channel) && guide.scale === id && guide.coordinate !== undefined &&
210
+ program.materializationConfigs?.guides?.axis?.[channel]?.binding?.standalone === true
211
+ ? [{ channel, coordinate: guide.coordinate }] : []);
212
+ }
@@ -1,6 +1,5 @@
1
1
  import { withGuideLayoutTransaction } from "./guides/layout.js";
2
2
  import {
3
- canDeferScaleConsumerApplication,
4
3
  getLayerScaleIds,
5
4
  getExistingMarkRematerializationStep,
6
5
  getMarkMaterializationStep,
@@ -13,54 +12,13 @@ import {
13
12
  applyMaterializationPlan as executeMaterializationPlan,
14
13
  buildMaterializationPlan
15
14
  } from "./planner.js";
16
- import { hasMaterializedLegend } from "./legends.js";
17
15
  import {
18
- needsCanvasScaleRematerialization,
19
16
  planScaleGuideRematerialization
20
17
  } from "./scaleGuideDependencies.js";
21
18
  import { planLayoutRematerialization } from "./layout.js";
22
19
  import { requireCoordinate } from "../selectors/coordinates.js";
23
20
 
24
- export function planCanvasRematerialization(program) {
25
- const marks = [];
26
- for (const layer of program.semanticSpec.layers) {
27
- const step = getMarkMaterializationStep(program, layer);
28
- if (step !== undefined) marks.push(step);
29
- }
30
- const deferredMarkIds = new Set(
31
- marks
32
- .map(step => step.args.id)
33
- );
34
- const scales = [];
35
- for (const scale of program.semanticSpec.scales) {
36
- if (needsCanvasScaleRematerialization(program, scale)) {
37
- const deferredConsumers = program.semanticSpec.layers.filter(layer =>
38
- canDeferScaleConsumerApplication(layer) &&
39
- getLayerScaleIds(layer).includes(scale.id)
40
- );
41
- const canDeferMarks = deferredConsumers.length === 0 ||
42
- deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
43
- scales.push({
44
- op: "rematerializeScale",
45
- args: {
46
- id: scale.id,
47
- guides: false,
48
- ...(canDeferMarks ? { marks: false } : {})
49
- }
50
- });
51
- }
52
- }
53
- const guides = program.semanticSpec.scales.flatMap(scale =>
54
- needsCanvasScaleRematerialization(program, scale)
55
- ? planScaleGuideRematerialization(program, scale.id)
56
- : []
57
- );
58
- if (hasMaterializedLegend(program)) {
59
- guides.push({ op: "rematerializeLegend" });
60
- }
61
- const layout = planLayoutRematerialization(program);
62
- return buildMaterializationPlan({ scales, marks, guides, layout });
63
- }
21
+ export { planCanvasRematerialization } from "./layout.js";
64
22
 
65
23
  export function planCoordinateRematerialization(program, target) {
66
24
  requireCoordinate(program, target);
@@ -74,7 +32,9 @@ export function planCoordinateRematerialization(program, target) {
74
32
  ...(layer.encoding?.parallel?.dimensions ?? []).map(
75
33
  dimension => dimension.scale
76
34
  )
77
- ]).filter(id => id !== undefined))];
35
+ ]).filter(id => id !== undefined)),
36
+ ...Object.values(program.semanticSpec.guides.axis ?? {}).filter(guide => guide.coordinate === target)
37
+ .map(guide => guide.scale).filter(id => id !== undefined)];
78
38
  const directMarks = layers
79
39
  .map(layer => getMarkMaterializationStep(program, layer))
80
40
  .filter(step => step !== undefined);
@@ -0,0 +1 @@
1
+ import{PLOT_LEGEND_POSITIONS as B}from"../../layout/legend.js";import{resolveConcreteGraphicBounds as G}from"../../grammar/schemas/graphicBounds.js";import{resolveFacetOuterAxes as I}from"../../grammar/facets/guides.js";import{resolveTextBounds as L,textBoundsIntersect as S}from"../../core/textMetrics.js";import{axisGraphicIds as y}from"../guides/resources.js";import{resolveFacetHeaderItems as F}from"../facetHeaders.js";function p(s,t,e){return{left:s.left+t,right:s.right+t,top:s.top+e,bottom:s.bottom+e}}function P(s,t,e,m){const c=s.materializationConfigs.textMetrics,d=I({placements:t.children}),i=[];for(const n of t.children){const a=s.children[n.id];for(const g of["x","y"])if(!(s.compositionSpec.facet.guides.axes==="outer"&&!d.children[n.id][g]))for(const f of y(g)){if(!/Labels|Title/.test(f)||a.graphicSpec.objects[f]===void 0)continue;const x=G(a.graphicSpec,f,c);x!==void 0&&i.push(p(x,n.x,n.y))}}const{items:h}=F(t,e,m);for(const n of h)n.properties.text!==""&&i.push(L(n.properties,c));return i}function w(s,t,e,m,c){const d=s.right-s.left,i=s.bottom-s.top,h=e==="right"?t.x+t.width+c:e==="left"?t.x-c-d:m==="left"?t.x:m==="right"?t.x+t.width-d:t.x+(t.width-d)/2,n=["left","right"].includes(e)?t.y:e==="top"?t.y-c-i:t.y+t.height+c;return{left:h,right:h+d,top:n,bottom:n+i}}function j(s,{layout:t,plot:e,plots:m,title:c,preparedHeaders:d,prepared:i}){if(i?.mode!=="promoted")return{layout:t,plot:e};const h=i.occupiedGroup;if(h?.placement.at!==void 0||B.includes(h?.placement.position))return{layout:t,plot:e};const n=i.reservation.position,a=i.reservation.align,g=i.kinds.map(r=>i.source.guideConfigs.legend[r]),f=i.reservation.gap,x=h===void 0?g.every(r=>r.inferredOffset===!0):h.inferredOffset===!0;let o=w(i.bounds,e,n,a,f);const v=P(s,t,m,d),b=v.map(r=>n==="right"?r.right-(e.x+e.width):n==="left"?e.x-r.left:n==="top"?e.y-r.top:r.bottom-(e.y+e.height));if(x)o=w(i.bounds,e,n,a,Math.max(f,...b.map(r=>r+f)));else if(v.some(r=>S(o,r)))throw new Error("Facet shared legend overlaps retained axes or headers.");const u=Math.max(0,t.padding.left-o.left),l=Math.max(0,c.height+t.padding.top-o.top),M=t.children.map(r=>({...r,x:r.x+u,y:r.y+l})),O={...e,x:e.x+u,y:e.y+l};return o=p(o,u,l),{plot:O,layout:{...t,children:M,width:Math.ceil(Math.max(t.width+u,o.right+t.padding.right)),height:Math.ceil(Math.max(t.height+l,o.bottom+t.padding.bottom)),legend:{position:n,x:o.left,y:o.top,width:o.right-o.left,height:o.bottom-o.top,translation:{x:o.left-i.bounds.left,y:o.top-i.bounds.top}}}}}export{j as resolvePromotedFacetLegendLayout};
@@ -1,3 +1,4 @@
1
+ import { resolveLegendGroupPlacement } from "../../layout/legendGroup.js";
1
2
  import { isPlainObject } from "../../core/immutable.js";
2
3
  import { resolveConcreteGraphicBounds } from
3
4
  "../../grammar/schemas/graphicBounds.js";
@@ -18,6 +19,9 @@ function copyLegendConfig(program, source) {
18
19
  for (const [kind, config] of Object.entries(source.guideConfigs.legend ?? {})) {
19
20
  next = next._withLegendConfig(kind, config);
20
21
  }
22
+ for (const [target, group] of Object.entries(source.guideConfigs.legendGroups ?? {})) {
23
+ next = next._withMaterializationConfig(["guides", "legendGroups", target], group);
24
+ }
21
25
  return next;
22
26
  }
23
27
 
@@ -33,6 +37,12 @@ function removeNamespacedGraphics(program, namespace, ids) {
33
37
  }
34
38
 
35
39
  function legendTranslation(prepared, plot, layout) {
40
+ if (layout.legend?.translation !== undefined) return layout.legend.translation;
41
+ if (prepared.occupiedGroup !== undefined) {
42
+ const [placement] = resolveLegendGroupPlacement({ blocks: [{ id: "shared", bounds: prepared.bounds }],
43
+ group: prepared.occupiedGroup, plot, canvas: layout });
44
+ return { x: placement.dx, y: placement.dy };
45
+ }
36
46
  const position = prepared.reservation.position;
37
47
  if (["top", "bottom"].includes(position)) {
38
48
  const width = prepared.bounds.right - prepared.bounds.left;
@@ -55,18 +65,16 @@ function legendTranslation(prepared, plot, layout) {
55
65
  const strips = prepared.source.graphicSpec.objects[`${prefix}Strips`];
56
66
  if (strips?.items?.[0] !== undefined) {
57
67
  const properties = strips.items[0].properties;
58
- const length = prepared.source.guideConfigs.legend[gradientKind].gradient.length;
59
68
  return {
60
69
  x: layout.legend.x - (position === "left"
61
70
  ? prepared.bounds.left
62
71
  : properties.x),
63
- y: plot.y + (plot.height - length) / 2 - properties.y
72
+ y: plot.y - prepared.bounds.top
64
73
  };
65
74
  }
66
- const height = prepared.bounds.bottom - prepared.bounds.top;
67
75
  return {
68
76
  x: layout.legend.x - prepared.bounds.left,
69
- y: plot.y + Math.max(0, (plot.height - height) / 2) - prepared.bounds.top
77
+ y: plot.y - prepared.bounds.top
70
78
  };
71
79
  }
72
80
 
@@ -91,7 +99,7 @@ function attachParentLegend(program, prepared, plot, layout) {
91
99
  return next;
92
100
  }
93
101
 
94
- function assertSharedLegendFits(program, id, layout, plot) {
102
+ function assertSharedLegendFits(program, id, layout, plot, occupiedGroup) {
95
103
  const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
96
104
  if (bounds === undefined) {
97
105
  throw new Error("Facet shared legend requires measurable concrete bounds.");
@@ -102,7 +110,7 @@ function assertSharedLegendFits(program, id, layout, plot) {
102
110
  Math.max(bounds.left, plot.x) &&
103
111
  Math.min(bounds.bottom, plot.y + plot.height) >
104
112
  Math.max(bounds.top, plot.y);
105
- if (outside || overlapsPlot) {
113
+ if (outside || overlapsPlot && occupiedGroup === undefined) {
106
114
  throw new Error(
107
115
  "Facet shared legend requires more space than the composed Canvas provides."
108
116
  );
@@ -153,10 +161,13 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
153
161
  let id;
154
162
  if (prepared.mode === "legacyCategorical") {
155
163
  id = `${program.compositionSpec.id}-legend`;
156
- next = materializeLegacyCategoricalLegend(next, prepared, layout);
164
+ next = materializeLegacyCategoricalLegend(next, prepared, {
165
+ ...layout,
166
+ legend: { ...layout.legend, y: plot.y }
167
+ });
157
168
  } else {
158
169
  id = `${program.compositionSpec.id}-shared-legend`;
159
170
  next = attachParentLegend(next, prepared, plot, layout);
160
171
  }
161
- return assertSharedLegendFits(next, id, layout, plot);
172
+ return assertSharedLegendFits(next, id, layout, plot, prepared.occupiedGroup);
162
173
  }
@@ -1,3 +1,5 @@
1
+ import { PLOT_LEGEND_POSITIONS } from "../../layout/legend.js";
2
+ import { isContinuousColorScaleType } from "../../grammar/scales/types.js";
1
3
  import {
2
4
  unionConcreteGraphicBounds
3
5
  } from "../../grammar/schemas/graphicBounds.js";
@@ -109,7 +111,7 @@ function prepareAutoLegendSource(child) {
109
111
  request.channels.includes("color") ? "color" : "stroke"
110
112
  ]?.scale
111
113
  );
112
- const gradient = scale?.type === "sequential";
114
+ const gradient = isContinuousColorScaleType(scale?.type);
113
115
  const gradientPrefix = request.channels.includes("color")
114
116
  ? "colorGradient"
115
117
  : "strokeGradient";
@@ -123,7 +125,7 @@ function prepareAutoLegendSource(child) {
123
125
  titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
124
126
  } : {})
125
127
  });
126
- if (gradient) {
128
+ if (gradient && scale.type === "sequential") {
127
129
  const resolved = source.resolvedScales[scale.id];
128
130
  source = source
129
131
  .editGraphics({
@@ -201,8 +203,13 @@ export function prepareSharedFacetLegend(program) {
201
203
  if (bounds === undefined) {
202
204
  throw new Error("Shared facet legend has no measurable concrete bounds.");
203
205
  }
204
- const gradient = kinds.some(kind => ["gradient", "strokeGradient"].includes(kind));
205
- const placement = promotedLegendPlacement(source, kinds);
206
+ const targets = new Set(kinds.map(kind => source.guideConfigs.legend[kind].target));
207
+ const group = targets.size === 1 ? source.guideConfigs.legendGroups?.[[...targets][0]] : undefined;
208
+ const insideGroup = group !== undefined && (group.placement.at !== undefined || PLOT_LEGEND_POSITIONS.includes(group.placement.position));
209
+ const placement = group === undefined ? promotedLegendPlacement(source, kinds)
210
+ : { position: insideGroup ? "right" : group.placement.position,
211
+ align: group.placement.align ?? "left" };
212
+
206
213
  const width = Math.ceil(bounds.right - bounds.left);
207
214
  return {
208
215
  mode: "promoted",
@@ -212,11 +219,12 @@ export function prepareSharedFacetLegend(program) {
212
219
  roots,
213
220
  bounds,
214
221
  compatibility,
222
+ ...(group === undefined ? {} : { occupiedGroup: group }),
215
223
  reservation: {
216
224
  ...placement,
217
- gap: gradient ? 24 : 18,
218
- width: Math.max(gradient ? 112 : 132, width),
219
- height: Math.ceil(bounds.bottom - bounds.top)
225
+ gap: insideGroup ? 0 : group?.placement.offset ?? Math.max(...kinds.map(kind => source.guideConfigs.legend[kind].offset)),
226
+ width: insideGroup ? 0 : width,
227
+ height: insideGroup ? 0 : Math.ceil(bounds.bottom - bounds.top)
220
228
  }
221
229
  };
222
230
  }
@@ -1,347 +1 @@
1
- import { isPlainObject } from "../core/immutable.js";
2
- import {
3
- formatVisibleText,
4
- measureTextWidth,
5
- resolveTextBounds,
6
- textBoundsIntersect
7
- } from "../core/textMetrics.js";
8
- import { resolveDisplayLabel } from "../grammar/displayLabels.js";
9
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
10
-
11
- const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
12
- const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
13
-
14
- export function createDefaultFacetHeaders() {
15
- return {
16
- mode: "legacy",
17
- common: {
18
- fontSize: 12,
19
- fontFamily: DEFAULT_FONT_FAMILY,
20
- fontWeight: 600,
21
- color: DEFAULT_COLORS.strongText,
22
- offset: 10,
23
- align: "center"
24
- },
25
- row: { side: "left" },
26
- column: { side: "top" }
27
- };
28
- }
29
-
30
- export function normalizeFacetHeadersConfig(value) {
31
- if (!isPlainObject(value)) {
32
- throw new TypeError("Facet headers config must be a plain object.");
33
- }
34
- if (value.common === undefined && value.mode === undefined) {
35
- return {
36
- ...createDefaultFacetHeaders(),
37
- common: {
38
- ...createDefaultFacetHeaders().common,
39
- ...value
40
- }
41
- };
42
- }
43
- if (!["legacy", "roles"].includes(value.mode) ||
44
- !isPlainObject(value.common) || !isPlainObject(value.row) ||
45
- !isPlainObject(value.column)) {
46
- throw new Error("Facet headers config must contain canonical mode and role objects.");
47
- }
48
- return {
49
- mode: value.mode,
50
- common: { ...value.common },
51
- row: { ...value.row },
52
- column: { ...value.column }
53
- };
54
- }
55
-
56
- export function resolveFacetHeaderConfig(headers, role) {
57
- const normalized = normalizeFacetHeadersConfig(headers);
58
- return role === "all"
59
- ? { ...normalized.common }
60
- : { ...normalized.common, ...normalized[role] };
61
- }
62
-
63
- function mappedText(value, config) {
64
- return resolveDisplayLabel(value, config.labelMap, formatVisibleText);
65
- }
66
-
67
- function gridCellById(spec) {
68
- return new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
69
- }
70
-
71
- function legacyDescriptors(program, headers) {
72
- const spec = program.compositionSpec;
73
- const config = resolveFacetHeaderConfig(headers, "all");
74
- const cells = gridCellById(spec);
75
- return spec.children.map((id, index) => {
76
- const cell = cells.get(id);
77
- const text = cell === undefined
78
- ? mappedText(spec.facet.values[index], config)
79
- : `${mappedText(cell.rowValue, config)} · ${mappedText(cell.columnValue, config)}`;
80
- return { topology: "legacy", role: "column", id, text, config };
81
- });
82
- }
83
-
84
- function uniqueGridGroups(spec, role) {
85
- const coordinate = role === "column" ? "column" : "row";
86
- const valueKey = role === "column" ? "columnValue" : "rowValue";
87
- const groups = new Map();
88
- for (const cell of spec.facet.grid.cells) {
89
- const existing = groups.get(cell[coordinate]);
90
- if (existing === undefined) {
91
- groups.set(cell[coordinate], {
92
- coordinate: cell[coordinate],
93
- value: cell[valueKey],
94
- ids: [cell.id]
95
- });
96
- } else {
97
- if (existing.value !== cell[valueKey]) {
98
- throw new Error(`Facet ${role} header values conflict at coordinate ${cell[coordinate]}.`);
99
- }
100
- existing.ids.push(cell.id);
101
- }
102
- }
103
- return [...groups.values()].sort((left, right) =>
104
- left.coordinate - right.coordinate
105
- );
106
- }
107
-
108
- function roleDescriptors(program, headers) {
109
- const spec = program.compositionSpec;
110
- const column = resolveFacetHeaderConfig(headers, "column");
111
- if (spec.facet.grid === undefined) {
112
- return spec.children.map((id, index) => ({
113
- topology: "cell-column",
114
- role: "column",
115
- id,
116
- ids: [id],
117
- row: Math.floor(index / spec.columns),
118
- text: mappedText(spec.facet.values[index], column),
119
- config: column
120
- }));
121
- }
122
- const row = resolveFacetHeaderConfig(headers, "row");
123
- const columns = uniqueGridGroups(spec, "column").map(group => ({
124
- topology: "grid-column",
125
- role: "column",
126
- ids: group.ids,
127
- coordinate: group.coordinate,
128
- text: mappedText(group.value, column),
129
- config: column
130
- }));
131
- const rows = uniqueGridGroups(spec, "row").map(group => ({
132
- topology: "grid-row",
133
- role: "row",
134
- ids: group.ids,
135
- coordinate: group.coordinate,
136
- text: mappedText(group.value, row),
137
- config: row
138
- }));
139
- return [...columns, ...rows];
140
- }
141
-
142
- function laneThickness(descriptors, role, profile) {
143
- const visible = descriptors.filter(item => item.role === role && item.text !== "");
144
- if (visible.length === 0) return 0;
145
- if (role === "column") {
146
- return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
147
- }
148
- return Math.max(...visible.map(item => measureTextWidth(item.text, item.config, profile) +
149
- item.config.offset));
150
- }
151
-
152
- export function prepareFacetHeaders(program, headersConfig) {
153
- const headers = normalizeFacetHeadersConfig(headersConfig);
154
- const descriptors = headers.mode === "legacy"
155
- ? legacyDescriptors(program, headers)
156
- : roleDescriptors(program, headers);
157
- const rows = program.compositionSpec.facet.grid === undefined
158
- ? Math.ceil(program.compositionSpec.children.length / program.compositionSpec.columns)
159
- : Math.max(...program.compositionSpec.facet.grid.cells.map(cell => cell.row)) + 1;
160
- const headerLayout = {
161
- outer: { ...ZERO_SIDES },
162
- cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
163
- };
164
- if (headers.mode === "roles") {
165
- const column = resolveFacetHeaderConfig(headers, "column");
166
- const columnThickness = laneThickness(descriptors, "column", program.materializationConfigs.textMetrics);
167
- if (program.compositionSpec.facet.grid === undefined) {
168
- for (const descriptor of descriptors) {
169
- if (descriptor.text !== "") {
170
- headerLayout.cellRows[column.side][descriptor.row] = Math.max(
171
- headerLayout.cellRows[column.side][descriptor.row],
172
- descriptor.config.fontSize + descriptor.config.offset
173
- );
174
- }
175
- }
176
- } else {
177
- headerLayout.outer[column.side] = columnThickness;
178
- const row = resolveFacetHeaderConfig(headers, "row");
179
- headerLayout.outer[row.side] = laneThickness(descriptors, "row", program.materializationConfigs.textMetrics);
180
- }
181
- }
182
- return { headers, descriptors, headerLayout };
183
- }
184
-
185
- function horizontalAnchor(bounds, align) {
186
- if (align === "start") return { x: bounds.left, textAlign: "left" };
187
- if (align === "end") return { x: bounds.right, textAlign: "right" };
188
- return { x: (bounds.left + bounds.right) / 2, textAlign: "center" };
189
- }
190
-
191
- function verticalAnchor(bounds, align) {
192
- if (align === "start") return { y: bounds.top, textBaseline: "top" };
193
- if (align === "end") return { y: bounds.bottom, textBaseline: "bottom" };
194
- return { y: (bounds.top + bounds.bottom) / 2, textBaseline: "middle" };
195
- }
196
-
197
- function union(items) {
198
- return {
199
- left: Math.min(...items.map(item => item.left)),
200
- right: Math.max(...items.map(item => item.right)),
201
- top: Math.min(...items.map(item => item.top)),
202
- bottom: Math.max(...items.map(item => item.bottom))
203
- };
204
- }
205
-
206
- function placedPlot(cell, plot) {
207
- return {
208
- left: cell.x + plot.x,
209
- right: cell.x + plot.x + plot.width,
210
- top: cell.y + plot.y,
211
- bottom: cell.y + plot.y + plot.height
212
- };
213
- }
214
-
215
- function placedCell(cell) {
216
- return {
217
- left: cell.x,
218
- right: cell.x + cell.width,
219
- top: cell.y,
220
- bottom: cell.y + cell.height
221
- };
222
- }
223
-
224
- function roleItem(descriptor, cells, plots) {
225
- const selectedCells = descriptor.ids.map(id => cells.get(id));
226
- const missingCell = descriptor.ids.find((id, index) =>
227
- selectedCells[index] === undefined
228
- );
229
- const missingPlot = descriptor.ids.find(id => plots.get(id) === undefined);
230
- if (missingCell !== undefined || missingPlot !== undefined) {
231
- throw new Error(
232
- `Facet header requires cell and plot bounds for "${missingCell ?? missingPlot}".`
233
- );
234
- }
235
- const selectedPlots = descriptor.ids.map((id, index) =>
236
- placedPlot(selectedCells[index], plots.get(id))
237
- );
238
- const plot = union(selectedPlots);
239
- const snapshots = union(selectedCells.map(placedCell));
240
- const config = descriptor.config;
241
- let anchor;
242
- if (descriptor.role === "column") {
243
- anchor = horizontalAnchor(plot, config.align);
244
- const top = config.side === "top";
245
- anchor.y = top
246
- ? snapshots.top - config.offset - config.fontSize / 2
247
- : snapshots.bottom + config.offset + config.fontSize / 2;
248
- anchor.textBaseline = "middle";
249
- } else {
250
- anchor = verticalAnchor(plot, config.align);
251
- const left = config.side === "left";
252
- anchor.x = left
253
- ? snapshots.left - config.offset
254
- : snapshots.right + config.offset;
255
- anchor.textAlign = left ? "right" : "left";
256
- }
257
- return { descriptor, snapshots, properties: anchor };
258
- }
259
-
260
- function legacyItem(descriptor, cell, plot) {
261
- if (cell === undefined || plot === undefined) {
262
- throw new Error(`Facet header requires cell and plot bounds for "${descriptor.id}".`);
263
- }
264
- return {
265
- descriptor,
266
- snapshots: placedCell(cell),
267
- properties: {
268
- ...horizontalAnchor(placedPlot(cell, plot), descriptor.config.align),
269
- y: cell.y + descriptor.config.offset,
270
- textBaseline: "middle"
271
- }
272
- };
273
- }
274
-
275
- function styledItem(item) {
276
- const { config } = item.descriptor;
277
- return {
278
- type: "text",
279
- properties: {
280
- ...item.properties,
281
- text: item.descriptor.text,
282
- fill: config.color,
283
- fontSize: config.fontSize,
284
- fontFamily: config.fontFamily,
285
- fontWeight: config.fontWeight
286
- }
287
- };
288
- }
289
-
290
- function assertRoleHeadersFit(records, items, layout, profile) {
291
- const bounds = items.map((item, index) => records[index].descriptor.text === ""
292
- ? undefined
293
- : resolveTextBounds(item.properties, profile));
294
- for (const [index, current] of bounds.entries()) {
295
- if (current === undefined) continue;
296
- if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
297
- current.top < -1e-9 || current.bottom > layout.height + 1e-9 ||
298
- bounds.some((other, otherIndex) => otherIndex < index &&
299
- other !== undefined && textBoundsIntersect(other, current)) ||
300
- layout.children.some(cell => textBoundsIntersect(current, placedCell(cell)))) {
301
- throw new Error(
302
- "Facet role headers require sufficient non-overlapping reserved space."
303
- );
304
- }
305
- }
306
- }
307
-
308
- function assertLegacyHeadersFit(items, layout, plotById, profile) {
309
- const previous = [];
310
- for (let index = 0; index < items.length; index += 1) {
311
- if (items[index].properties.text === "") continue;
312
- const bounds = resolveTextBounds(items[index].properties, profile);
313
- const cell = layout.children[index];
314
- const plot = plotById.get(cell.id);
315
- if (bounds.left < 0 || bounds.right > layout.width ||
316
- bounds.top < 0 || bounds.bottom > layout.height ||
317
- previous.some(item => textBoundsIntersect(item, bounds)) ||
318
- textBoundsIntersect(bounds, placedPlot(cell, plot))) {
319
- throw new Error(
320
- "Facet headers require sufficient non-overlapping space above every child plot."
321
- );
322
- }
323
- previous.push(bounds);
324
- }
325
- }
326
-
327
- export function materializeFacetHeaders(program, layout, plots, prepared) {
328
- const profile = program.materializationConfigs.textMetrics;
329
- const cells = new Map(layout.children.map(cell => [cell.id, cell]));
330
- const plotById = new Map(plots.map(plot => [plot.id, plot]));
331
- const records = prepared.descriptors.map(descriptor => {
332
- if (descriptor.topology === "legacy") {
333
- return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
334
- }
335
- return roleItem(descriptor, cells, plotById);
336
- });
337
- const items = records.map(styledItem);
338
- if (prepared.headers.mode === "legacy") {
339
- assertLegacyHeadersFit(items, layout, plotById, profile);
340
- } else {
341
- assertRoleHeadersFit(records, items, layout, profile);
342
- }
343
- const id = `${program.compositionSpec.id}-headers`;
344
- return program
345
- .createGraphics({ id, type: "collection", parent: "canvas" })
346
- .editGraphics({ target: id, property: "items", value: items });
347
- }
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};