ggaction 0.0.17 → 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 (263) hide show
  1. package/CHANGELOG.md +46 -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/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 +3 -3
  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/box.js +2 -1
  34. package/src/actions/data/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -1,441 +1 @@
1
- export const SIDE_LEGEND_BLOCK_GAP = 24;
2
- export const HORIZONTAL_LEGEND_BLOCK_GAP = 40;
3
- export const SIDE_LEGEND_SYMBOL_CENTER = 16;
4
- export const SIDE_LEGEND_LABEL_START = 44;
5
- export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
6
-
7
- export function isHorizontalEdgeLegend(config) {
8
- return config !== undefined &&
9
- ["top", "bottom"].includes(config.position) &&
10
- config.layout !== "legacy-bottom";
11
- }
12
-
13
- export function resolveSingleHorizontalLegendPlacement({ plot, canvas, config, bounds }) {
14
- if (!isHorizontalEdgeLegend(config)) {
15
- throw new Error("Single legend placement requires a horizontal edge layout.");
16
- }
17
- const dx = config.align === "left" ? plot.x - bounds.left
18
- : config.align === "right" ? plot.x + plot.width - bounds.right
19
- : plot.x + plot.width / 2 - midpoint(bounds.left, bounds.right);
20
- const dy = config.position === "top" ? plot.y - config.offset - bounds.bottom
21
- : plot.y + plot.height + config.offset - bounds.top;
22
- const occupied = translateBounds(bounds, dx, dy);
23
- if (occupied.left < 0 || occupied.right > canvas.width || occupied.top < 0 || occupied.bottom > canvas.height) {
24
- throw new Error(`Legend layout requires more ${config.position}-margin or Canvas space.`);
25
- }
26
- return { dx, dy, occupied };
27
- }
28
-
29
- function midpoint(start, end) {
30
- const sum = start + end;
31
- const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
32
- return result === 0 ? 0 : result;
33
- }
34
-
35
- function unionBounds(bounds) {
36
- return bounds.reduce((union, item) => ({
37
- left: Math.min(union.left, item.left),
38
- right: Math.max(union.right, item.right),
39
- top: Math.min(union.top, item.top),
40
- bottom: Math.max(union.bottom, item.bottom)
41
- }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
42
- }
43
-
44
- function decoration(border) {
45
- return border === false
46
- ? 0
47
- : border.padding + border.lineWidth / 2;
48
- }
49
-
50
- function horizontalExtent(blocks, labelStart) {
51
- let left = 0;
52
- let right = 0;
53
- for (const block of blocks) {
54
- const inset = block.inset ?? 0;
55
- if (block.title !== undefined) right = Math.max(right, block.title.width + inset);
56
- left = Math.min(left,
57
- SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX - inset
58
- );
59
- right = Math.max(
60
- right,
61
- SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX + inset,
62
- labelStart + block.labels.width + inset
63
- );
64
- }
65
- return { left, right };
66
- }
67
-
68
- function placeBlock(block, cursor, labelStart) {
69
- let dy;
70
- if (block.title === undefined) {
71
- dy = cursor - block.bounds.top;
72
- } else {
73
- const desiredTitleY = Math.ceil(cursor + block.title.fontSize / 2);
74
- dy = desiredTitleY - block.title.y;
75
- const futureTop = block.bounds.top + dy;
76
- if (futureTop < cursor) dy += cursor - futureTop;
77
- }
78
- const bounds = {
79
- left: block.bounds.left,
80
- right: block.bounds.right,
81
- top: block.bounds.top + dy,
82
- bottom: block.bounds.bottom + dy
83
- };
84
- return {
85
- id: block.id,
86
- dy,
87
- titleDx: block.title === undefined
88
- ? undefined
89
- : -block.title.x,
90
- symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
91
- labelDx: labelStart - block.labels.x,
92
- bounds
93
- };
94
- }
95
-
96
- function overlap(a, b) {
97
- return a.left < b.right && a.right > b.left &&
98
- a.top < b.bottom && a.bottom > b.top;
99
- }
100
-
101
- export function resolveSideLegendLane({
102
- side,
103
- plot,
104
- canvas,
105
- groups,
106
- axisBounds
107
- }) {
108
- if (!["right", "left"].includes(side)) {
109
- throw new Error(`Unsupported side legend lane "${side}".`);
110
- }
111
- const blocks = groups.flatMap(group => group.blocks);
112
- if (blocks.length < 2) return undefined;
113
- const labelStart = Math.max(SIDE_LEGEND_LABEL_START, ...blocks.map(
114
- block => SIDE_LEGEND_SYMBOL_CENTER + Math.abs(block.labels.x - block.symbol.centerX)
115
- ));
116
- const laneExtent = horizontalExtent(blocks, labelStart);
117
- const offset = blocks.reduce(
118
- (maximum, block) => Math.max(maximum, block.offset),
119
- -Infinity
120
- );
121
- const titleStartX = side === "right"
122
- ? plot.x + plot.width + offset
123
- : plot.x - offset - laneExtent.right;
124
- const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
125
- const labelStartX = titleStartX + labelStart;
126
- const first = blocks[0];
127
- let cursor = first.title === undefined
128
- ? plot.y + 12
129
- : plot.y + 20 - first.title.fontSize / 2;
130
- const placements = [];
131
- const backgrounds = [];
132
- const occupiedGroups = [];
133
-
134
- for (const group of groups) {
135
- const inset = decoration(group.border);
136
- if (occupiedGroups.length > 0) cursor += inset;
137
- const groupPlacements = [];
138
- for (const block of group.blocks) {
139
- const placement = placeBlock(block, cursor, labelStart);
140
- groupPlacements.push(placement);
141
- placements.push({
142
- ...placement,
143
- titleDx: placement.titleDx === undefined
144
- ? undefined
145
- : placement.titleDx + titleStartX,
146
- symbolDx: placement.symbolDx + titleStartX,
147
- labelDx: placement.labelDx + titleStartX
148
- });
149
- cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
150
- }
151
- const vertical = unionBounds(groupPlacements.map(item => item.bounds));
152
- const groupExtent = horizontalExtent(group.blocks, labelStart);
153
- const foreground = {
154
- left: titleStartX + groupExtent.left,
155
- right: titleStartX + groupExtent.right,
156
- top: vertical.top,
157
- bottom: vertical.bottom
158
- };
159
- const occupied = {
160
- left: foreground.left - inset,
161
- right: foreground.right + inset,
162
- top: foreground.top - inset,
163
- bottom: foreground.bottom + inset
164
- };
165
- occupiedGroups.push(occupied);
166
- if (group.backgroundId !== undefined) {
167
- backgrounds.push({
168
- id: group.backgroundId,
169
- x: foreground.left - group.border.padding,
170
- y: foreground.top - group.border.padding,
171
- width: foreground.right - foreground.left + group.border.padding * 2,
172
- height: foreground.bottom - foreground.top + group.border.padding * 2
173
- });
174
- }
175
- cursor = occupied.bottom + SIDE_LEGEND_BLOCK_GAP;
176
- }
177
-
178
- const occupied = unionBounds(occupiedGroups);
179
- if (
180
- occupied.left < 0 || occupied.right > canvas.width ||
181
- occupied.top < 0 || occupied.bottom > canvas.height
182
- ) {
183
- throw new Error(`Legend lane requires more ${side}-margin or vertical Canvas space.`);
184
- }
185
- if (axisBounds !== undefined && overlap(occupied, axisBounds)) {
186
- throw new Error(`${side[0].toUpperCase()}${side.slice(1)} legend lane and y-axis guides require more margin space.`);
187
- }
188
- return {
189
- side,
190
- titleStartX,
191
- symbolCenterX,
192
- labelStartX,
193
- placements,
194
- backgrounds,
195
- occupied
196
- };
197
- }
198
-
199
- function translateBounds(bounds, dx, dy) {
200
- return {
201
- left: bounds.left + dx,
202
- right: bounds.right + dx,
203
- top: bounds.top + dy,
204
- bottom: bounds.bottom + dy
205
- };
206
- }
207
-
208
- function expandBounds(bounds, inset) {
209
- return {
210
- left: bounds.left - inset,
211
- right: bounds.right + inset,
212
- top: bounds.top - inset,
213
- bottom: bounds.bottom + inset
214
- };
215
- }
216
-
217
- function packHorizontalRows(groups, plot) {
218
- const rows = [];
219
- let row = [];
220
- let cursor = plot.x;
221
- for (const group of groups) {
222
- const interval = expandBounds(group.horizontal, group.inset);
223
- const width = interval.right - interval.left;
224
- if (width > plot.width) {
225
- throw new Error("Horizontal legend block requires more plot width.");
226
- }
227
- let dx = cursor - interval.left;
228
- let placed = translateBounds(interval, dx, 0);
229
- if (row.length > 0 && placed.right > plot.x + plot.width) {
230
- rows.push(row);
231
- row = [];
232
- cursor = plot.x;
233
- dx = cursor - interval.left;
234
- placed = translateBounds(interval, dx, 0);
235
- }
236
- row.push({ group, interval: placed, dx });
237
- cursor = placed.right + HORIZONTAL_LEGEND_BLOCK_GAP;
238
- }
239
- if (row.length > 0) rows.push(row);
240
- return rows;
241
- }
242
-
243
- function normalizeHorizontalRow(entries) {
244
- const stacked = entries.filter(entry =>
245
- entry.group.title !== undefined && entry.group.inline !== true
246
- );
247
- const commonTitleY = stacked[0]?.group.title.y;
248
- const titleDescent = stacked.length === 0
249
- ? 0
250
- : stacked.reduce((maximum, entry) => Math.max(
251
- maximum,
252
- entry.group.title.bounds.bottom - entry.group.title.y
253
- ), -Infinity);
254
- const elementAnchor = commonTitleY === undefined
255
- ? midpoint(
256
- entries[0].group.element.top,
257
- entries[0].group.element.bottom
258
- )
259
- : commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
260
- return entries.map(({ group, dx }) => {
261
- if (group.atomic) {
262
- const contentDy = elementAnchor - group.element.top;
263
- const foreground = translateBounds(group.content, dx, contentDy);
264
- return { id: group.id, dx, titleDy: contentDy, contentDy, foreground,
265
- occupied: expandBounds(foreground, group.inset), padding: group.padding,
266
- backgroundId: group.backgroundId };
267
- }
268
- const contentDy = commonTitleY === undefined
269
- ? elementAnchor - midpoint(
270
- group.element.top,
271
- group.element.bottom
272
- )
273
- : elementAnchor - group.element.top;
274
- const titleDy = group.title === undefined
275
- ? 0
276
- : group.inline === true
277
- ? contentDy
278
- : commonTitleY - group.title.y;
279
- const foreground = unionBounds([
280
- ...(group.title === undefined
281
- ? []
282
- : [translateBounds(group.title.bounds, dx, titleDy)]),
283
- translateBounds(group.content, dx, contentDy)
284
- ]);
285
- return {
286
- id: group.id,
287
- dx,
288
- titleDy,
289
- contentDy,
290
- foreground,
291
- occupied: expandBounds(foreground, group.inset),
292
- padding: group.padding,
293
- backgroundId: group.backgroundId
294
- };
295
- });
296
- }
297
-
298
- function translateHorizontalPlacement(placement, dy) {
299
- return {
300
- ...placement,
301
- titleDy: placement.titleDy + dy,
302
- contentDy: placement.contentDy + dy,
303
- foreground: translateBounds(placement.foreground, 0, dy),
304
- occupied: translateBounds(placement.occupied, 0, dy)
305
- };
306
- }
307
-
308
- export function resolveHorizontalLegendLane({
309
- edge,
310
- plot,
311
- canvas,
312
- groups,
313
- collisionBounds = []
314
- }) {
315
- if (!["top", "bottom"].includes(edge)) {
316
- throw new Error(`Unsupported horizontal legend lane "${edge}".`);
317
- }
318
- if (groups.length < 2) return undefined;
319
- const packed = packHorizontalRows(groups, plot);
320
- const normalized = packed.map(normalizeHorizontalRow);
321
- const placements = [];
322
- let previousRowBounds;
323
- for (let index = 0; index < normalized.length; index += 1) {
324
- const row = normalized[index];
325
- const rowBounds = unionBounds(row.map(item => item.occupied));
326
- let dy;
327
- if (index === 0) {
328
- const anchor = edge === "top"
329
- ? packed[index].reduce((maximum, item) => Math.max(
330
- maximum,
331
- item.group.horizontal.bottom
332
- ), -Infinity)
333
- : packed[index].reduce((minimum, item) => Math.min(
334
- minimum,
335
- item.group.horizontal.top
336
- ), Infinity);
337
- dy = edge === "top"
338
- ? anchor - rowBounds.bottom
339
- : anchor - rowBounds.top;
340
- } else {
341
- dy = edge === "bottom"
342
- ? previousRowBounds.bottom + HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.top
343
- : previousRowBounds.top - HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.bottom;
344
- }
345
- const translated = row.map(item => translateHorizontalPlacement(item, dy));
346
- placements.push(...translated);
347
- previousRowBounds = unionBounds(translated.map(item => item.occupied));
348
- }
349
- const occupied = unionBounds(placements.map(item => item.occupied));
350
- if (placements.some(placement =>
351
- placement.occupied.left < 0 || placement.occupied.right > canvas.width ||
352
- placement.occupied.top < 0 || placement.occupied.bottom > canvas.height
353
- )) {
354
- throw new Error(`Legend lane requires more ${edge}-margin or Canvas space.`);
355
- }
356
- if (placements.some(placement =>
357
- collisionBounds.some(bounds => overlap(placement.occupied, bounds))
358
- )) {
359
- const owner = edge === "top" ? "chart titles" : "x-axis guides";
360
- throw new Error(
361
- `${edge[0].toUpperCase()}${edge.slice(1)} legend lane and ${owner} require more margin space.`
362
- );
363
- }
364
- return {
365
- edge,
366
- placements: placements.map(placement => ({
367
- ...placement,
368
- ...(placement.backgroundId === undefined
369
- ? {}
370
- : {
371
- background: {
372
- id: placement.backgroundId,
373
- x: placement.foreground.left - placement.padding,
374
- y: placement.foreground.top - placement.padding,
375
- width: placement.foreground.right - placement.foreground.left +
376
- placement.padding * 2,
377
- height: placement.foreground.bottom - placement.foreground.top +
378
- placement.padding * 2
379
- }
380
- })
381
- })),
382
- occupied,
383
- rowCount: packed.length
384
- };
385
- }
386
-
387
- // Compose independently measured content blocks before the outer edge lane
388
- // treats the whole group as one indivisible item.
389
- export function resolveHorizontalLegendGroup({ edge, plot, canvas, groups,
390
- align, offset, border, backgroundId, collisionBounds = [] }) {
391
- const inset = decoration(border);
392
- const innerPlot = { ...plot, x: plot.x + inset, width: plot.width - inset * 2 };
393
- const packed = packHorizontalRows(groups, innerPlot);
394
- const rows = packed.map(normalizeHorizontalRow);
395
- let cursor = 0;
396
- const placements = [];
397
- for (const row of rows) {
398
- const bounds = unionBounds(row.map(item => item.occupied));
399
- const dy = edge === "top" ? cursor - bounds.bottom : cursor - bounds.top;
400
- const translated = row.map(item => translateHorizontalPlacement(item, dy));
401
- placements.push(...translated);
402
- const occupied = unionBounds(translated.map(item => item.occupied));
403
- cursor = edge === "top" ? occupied.top - HORIZONTAL_LEGEND_BLOCK_GAP
404
- : occupied.bottom + HORIZONTAL_LEGEND_BLOCK_GAP;
405
- }
406
- const foreground = unionBounds(placements.map(item => item.occupied));
407
- const occupied = expandBounds(foreground, inset);
408
- const width = occupied.right - occupied.left;
409
- const x = align === "left" ? plot.x : align === "right" ? plot.x + plot.width - width
410
- : plot.x + (plot.width - width) / 2;
411
- const dx = x - occupied.left;
412
- const dy = edge === "top" ? plot.y - offset - occupied.bottom
413
- : plot.y + plot.height + offset - occupied.top;
414
- const finalBounds = translateBounds(occupied, dx, dy);
415
- if (finalBounds.left < 0 || finalBounds.right > canvas.width ||
416
- finalBounds.top < 0 || finalBounds.bottom > canvas.height) {
417
- throw new Error(`Combined legend requires more ${edge}-margin or Canvas space.`);
418
- }
419
- if (collisionBounds.some(bounds => overlap(finalBounds, bounds))) {
420
- throw new Error(`Combined ${edge} legend and ${edge === "top" ? "chart titles" : "x-axis guides"} require more margin space.`);
421
- }
422
- return {
423
- placements: placements.map(item => ({ ...item, dx: item.dx + dx,
424
- titleDy: item.titleDy + dy, contentDy: item.contentDy + dy,
425
- ...(item.backgroundId === undefined ? {} : { background: {
426
- id: item.backgroundId,
427
- x: item.foreground.left + dx - item.padding,
428
- y: item.foreground.top + dy - item.padding,
429
- width: item.foreground.right - item.foreground.left + item.padding * 2,
430
- height: item.foreground.bottom - item.foreground.top + item.padding * 2
431
- } }) })),
432
- occupied: finalBounds,
433
- background: backgroundId === undefined ? undefined : {
434
- id: backgroundId,
435
- x: foreground.left + dx - border.padding,
436
- y: foreground.top + dy - border.padding,
437
- width: foreground.right - foreground.left + border.padding * 2,
438
- height: foreground.bottom - foreground.top + border.padding * 2
439
- }
440
- };
441
- }
1
+ import{canvasOverflowError as S}from"./canvas.js";import{unionBounds as _}from"../core/textMetrics.js";const N=24,$=40,v=16,X=44,U=12;function K(t){return t!==void 0&&["top","bottom"].includes(t.position)&&t.layout!=="legacy-bottom"}function W({plot:t,canvas:o,config:n,bounds:r}){if(!K(n))throw new Error("Single legend placement requires a horizontal edge layout.");const i=n.align==="left"?t.x-r.left:n.align==="right"?t.x+t.width-r.right:t.x+t.width/2-B(r.left,r.right),e=n.position==="top"?t.y-n.offset-r.bottom:t.y+t.height+n.offset-r.top,s=w(r,i,e);if(s.left<0||s.right>o.width||s.top<0||s.bottom>o.height)throw S(`Legend layout requires more ${n.position}-margin or Canvas space.`,[s],o);return{dx:i,dy:e,occupied:s}}function B(t,o){const n=t+o,r=Number.isFinite(n)?n/2:t/2+o/2;return r===0?0:r}function T(t){return t===!1?0:t.padding+t.lineWidth/2}function A(t,o){let n=0,r=0;for(const i of t){const e=i.inset??0;if(i.preserveGrid){n=Math.min(n,-e),r=Math.max(r,i.bounds.right-i.bounds.left+e);continue}i.title!==void 0&&(r=Math.max(r,i.title.width+e)),n=Math.min(n,v+i.symbol.left-i.symbol.centerX-e),r=Math.max(r,v+i.symbol.right-i.symbol.centerX+e,o+i.labels.width+e)}return{left:n,right:r}}function Y(t,o,n){const r=o-t.bounds.top,i={left:t.bounds.left,right:t.bounds.right,top:t.bounds.top+r,bottom:t.bounds.bottom+r};return{id:t.id,dy:r,titleDx:t.title===void 0?void 0:t.preserveGrid?-t.bounds.left:-t.title.x,symbolDx:t.preserveGrid?-t.bounds.left:v-t.symbol.centerX,labelDx:t.preserveGrid?-t.bounds.left:n-t.labels.x,bounds:i}}function H(t,o){return t.left<o.right&&t.right>o.left&&t.top<o.bottom&&t.bottom>o.top}function j({side:t,plot:o,canvas:n,groups:r,axisBounds:i}){if(!["right","left"].includes(t))throw new Error(`Unsupported side legend lane "${t}".`);const e=r.flatMap(c=>c.blocks);if(e.length<2)return;const s=Math.max(X,...e.filter(c=>!c.preserveGrid).map(c=>v+Math.abs(c.labels.x-c.symbol.centerX))),f=A(e,s),g=e.reduce((c,x)=>Math.max(c,x.offset),-1/0),l=t==="right"?o.x+o.width+g:o.x-g-f.right,h=l+v,d=l+s;let y=o.y;const z=[],E=[],D=[];for(const c of r){const x=T(c.border);D.length>0&&(y+=x);const G=[];for(const C of c.blocks){const I=Y(C,y,s);G.push(I),z.push({...I,titleDx:I.titleDx===void 0?void 0:I.titleDx+l,symbolDx:I.symbolDx+l,labelDx:I.labelDx+l}),y=I.bounds.bottom+N}const L=_(G.map(C=>C.bounds)),u=A(c.blocks,s),b={left:l+u.left,right:l+u.right,top:L.top,bottom:L.bottom},k={left:b.left-x,right:b.right+x,top:b.top-x,bottom:b.bottom+x};D.push(k),c.backgroundId!==void 0&&E.push({id:c.backgroundId,x:b.left-c.border.padding,y:b.top-c.border.padding,width:b.right-b.left+c.border.padding*2,height:b.bottom-b.top+c.border.padding*2}),y=k.bottom+N}const a=_(D),p=o.y-a.top,m=w(a,0,p);if(m.left<0||m.right>n.width||m.top<0||m.bottom>n.height)throw S(`Legend lane requires more ${t}-margin or vertical Canvas space.`,[m],n);if(i!==void 0&&H(m,i))throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and y-axis guides require more margin space.`);return{side:t,titleStartX:l,symbolCenterX:h,labelStartX:d,placements:z.map(c=>({...c,dy:c.dy+p,bounds:w(c.bounds,0,p)})),backgrounds:E.map(c=>({...c,y:c.y+p})),occupied:m}}function J({side:t,plot:o,canvas:n,bounds:r}){if(!["left","right"].includes(t))throw new Error("Single side legend placement requires a left or right edge.");const i=o.y-r.top,e=w(r,0,i);if(e.left<0||e.right>n.width||e.top<0||e.bottom>n.height)throw S(`Legend layout requires more ${t}-margin or Canvas space.`,[e],n);return{dy:i,occupied:e}}function w(t,o,n){return{left:t.left+o,right:t.right+o,top:t.top+n,bottom:t.bottom+n}}function q(t,o){return{left:t.left-o,right:t.right+o,top:t.top-o,bottom:t.bottom+o}}function O(t,o,n=!1){const r=[];let i=[],e=o.x;for(const s of t){const f=q(s.horizontal,s.inset);if(f.right-f.left>o.width&&!n)throw new Error("Horizontal legend block requires more plot width.");let l=e-f.left,h=w(f,l,0);i.length>0&&h.right>o.x+o.width&&(r.push(i),i=[],e=o.x,l=e-f.left,h=w(f,l,0)),i.push({group:s,interval:h,dx:l}),e=h.right+$}return i.length>0&&r.push(i),r}function P(t){const o=t.filter(e=>e.group.title!==void 0&&e.group.inline!==!0),n=o[0]?.group.title.y,r=o.length===0?0:o.reduce((e,s)=>Math.max(e,s.group.title.bounds.bottom-s.group.title.y),-1/0),i=n===void 0?B(t[0].group.element.top,t[0].group.element.bottom):n+r+U;return t.map(({group:e,dx:s})=>{if(e.atomic){const h=i-e.element.top,d=w(e.content,s,h);return{id:e.id,dx:s,titleDy:h,contentDy:h,foreground:d,occupied:q(d,e.inset),padding:e.padding,backgroundId:e.backgroundId}}const f=n===void 0?i-B(e.element.top,e.element.bottom):i-e.element.top,g=e.title===void 0?0:e.inline===!0?f:n-e.title.y,l=_([...e.title===void 0?[]:[w(e.title.bounds,s,g)],w(e.content,s,f)]);return{id:e.id,dx:s,titleDy:g,contentDy:f,foreground:l,occupied:q(l,e.inset),padding:e.padding,backgroundId:e.backgroundId}})}function R(t,o){return{...t,titleDy:t.titleDy+o,contentDy:t.contentDy+o,foreground:w(t.foreground,0,o),occupied:w(t.occupied,0,o)}}function Q({edge:t,plot:o,canvas:n,groups:r,collisionBounds:i=[],allowOverflow:e=!1}){if(!["top","bottom"].includes(t))throw new Error(`Unsupported horizontal legend lane "${t}".`);if(r.length<2)return;const s=O(r,o,e),f=s.map(P),g=[];let l;for(let d=0;d<f.length;d+=1){const y=f[d],z=_(y.map(a=>a.occupied));let E;if(d===0){const a=t==="top"?s[d].reduce((p,m)=>Math.max(p,m.group.horizontal.bottom),-1/0):s[d].reduce((p,m)=>Math.min(p,m.group.horizontal.top),1/0);E=t==="top"?a-z.bottom:a-z.top}else E=t==="bottom"?l.bottom+$-z.top:l.top-$-z.bottom;const D=y.map(a=>R(a,E));g.push(...D),l=_(D.map(a=>a.occupied))}const h=_(g.map(d=>d.occupied));if(g.some(d=>d.occupied.left<0||d.occupied.right>n.width||d.occupied.top<0||d.occupied.bottom>n.height))throw S(`Legend lane requires more ${t}-margin or Canvas space.`,[h],n);if(g.some(d=>i.some(y=>H(d.occupied,y)))){const d=t==="top"?"chart titles":"x-axis guides";throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and ${d} require more margin space.`)}return{edge:t,placements:g.map(d=>({...d,...d.backgroundId===void 0?{}:{background:{id:d.backgroundId,x:d.foreground.left-d.padding,y:d.foreground.top-d.padding,width:d.foreground.right-d.foreground.left+d.padding*2,height:d.foreground.bottom-d.foreground.top+d.padding*2}}})),occupied:h,rowCount:s.length}}function V({edge:t,plot:o,canvas:n,groups:r,align:i,offset:e,border:s,backgroundId:f,collisionBounds:g=[],allowOverflow:l=!1}){const h=T(s),d={...o,x:o.x+h,width:o.width-h*2},z=O(r,d,l).map(P);let E=0;const D=[];for(const u of z){const b=_(u.map(M=>M.occupied)),k=t==="top"?E-b.bottom:E-b.top,C=u.map(M=>R(M,k));D.push(...C);const I=_(C.map(M=>M.occupied));E=t==="top"?I.top-$:I.bottom+$}const a=_(D.map(u=>u.occupied)),p=q(a,h),m=p.right-p.left,x=(i==="left"?o.x:i==="right"?o.x+o.width-m:o.x+(o.width-m)/2)-p.left,G=t==="top"?o.y-e-p.bottom:o.y+o.height+e-p.top,L=w(p,x,G);if(L.left<0||L.right>n.width||L.top<0||L.bottom>n.height)throw S(`Combined legend requires more ${t}-margin or Canvas space.`,[L],n);if(g.some(u=>H(L,u)))throw new Error(`Combined ${t} legend and ${t==="top"?"chart titles":"x-axis guides"} require more margin space.`);return{placements:D.map(u=>({...u,dx:u.dx+x,titleDy:u.titleDy+G,contentDy:u.contentDy+G,...u.backgroundId===void 0?{}:{background:{id:u.backgroundId,x:u.foreground.left+x-u.padding,y:u.foreground.top+G-u.padding,width:u.foreground.right-u.foreground.left+u.padding*2,height:u.foreground.bottom-u.foreground.top+u.padding*2}}})),occupied:L,background:f===void 0?void 0:{id:f,x:a.left+x-s.padding,y:a.top+G-s.padding,width:a.right-a.left+s.padding*2,height:a.bottom-a.top+s.padding*2}}}export{$ as HORIZONTAL_LEGEND_BLOCK_GAP,U as HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP,N as SIDE_LEGEND_BLOCK_GAP,X as SIDE_LEGEND_LABEL_START,v as SIDE_LEGEND_SYMBOL_CENTER,K as isHorizontalEdgeLegend,V as resolveHorizontalLegendGroup,Q as resolveHorizontalLegendLane,j as resolveSideLegendLane,W as resolveSingleHorizontalLegendPlacement,J as resolveSingleSideLegendPlacement};
@@ -0,0 +1 @@
1
+ import{resolveTextBounds as f,resolveTextLines as m}from"../core/textMetrics.js";function s(e,t,n){const l=m(String(e),{fontSize:t.fontSize,align:"left",baseline:"middle",blockAlign:"middle"}),o=l.length>1?l:void 0,i=f({x:0,y:0,text:String(e),...t,lines:o,textAlign:"left",textBaseline:"middle"},n);return{lines:o,width:i.right-i.left,height:i.bottom-i.top,extraHeight:i.bottom-i.top-t.fontSize}}function h(e,t,n,l,o){if(n.align===void 0)return e;const i=s(t,n,o),r=f({x:e.x,y:e.y,text:t,...n,lines:i.lines,textAlign:e.align,textBaseline:"middle"},o),a=Math.min(r.left,...l.map(g=>g.left)),d=Math.max(r.right,...l.map(g=>g.right));return{...e,x:n.align==="left"?a:n.align==="right"?d:(a+d)/2,align:n.align}}export{h as alignLegendTitle,s as resolveLegendTitleText};
@@ -1,16 +1,21 @@
1
+ import { unionBounds } from "../core/textMetrics.js";
1
2
  import { measureTextWidth, resolveTextBounds, wrapText } from "./text.js";
