ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,23 +1,15 @@
1
- import { isPlainObject } from "../../../../core/immutable.js";
2
1
  import { noOptions } from "../../../../core/validation.js";
3
2
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
4
- import { unionConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
5
3
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
4
  import { formatVisibleText } from "../../../../core/textMetrics.js";
7
5
  import {
8
6
  assertLegendBoundsInsideCanvas,
9
- formatContinuousValues,
10
7
  resolveContinuousBounds,
11
- resolveLegendBackgroundFromBounds,
12
- resolveLegendTextBounds,
13
- sampleContinuousValues
8
+ resolveLegendBackgroundFromBounds
14
9
  } from "../continuous/common.js";
15
- import {
16
- alignLegendStart,
17
- measureLegendSymbolHeight,
18
- measureLegendTextWidth,
19
- resolveLegendGrid
20
- } from "../../../../layout/legend.js";
10
+ import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
11
+ import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
12
+ import { resolveConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
21
13
 
22
14
  export function activeConfig(program) {
23
15
  const kinds = ["series", "color"]
@@ -54,347 +46,42 @@ export function symbolWidth(config) {
54
46
  }));
55
47
  }
56
48
 
57
- function leftGuideBoundary(program, bounds) {
58
- const ids = ["yAxisLine", "yAxisTicks", "yAxisLabels", "yAxisTitle"]
59
- .filter(id => program.graphicSpec.objects[id] !== undefined);
60
- const occupied = unionConcreteGraphicBounds(program.graphicSpec, ids);
61
- return occupied === undefined ? bounds.x : Math.min(bounds.x, occupied.left);
62
- }
63
-
64
- function textBounds(x, y, value, style, textAlign = "left") {
65
- return resolveLegendTextBounds(
66
- { x, y, align: textAlign },
67
- formatVisibleText(value),
68
- style
69
- );
70
- }
71
-
72
- function resolveLeftSizeMetrics(program, sizeConfig) {
73
- if (sizeConfig === undefined) return undefined;
74
- const scale = program.resolvedScales[sizeConfig.scale];
75
- if (scale?.type !== "linear") {
76
- throw new Error(`Legend requires resolved linear scale "${sizeConfig.scale}".`);
77
- }
78
- const values = sampleContinuousValues(scale.domain, sizeConfig.count);
79
- const labels = formatContinuousValues(values, scale.domain, "quantitative");
80
- const maximumRadius = Math.sqrt(Math.max(...scale.range) / Math.PI);
81
- return { values, labels, maximumRadius };
82
- }
83
-
84
- function resolveLeftLayout(program, bounds, canvas, config, width, count) {
85
- const sizeConfig = program.guideConfigs.legend?.size;
86
- const size = resolveLeftSizeMetrics(program, sizeConfig);
87
- const padding = config.border === false ? 0 : config.border.padding;
88
- const categoricalWidth = config.domain.reduce((maximum, value) => Math.max(
89
- maximum,
90
- width + config.labels.offset +
91
- measureLegendTextWidth(value, config.labels)
92
- ), config.titleVisible === false
93
- ? 0
94
- : measureLegendTextWidth(config.title, config.titleStyle));
95
- const sizeWidth = size === undefined
96
- ? 0
97
- : size.labels.reduce((maximum, label) => Math.max(
98
- maximum,
99
- 44 + measureLegendTextWidth(label, config.labels)
100
- ), measureLegendTextWidth(sizeConfig.title, config.titleStyle));
101
- const contentWidth = Math.max(categoricalWidth, sizeWidth);
102
- const occupiedRight = bounds.x - config.offset;
103
- const contentRight = occupiedRight - padding;
104
- const start = contentRight - contentWidth;
105
- if (start < padding) {
106
- throw new Error("Legend layout requires more left-margin space.");
107
- }
108
- if (occupiedRight >= leftGuideBoundary(program, bounds)) {
109
- throw new Error("Left legend and y-axis guides require more left-margin space.");
110
- }
111
- const symbolX = Array(count).fill(start);
112
- const itemY = Array.from(
113
- { length: count },
114
- (_, index) => bounds.y + 52 + index * config.itemGap
115
- );
116
- const labelX = symbolX.map(value => value + width + config.labels.offset);
117
- const titleX = start;
118
- const titleY = bounds.y + 20;
119
- const sizeTitleY = bounds.y + 56 + count * 34 + 22;
120
- const sizeItemY = size?.values.map((_, index) => sizeTitleY + 34 + index * 40);
121
- const sizeSymbolX = size?.values.map(() => start + 16);
122
- const sizeLabelX = size?.values.map(() => start + 44);
123
- const categoricalTop = config.titleVisible === false
124
- ? itemY[0] - Math.max(
125
- config.labels.fontSize,
126
- measureLegendSymbolHeight(config)
127
- ) / 2
128
- : titleY - config.titleStyle.fontSize / 2;
129
- const sizeBottom = size === undefined
130
- ? -Infinity
131
- : sizeItemY.at(-1) + Math.max(size.maximumRadius, config.labels.fontSize / 2);
132
- const categoricalBottom = itemY.at(-1) +
133
- Math.max(config.labels.fontSize, measureLegendSymbolHeight(config)) / 2;
134
- const blockTop = Math.min(categoricalTop, sizeTitleY - config.titleStyle.fontSize / 2);
135
- const blockBottom = Math.max(categoricalBottom, sizeBottom);
136
- if (blockTop < 0 || blockBottom > canvas.height) {
137
- throw new Error("Legend layout requires more vertical Canvas space.");
138
- }
139
- let background;
140
- if (config.border !== false) {
141
- const backgroundTop = Math.floor(
142
- blockTop - config.border.padding - config.border.lineWidth
143
- );
144
- const backgroundBottom = Math.ceil(blockBottom + config.border.padding);
145
- background = {
146
- x: start - config.border.padding,
147
- y: backgroundTop,
148
- width: occupiedRight - (start - config.border.padding),
149
- height: backgroundBottom - backgroundTop
150
- };
151
- if (
152
- background.x < 0 || background.y < 0 ||
153
- background.x + background.width > canvas.width ||
154
- background.y + background.height > canvas.height
155
- ) {
156
- throw new Error("Legend background requires more left-margin space.");
157
- }
158
- }
159
- return {
160
- symbolX,
161
- itemY,
162
- labelX,
163
- titleX,
164
- titleY,
165
- background,
166
- blockTop,
167
- blockBottom,
168
- size: size === undefined ? undefined : {
169
- titleX: start,
170
- titleY: sizeTitleY,
171
- itemY: sizeItemY,
172
- symbolX: sizeSymbolX,
173
- labelX: sizeLabelX
174
- }
175
- };
176
- }
177
-
178
- function resolveGridLayout(program, bounds, canvas, config, width, count, top) {
179
- const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
180
- resolveLegendGrid(config, width, count);
181
- const inlineTitle = config.titlePosition === "left";
182
- const titleWidth = config.titleVisible === false
183
- ? 0
184
- : measureLegendTextWidth(config.title, config.titleStyle);
185
- const titleGap = inlineTitle ? 20 : 12;
186
- const totalWidth = inlineTitle
187
- ? titleWidth + titleGap + gridWidth
188
- : Math.max(titleWidth, gridWidth);
189
- const start = alignLegendStart(bounds, totalWidth, config.align);
190
- if (start < 0 || start + totalWidth > canvas.width) {
191
- throw new Error("Legend layout requires more horizontal Canvas space.");
192
- }
193
- const edge = top
194
- ? bounds.y - config.offset
195
- : bounds.y + bounds.height + config.offset;
196
- const gridStart = inlineTitle
197
- ? start + titleWidth + titleGap
198
- : start + (totalWidth - gridWidth) / 2;
199
- const gridTop = top
200
- ? edge - gridHeight
201
- : inlineTitle ? edge : edge + config.titleStyle.fontSize + titleGap;
202
- const blockTop = top
203
- ? inlineTitle
204
- ? Math.min(
205
- gridTop,
206
- edge / 2 + gridTop / 2 - config.titleStyle.fontSize / 2
207
- )
208
- : gridTop - titleGap - config.titleStyle.fontSize
209
- : edge;
210
- const blockBottom = top ? edge : gridTop + gridHeight;
211
- if (top) {
212
- if (blockTop < 0) {
213
- throw new Error("Legend layout requires more top-margin space.");
214
- }
215
- const titleGraphic = program.graphicSpec.objects.chartSubtitle ??
216
- program.graphicSpec.objects.chartTitle;
217
- if (titleGraphic?.type === "text" &&
218
- titleGraphic.properties.y + titleGraphic.properties.fontSize / 2 >= blockTop) {
219
- throw new Error("Top legend and chart title require more top-margin space.");
220
- }
221
- } else {
222
- const occupiedTop = config.border === false
223
- ? blockTop
224
- : blockTop - config.border.padding;
225
- const axisTitle = program.graphicSpec.objects.xAxisTitle;
226
- if (axisTitle?.type === "text" &&
227
- axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop) {
228
- throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
229
- }
230
- const occupiedBottom = config.border === false
231
- ? blockBottom
232
- : blockBottom + config.border.padding;
233
- if (occupiedTop < 0 || occupiedBottom > canvas.height) {
234
- throw new Error("Legend layout requires more bottom-margin space.");
235
- }
236
- }
237
- const columnX = [];
238
- let cursor = gridStart;
239
- for (const columnWidth of columnWidths) {
240
- columnX.push(cursor);
241
- cursor += columnWidth + config.itemGap;
242
- }
243
- const symbolX = cells.map(cell => columnX[cell.column]);
244
- const labelX = symbolX.map(value => value + width + config.labels.offset);
245
- const itemY = cells.map(cell =>
246
- gridTop + rowHeight / 2 + cell.row * (rowHeight + config.itemGap)
247
- );
248
- const titleX = inlineTitle ? start : start + totalWidth / 2;
249
- const titleY = inlineTitle
250
- ? gridTop + gridHeight / 2
251
- : top
252
- ? gridTop - titleGap - config.titleStyle.fontSize / 2
253
- : blockTop + config.titleStyle.fontSize / 2;
254
- let background;
255
- if (config.border !== false) {
256
- background = {
257
- x: start - config.border.padding,
258
- y: blockTop - config.border.padding,
259
- width: totalWidth + config.border.padding * 2,
260
- height: blockBottom - blockTop + config.border.padding * 2
261
- };
262
- if (background.x < 0 || background.y < 0 ||
263
- background.x + background.width > canvas.width ||
264
- (!top && background.y + background.height > canvas.height)) {
265
- throw new Error(`Legend background requires more ${top ? "top" : "bottom"} or horizontal space.`);
266
- }
267
- }
268
- return {
269
- symbolX,
270
- itemY,
271
- labelX,
272
- titleX,
273
- titleY,
274
- background,
275
- blockTop,
276
- blockBottom
277
- };
278
- }
279
-
280
- function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
281
- const labels = config.domain.map(formatVisibleText);
282
- const itemWidths = labels.map(
283
- label => width + config.labels.offset +
284
- measureLegendTextWidth(label, config.labels)
285
- );
286
- const totalWidth = itemWidths.reduce((sum, value) => sum + value, 0) +
287
- config.itemGap * Math.max(0, count - 1);
288
- const start = config.align === "center"
289
- ? (canvas.width - totalWidth) / 2
290
- : alignLegendStart(bounds, totalWidth, config.align);
291
- if (start < 0 || start + totalWidth > canvas.width) {
292
- throw new Error("Legend layout requires more horizontal Canvas space.");
293
- }
294
- const symbolX = [];
295
- const labelX = [];
296
- let cursor = start;
297
- for (let index = 0; index < count; index += 1) {
298
- symbolX.push(cursor);
299
- labelX.push(cursor + width + config.labels.offset);
300
- cursor += itemWidths[index] + config.itemGap;
301
- }
302
- const itemY = Array(count).fill(canvas.height - 28);
303
- const titleX = start + totalWidth / 2;
304
- const titleY = canvas.height - 52;
305
- if (titleY <= bounds.y + bounds.height) {
306
- throw new Error("Legend layout requires more bottom-margin space.");
307
- }
308
- let background;
309
- if (config.border !== false) {
310
- const maxHeight = Math.max(
311
- config.labels.fontSize,
312
- measureLegendSymbolHeight(config)
313
- );
314
- const x = start - config.border.padding;
315
- const y = titleY - config.titleStyle.fontSize / 2 - config.border.padding;
316
- background = {
317
- x,
318
- y,
319
- width: totalWidth + config.border.padding * 2,
320
- height: itemY[0] + maxHeight / 2 + config.border.padding - y
321
- };
322
- if (
323
- background.x < 0 || background.y < 0 ||
324
- background.x + background.width > canvas.width ||
325
- background.y + background.height > canvas.height
326
- ) {
327
- throw new Error("Legend background requires more bottom or horizontal space.");
328
- }
329
- }
330
- return { symbolX, itemY, labelX, titleX, titleY, background };
49
+ function resolveSampleBounds(program, config, width) {
50
+ const appearance = resolveAppearance(program, config);
51
+ return config.domain.map((_, index) => {
52
+ const items = config.symbol.layers.map(layer => {
53
+ if (layer.type === "line") {
54
+ const x = (width - layer.length) / 2;
55
+ return { type: "line", properties: { x1: x, x2: x + layer.length, y1: 0, y2: 0,
56
+ strokeWidth: layer.lineWidth } };
57
+ }
58
+ if (layer.type === "swatch") {
59
+ return { type: "rect", properties: { x: (width - layer.width) / 2, y: -layer.height / 2,
60
+ width: layer.width, height: layer.height, strokeWidth: layer.strokeWidth } };
61
+ }
62
+ if (config.channels.includes("shape")) {
63
+ return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
64
+ area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
65
+ stroke: layer.stroke, strokeWidth: layer.strokeWidth });
66
+ }
67
+ return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
68
+ strokeWidth: layer.strokeWidth } };
69
+ });
70
+ return resolveConcreteGraphicBounds({ objects: { sample: { type: "collection", items } }, order: ["sample"] }, "sample");
71
+ });
331
72
  }