2
3
 
3
4
  function lineCenters(lines, style, lineHeight, start) {
4
5
  return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
5
6
  }
6
7
 
8
+ function wrapTitleLines(text, options) {
9
+ return text.split(/\r\n|\r|\n/u).flatMap(line => line === "" ? [""] : wrapText(line, options));
10
+ }
11
+
7
12
  export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
8
- const titleLines = wrapText(text, {
13
+ const titleLines = wrapTitleLines(text, {
9
14
  maxWidth: config.maxWidth,
10
15
  mode: config.wrap,
11
16
  style: config.titleStyle, profile
12
17
  });
13
- const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
18
+ const subtitleLines = subtitle === undefined ? [] : wrapTitleLines(subtitle, {
14
19
  maxWidth: config.maxWidth,
15
20
  mode: config.wrap,
16
21
  style: config.subtitleStyle, profile
@@ -73,14 +78,6 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile)
73
78
  }, profile);
74
79
  }
75
80
 
76
- function unionBounds(bounds) {
77
- return bounds.reduce((union, item) => ({
78
- left: Math.min(union.left, item.left),
79
- right: Math.max(union.right, item.right),
80
- top: Math.min(union.top, item.top),
81
- bottom: Math.max(union.bottom, item.bottom)
82
- }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
83
- }
84
81
 
85
82
  export function resolveTitleComponentBounds(component, style, profile) {
86
83
  return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
@@ -97,7 +94,32 @@ export function unionTitleBounds(bounds) {
97
94
  return unionBounds(bounds);
98
95
  }
99
96
 
100
- export function layoutBoundsIntersect(first, second) {
101
- return first.left < second.right && first.right > second.left &&
102
- first.top < second.bottom && first.bottom > second.top;
97
+ export function paintedTitleComponent(component) {
98
+ const indices = component.lines.flatMap((line, index) => line === "" ? [] : [index]);
99
+ const selected = value => Array.isArray(value) ? indices.map(index => value[index]) : value;
100
+ return {
101
+ ...component,
102
+ lines: indices.map(index => component.lines[index]),
103
+ x: selected(component.x),
104
+ y: selected(component.y)
105
+ };
106
+ }
107
+
108
+ export { textBoundsIntersect as layoutBoundsIntersect } from "../core/textMetrics.js";
109
+
110
+
111
+ export function titleTextProperties(component, style) {
112
+ const distributed = value => component.lines.length === 1 && Array.isArray(value) ? value[0] : value;
113
+ return {
114
+ x: distributed(component.x),
115
+ y: distributed(component.y),
116
+ text: distributed(component.lines),
117
+ fill: style.color,
118
+ fontSize: style.fontSize,
119
+ fontFamily: style.fontFamily,
120
+ fontWeight: style.fontWeight,
121
+ ...(style.fontStyle === undefined ? {} : { fontStyle: style.fontStyle }),
122
+ textAlign: component.textAlign,
123
+ textBaseline: "middle"
124
+ };
103
125
  }
@@ -1,3 +1,4 @@
1
+ import { resolveBarCellOpacities } from "./resolve.js";
1
2
  import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
2
3
  import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
3
4
  import {
@@ -7,7 +8,7 @@ import {
7
8
  } from "../../grammar/scales/index.js";
8
9
  import { mapScaleConsumerValues } from "../scales/map.js";
9
10
  import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
10
- import { resolveBarWidth } from "../../grammar/bars/geometry.js";
11
+ import { resolveBarWidth, resolveBarInterval } from "../../grammar/bars/geometry.js";
11
12
  import { finiteMidpoint } from "../../grammar/numeric.js";
12
13
 
13
14
  export function deriveAggregateRectangles(required, resolved, widthConfig) {
@@ -41,16 +42,20 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
41
42
  resolved.resolvedScales[stroke.scale],
42
43
  "stroke"
43
44
  );
45
+ const opacities = resolveBarCellOpacities(resolved, layer, segments.map(segment => segment.cell.sourceIndices.map(index => dataset.values[index])));
44
46
  const config = resolved.markConfigs[layer.id] ?? {};
45
47
  const existing = resolved.graphicSpec.objects[layer.id].items;
46
- const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
48
+ if (widthConfig?.interval !== undefined && (grouped || !temporal)) throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
49
+ const defaultWidth = widthConfig?.interval === undefined ? resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step)) : undefined;
47
50
  const indices = new Map(categoryScale.domain.map((value, index) => [value, index]));
48
51
  return segments.map(({ cell, start, end, seriesIndex }, index) => {
49
52
  const categoryValue = cell[channels.category];
53
+ const interval = widthConfig?.interval === undefined ? undefined : resolveBarInterval(widthConfig, categoryValue, categoryScale);
54
+ const width = interval?.width ?? defaultWidth;
50
55
  const categoryCenter = (temporal ? mapContinuousScaleValues : mapOrdinalPositionValues)([categoryValue], categoryScale)[0];
51
- let position = !grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
56
+ let position = interval?.position ?? (!grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
52
57
  ? (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step + categoryScale.bandwidth / 2 - width / 2
53
- : categoryCenter - width / 2;
58
+ : categoryCenter - width / 2);
54
59
  if (grouped) {
55
60
  const direction = Math.sign(categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]) || 1;
56
61
  const offsetCenter = offsetScale.start + seriesIndex * offsetScale.step + (Math.sign(offsetScale.step) || 1) * offsetScale.bandwidth / 2;
@@ -65,6 +70,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
65
70
  width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
66
71
  fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
67
72
  ...resolveBarAppearance(config, existing[index]?.properties),
68
- ...(strokes === undefined ? {} : { stroke: strokes[index] }) };
73
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }),
74
+ ...(opacities === undefined ? {} : { opacity: opacities[index] }) };
69
75
  });