332
73
 
333
74
  export function resolveLayout(program, config) {
334
- const resolved = resolveContinuousBounds(
335
- program,
336
- "Legend layout requires Canvas bounds, width, and height."
337
- );
338
- const bounds = resolved.plot;
339
- const canvas = resolved.canvas;
340
-
341
- const count = config.domain.length;
75
+ const { plot, canvas } = resolveContinuousBounds(program,
76
+ "Legend layout requires Canvas bounds, width, and height.");
342
77
  const width = symbolWidth(config);
343
- if (config.position === "right") {
344
- const symbolX = Array(count).fill(
345
- bounds.x + bounds.width + config.offset
346
- );
347
- const itemY = Array.from(
348
- { length: count },
349
- (_, index) => bounds.y + 52 + index * config.itemGap
350
- );
351
- const labelX = symbolX.map(value => value + width + config.labels.offset);
352
- const titleX = symbolX[0];
353
- const titleY = bounds.y + 20;
354
- const labelBounds = config.domain.map((value, index) => textBounds(
355
- labelX[index], itemY[index], value, config.labels
356
- ));
357
- const titleBounds = config.titleVisible === false
358
- ? undefined
359
- : textBounds(titleX, titleY, config.title, config.titleStyle);
360
- const symbolHeight = measureLegendSymbolHeight(config);
361
- const symbolBounds = itemY.map(y => ({
362
- left: symbolX[0],
363
- right: symbolX[0] + width,
364
- top: y - symbolHeight / 2,
365
- bottom: y + symbolHeight / 2
366
- }));
367
- const contentBounds = [
368
- ...labelBounds,
369
- ...symbolBounds,
370
- ...(titleBounds === undefined ? [] : [titleBounds])
371
- ];
372
- assertLegendBoundsInsideCanvas(
373
- contentBounds,
374
- canvas,
375
- "Right legend layout"
376
- );
377
- const background = resolveLegendBackgroundFromBounds(
378
- contentBounds,
379
- config.border,
380
- canvas,
381
- "Right legend"
382
- );
383
- return { symbolX, itemY, labelX, titleX, titleY, background };
384
- }
385
-
386
- if (config.position === "left") {
387
- return resolveLeftLayout(program, bounds, canvas, config, width, count);
388
- }
389
-
390
- if (config.position === "top") {
391
- return resolveGridLayout(program, bounds, canvas, config, width, count, true);
392
- }
393
-
394
- if (config.bottomGrid !== true) {
395
- return resolveCompactBottomLayout(bounds, canvas, config, width, count);
396
- }
397
- return resolveGridLayout(program, bounds, canvas, config, width, count, false);
78
+ const layout = resolveLegendItemLayout(plot, config, config.domain.map(formatVisibleText), {
79
+ width, height: 0, itemBounds: resolveSampleBounds(program, config, width)
80
+ }, canvas);
81
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Categorical legend layout", config);
82
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas,
83
+ "Categorical legend", config);
84
+ return { ...layout, titleX: layout.title.x, titleY: layout.title.y, background };
398
85
  }