70
76
  }
@@ -1,3 +1,4 @@
1
+ import { resolveBarCellOpacities } from "./resolve.js";
1
2
  import { readSeriesIdentity } from "../../grammar/pathSeries.js";
2
3
  import {
3
4
  findHistogramBinIndex,
@@ -172,6 +173,7 @@ export function deriveHistogramRectangles(required, resolved) {
172
173
  ...required,
173
174
  resolvedScales: resolved.resolvedScales
174
175
  });
176
+ const opacities = resolveBarCellOpacities(resolved, required.layer, segments.map(segment => segment.members));
175
177
  const existing = resolved.graphicSpec.objects[required.layer.id].items;
176
178
  const layout = resolveBarColorLayout(required.layer);
177
179
  const config = resolved.markConfigs[required.layer.id] ?? {};
@@ -224,7 +226,8 @@ export function deriveHistogramRectangles(required, resolved) {
224
226
  existing[index]?.properties.fill ??
225
227
  DEFAULT_BAR_FILL,
226
228
  ...resolveBarAppearance(config, existing[index]?.properties),
227
- ...(strokes === undefined ? {} : { stroke: strokes[index] })
229
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }),
230
+ ...(opacities === undefined ? {} : { opacity: opacities[index] })
228
231
  };
229
232
  });