399
86
 
400
87
  export function resolveAppearance(program, config) {
@@ -16,7 +16,9 @@ export const CHANNELS = CATEGORICAL_LEGEND_CHANNELS;
16
16
  const OPTIONS = Object.freeze([
17
17
  "target",
18
18
  "channels",
19
+ "order",
19
20
  "position",
21
+ "layout",
20
22
  "align",
21
23
  "direction",
22
24
  "columns",
@@ -34,7 +36,8 @@ const TEXT_OPTIONS = Object.freeze([
34
36
  "color",
35
37
  "fontSize",
36
38
  "fontFamily",
37
- "fontWeight"
39
+ "fontWeight",
40
+ "format"
38
41
  ]);
39
42
  const TITLE_OPTIONS = Object.freeze([
40
43
  "color",
@@ -89,6 +92,9 @@ export function normalizeOptions(args, kind) {
89
92
  validateOptionObject(args, OPTIONS, "createLegend");
90
93
  if (Object.hasOwn(args, "labels")) {
91
94
  validateObject(args.labels, TEXT_OPTIONS, "createLegend.labels");
95
+ if (args.labels.format !== undefined && args.labels.format !== "auto") {
96
+ throw new Error("Categorical legend labels do not accept format.");
97
+ }
92
98
  }
93
99
  if (Object.hasOwn(args, "titleStyle")) {
94
100
  validateObject(
@@ -108,26 +114,33 @@ export function normalizeOptions(args, kind) {
108
114
  offset: defaults.offset,
109
115
  ...(args.labels ?? {})
110
116
  };
117
+ delete labels.format;
111
118
  const titleStyle = {
112
119
  ...COMMON_DEFAULTS.titleStyle,
113
120
  ...(args.titleStyle ?? {})
114
121
  };
115
122
  const position = args.position ?? defaults.position;
116
123
  const align = args.align ?? defaults.align;
117
- const direction = args.direction ?? (position === "left" ? "vertical" : "horizontal");
124
+ const side = ["left", "right"].includes(position);
125
+ const direction = args.direction ?? (side ? "vertical" : "horizontal");
118
126
  const columns = args.columns;
119
127
  const offset = args.offset ?? 8;
120
128
  const titlePosition = args.titlePosition ?? "top";
121
129
  const itemGap = args.itemGap ?? (["right", "left"].includes(position)
122
130
  ? 28
123
131
  : position === "top" ? 24 : 20);
124
- const bottomGrid = position !== "bottom" || [
125
- "columns",
126
- "direction",
127
- "offset",
128
- "titlePosition",
129
- "itemGap"
130
- ].some(key => Object.hasOwn(args, key));
132
+ const layout = args.layout === undefined ? "edge" : args.layout;
133
+ if (!["edge", "legacy-bottom"].includes(layout)) {
134
+ throw new Error(`Unsupported legend layout "${layout}".`);
135
+ }
136
+ if (layout === "legacy-bottom" && position !== "bottom") {
137
+ throw new Error('Legend layout "legacy-bottom" requires position "bottom".');
138
+ }
139
+ if (layout === "legacy-bottom" && (
140
+ columns !== undefined || direction !== "horizontal" || titlePosition !== "top" || offset !== 8
141
+ )) {
142
+ throw new Error('Legend columns, vertical direction, left title and custom offset require layout "edge".');
143
+ }
131
144
 
132
145
  if (!["right", "left", "bottom", "top"].includes(position)) {
133
146
  throw new Error(`Unsupported legend position "${position}".`);
@@ -141,11 +154,14 @@ export function normalizeOptions(args, kind) {
141
154
  if (!["horizontal", "vertical"].includes(direction)) {
142
155
  throw new Error(`Unsupported legend direction "${direction}".`);
143
156
  }
144
- if (position === "left" && direction !== "vertical") {
145
- throw new Error("Left legends currently require vertical direction.");
157
+ if (side && direction !== "vertical") {
158
+ throw new Error("Side legends require vertical direction.");
159
+ }
160
+ if (side && columns !== undefined && columns !== 1) {
161
+ throw new Error("Side legends require one column.");
146
162
  }
147
- if (position === "left" && columns !== undefined) {
148
- throw new Error("Left legends do not accept columns.");
163
+ if (side && titlePosition !== "top") {
164
+ throw new Error("Side legends require a top title.");
149
165
  }
150
166
  if (columns !== undefined && (!Number.isInteger(columns) || columns <= 0)) {
151
167
  throw new RangeError("Legend columns must be a positive integer.");
@@ -179,7 +195,7 @@ export function normalizeOptions(args, kind) {
179
195
  labels,
180
196
  titleStyle,
181
197
  itemGap,
182
- bottomGrid,
198
+ layout,
183
199
  border: normalizeBorder(args.border)
184
200
  };
185
201
  }
@@ -25,6 +25,20 @@ const LAYER_OPTIONS = Object.freeze({
25
25
  ])
26
26
  });
27
27
 
28
+ export function resolveLegendSymbol(program, layer, channels, requested) {
29
+ if (requested !== undefined && requested !== "auto") return requested;
30
+ if (layer.mark?.type !== "point" || !channels?.includes("shape")) return undefined;
31
+ const color = channels.includes("color") ? layer.encoding?.color : undefined;
32
+ const hasMatchingLine = color?.scale !== undefined && program.semanticSpec.layers.some(candidate =>
33
+ candidate.mark?.type === "line" && candidate.encoding?.color?.field === color.field &&
34
+ candidate.encoding?.color?.scale === color.scale
35
+ );
36
+ return { layers: [
37
+ ...(hasMatchingLine ? [{ type: "line", length: 32, lineWidth: 3 }] : []),
38
+ { type: "point", size: Math.sqrt(64 / Math.PI), stroke: "white", strokeWidth: 0 }
39
+ ] };
40
+ }
41
+
28
42
  function defaultRecipe(kind) {
29
43
  return kind === "series"
30
44
  ? { layers: [{ type: "line", length: 32, lineWidth: 2 }] }
@@ -1,3 +1,6 @@
1
+ import { requireDataset } from "../../../../selectors/datasets.js";
2
+ import { readNominalField } from "../../../../grammar/scales/fields.js";
3
+ import { resolveLegendOrderDomain } from "../../../../grammar/categoryOrder.js";
1
4
  import { validateUserId } from "../../../../core/identifiers.js";
2
5
  import {
3
6
  sameOrderedValues,
@@ -41,7 +44,7 @@ export function resolveTarget(program, requested) {
41
44
 
42
45
  export const sameValues = sameOrderedValues;
43
46
 
44
- export function resolveLegendKind(layer, requestedChannels) {
47
+ function resolveLegendKind(layer, requestedChannels) {
45
48
  if (["bar", "area", "arc", "rect"].includes(layer.mark.type)) return "color";
46
49
  if (
47
50
  layer.mark.type === "point" &&
@@ -74,13 +77,11 @@ function resolveOrdinalScales(program, scaleIds) {
74
77
  return { scales, domain: scales[0].domain };
75
78
  }
76
79
 
77
- export function resolveDefinition(program, layer, requestedChannels, requestedTitle) {
78
- const kind = resolveLegendKind(layer, requestedChannels);
79
- const channels = requestedChannels ?? (kind === "color"
80
- ? ["color"]
81
- : CHANNELS.filter(
82
- channel => layer.encoding?.[channel]?.scale !== undefined
83
- ));
80
+ export function resolveDefinition(program, layer, requestedChannels, requestedTitle, order) {
81
+ const channels = requestedChannels ?? (["line", "point"].includes(layer.mark.type)
82
+ ? CHANNELS.filter(channel => layer.encoding?.[channel]?.scale !== undefined)
83
+ : ["color"]);
84
+ const kind = resolveLegendKind(layer, channels);
84
85
  if (
85
86
  !Array.isArray(channels) ||
86
87
  channels.length === 0 ||
@@ -109,13 +110,22 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
109
110
  const scales = encodings.map(item => item.encoding.scale);
110
111
  const resolved = resolveOrdinalScales(program, scales);
111
112
  const field = [...fields][0];
113
+ let linked;
114
+ if (order?.channel !== undefined) {
115
+ const position = layer.encoding?.[order.channel];
116
+ if (!["nominal", "ordinal"].includes(position?.fieldType) || position.field !== field) {
117
+ throw new Error("Linked legend order requires the same categorical field on its target.");
118
+ }
119
+ linked = program.resolvedScales[position.scale]?.domain;
120
+ }
112
121
  return {
113
122
  kind,
114
123
  channels,
115
124
  scales,
116
125
  field,
117
126
  title: nonEmptyString(requestedTitle ?? field, "Legend title"),
118
- domain: resolved.domain
127
+ domain: resolveLegendOrderDomain(resolved.domain, order, linked,
128
+ order?.values === undefined ? undefined : readNominalField(requireDataset(program, layer.data).values, field))
119
129
  };
120
130
  }
121
131
 
@@ -133,6 +143,7 @@ export function resolveCurrentDefinition(program, config) {
133
143
  program,
134
144
  layer,
135
145
  channels,
136
- config.inferredTitle === true ? undefined : guide.title
146
+ config.inferredTitle === true ? undefined : guide.title,
147
+ guide.order
137
148
  );
138
149
  }
@@ -9,8 +9,7 @@ import {
9
9
  symbolGraphic,
10
10
  symbolWidth
11
11
  } from "./layout.js";
12
- import { resolveLegendGraphicPlacement } from
13
- "../../../../materialization/graphicHierarchy.js";
12
+ import { resolveCategoricalLegendPlacement } from "../lifecycle.js";
14
13
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
15
14
 
16
15
  function makeEditSymbol(type) {
@@ -129,7 +128,7 @@ function makeCreateSymbol(type, edit) {
129
128
  .createGraphics({
130
129
  id,
131
130
  type: graphicType,
132
- ...resolveLegendGraphicPlacement(this),
131
+ ...resolveCategoricalLegendPlacement(this),
133
132
  ...(this.graphicSpec.objects[graphic(config, "Labels")] === undefined
134
133
  ? {}
135
134
  : { before: graphic(config, "Labels") }),