230
233
  }
@@ -1,3 +1,4 @@
1
+ import { resolveBarCellOpacities } from "./resolve.js";
1
2
  import {
2
3
  mapContinuousScaleValues,
3
4
  mapOrdinalPositionValues,
@@ -6,6 +7,7 @@ import {
6
7
  } from "../../grammar/scales/index.js";
7
8
  import { mapScaleConsumerValues } from "../scales/map.js";
8
9
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
10
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../../grammar/bars/policy.js";
9
11
  import {
10
12
  DEFAULT_BAR_FILL,
11
13
  DEFAULT_BAR_STROKE,
@@ -14,7 +16,9 @@ import {
14
16
 
15
17
  export function deriveRangedRectangles(required, program, width) {
16
18
  const { layer, dataset } = required;
17
- const vertical = layer.encoding?.y2 !== undefined;
19
+ if (width?.interval !== undefined) throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
20
+ const centered = resolveBarGrain(layer) === BAR_GRAINS.centered;
21
+ const vertical = centered ? resolveBarChannels(layer).orientation === "vertical" : layer.encoding?.y2 !== undefined;
18
22
  const categoryChannel = vertical ? "x" : "y";
19
23
  const measureChannel = vertical ? "y" : "x";
20
24
  const secondaryChannel = `${measureChannel}2`;
@@ -23,16 +27,22 @@ export function deriveRangedRectangles(required, program, width) {
23
27
  const secondary = layer.encoding[secondaryChannel];
24
28
  const categoryScale = program.resolvedScales[category.scale];
25
29
  const measureScale = program.resolvedScales[primary.scale];
26
- const centers = mapOrdinalPositionValues(dataset.values.map(row => row[category.field]), categoryScale);
30
+ const categoryValues = readScaleField(dataset.values, category.field, category.fieldType, {
31
+ temporalUnit: category.temporalUnit
32
+ });
33
+ const centers = (centered || categoryScale.type === "time" ? mapContinuousScaleValues : mapOrdinalPositionValues)(
34
+ categoryValues, categoryScale
35
+ );
27
36
  const first = mapContinuousScaleValues(
28
37
  dataset.values.map(row => row[primary.field]),
29
38
  measureScale
30
39
  );
31
40
  const second = mapContinuousScaleValues(
32
- dataset.values.map(row => row[secondary.field]),
41
+ dataset.values.map(row => secondary === undefined ? 0 : row[secondary.field]),
33
42
  measureScale
34
43
  );
35
- const band = resolveBarWidth(width, Math.abs(categoryScale.bandwidth ?? categoryScale.step));
44
+ if (centered && width?.band !== undefined) throw new Error("Numeric-center bars require pixel width.");
45
+ const band = resolveBarWidth(centered ? width ?? { pixels: 5 } : width, Math.abs(categoryScale.bandwidth ?? categoryScale.step));
36
46
  const config = program.markConfigs[layer.id] ?? {};
37
47
  const appearance = config.barAppearance ?? {};
38
48
  const color = layer.encoding?.color;
@@ -50,6 +60,7 @@ export function deriveRangedRectangles(required, program, width) {
50
60
  program.resolvedScales[stroke.scale],
51
61
  "stroke"
52
62
  );
63
+ const opacities = resolveBarCellOpacities(program, layer, dataset.values.map(row => [row]));
53
64
  return dataset.values.map((_, index) => vertical ? {
54
65
  x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
55
66
  height: Math.abs(second[index] - first[index]), fill: fills[index],
@@ -59,7 +70,8 @@ export function deriveRangedRectangles(required, program, width) {
59
70
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
60
71
  1
61
72
  ),
62
- ...(strokes === undefined ? {} : { stroke: strokes[index] })
73
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }),
74
+ ...(opacities === undefined ? {} : { opacity: opacities[index] })
63
75
  } : {
64
76
  x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
65
77
  width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
@@ -69,6 +81,7 @@ export function deriveRangedRectangles(required, program, width) {
69
81
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
70
82
  1
71
83
  ),
72
- ...(strokes === undefined ? {} : { stroke: strokes[index] })
84
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }),
85
+ ...(opacities === undefined ? {} : { opacity: opacities[index] })
73
86
  });
74
87
  }