ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -59,24 +59,20 @@ export function resolveFacetLayout({
59
59
  "Facet shared legend width"
60
60
  );
61
61
  const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
62
- const columnWidths = Array.from({ length: resolvedColumns }, (_, column) =>
63
- Math.max(...resolvedChildren
64
- .filter((_, index) => index % resolvedColumns === column)
65
- .map(child => child.width))
66
- );
67
- const rowHeights = Array.from({ length: rowCount }, (_, row) =>
68
- Math.max(...resolvedChildren
69
- .slice(row * resolvedColumns, (row + 1) * resolvedColumns)
70
- .map(child => child.height))
71
- );
62
+ const columnWidths = Array(resolvedColumns).fill(-Infinity);
63
+ const rowHeights = Array(rowCount).fill(-Infinity);
64
+ resolvedChildren.forEach((child, index) => {
65
+ const column = index % resolvedColumns;
66
+ const row = Math.floor(index / resolvedColumns);
67
+ columnWidths[column] = Math.max(columnWidths[column], child.width);
68
+ rowHeights[row] = Math.max(rowHeights[row], child.height);
69
+ });
72
70
  const columnStarts = columnWidths.map((_, column) =>
73
- resolvedPadding.left + columnWidths
74
- .slice(0, column)
71
+ resolvedPadding.left + columnWidths.slice(0, column)
75
72
  .reduce((sum, width) => sum + width, 0) + resolvedGap * column
76
73
  );
77
74
  const rowStarts = rowHeights.map((_, row) =>
78
- resolvedTitleHeight + resolvedPadding.top + rowHeights
79
- .slice(0, row)
75
+ resolvedTitleHeight + resolvedPadding.top + rowHeights.slice(0, row)
80
76
  .reduce((sum, height) => sum + height, 0) + resolvedGap * row
81
77
  );
82
78
  const placements = resolvedChildren.map((child, index) => {
@@ -1,8 +1,13 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { resolveTextBounds } from "../core/textMetrics.js";
2
+ import {
3
+ resolveTextBounds,
4
+ textBoundsFitCanvas,
5
+ textBoundsIntersect
6
+ } from "../core/textMetrics.js";
3
7
  import {
4
8
  validateNonNegativeFinite,
5
- validateOptionObject
9
+ validateOptionObject,
10
+ validateWorkLimit
6
11
  } from "../core/validation.js";
7
12
 
8
13
  const AXES = new Set(["x", "y", "both"]);
@@ -10,6 +15,10 @@ const BOUNDS = new Set(["plot", "canvas"]);
10
15
  const POLICY_OPTIONS = Object.freeze([
11
16
  "axis", "padding", "maxDisplacement", "bounds"
12
17
  ]);
18
+ const MAX_EXHAUSTIVE_OFFSET_STEPS = 28;
19
+ const MAX_SEARCH_DISPLACEMENT = 1_000_000;
20
+ const DISTANT_RING_COUNT = 16;
21
+ const DISTANT_ANGLE_COUNT = 32;
13
22
 
14
23
  export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
15
24
  axis: "both",
@@ -98,16 +107,46 @@ function overflow(bounds, boundary) {
98
107
  export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
99
108
  const policy = normalizeLabelLayoutGeometry({ axis, padding, maxDisplacement });
100
109
  const step = Math.max(2, policy.padding);
101
- const limit = Math.ceil(policy.maxDisplacement / step);
110
+ const displacement = Math.min(
111
+ policy.maxDisplacement,
112
+ MAX_SEARCH_DISPLACEMENT
113
+ );
114
+ const limit = Math.ceil(displacement / step);
115
+ const localLimit = Math.min(limit, MAX_EXHAUSTIVE_OFFSET_STEPS);
102
116
  const candidates = [];
103
- for (let x = -limit; x <= limit; x += 1) {
104
- for (let y = -limit; y <= limit; y += 1) {
117
+ const addCandidate = (x, y) => {
118
+ if (![x, y].every(Number.isFinite)) return;
119
+ const distanceSquared = x ** 2 + y ** 2;
120
+ if (Math.sqrt(distanceSquared) > displacement + 1e-9) return;
121
+ candidates.push({ x, y, distanceSquared });
122
+ };
123
+ for (let x = -localLimit; x <= localLimit; x += 1) {
124
+ for (let y = -localLimit; y <= localLimit; y += 1) {
105
125
  if (policy.axis === "x" && y !== 0) continue;
106
126
  if (policy.axis === "y" && x !== 0) continue;
107
- const offset = { x: x * step, y: y * step };
108
- const distanceSquared = offset.x ** 2 + offset.y ** 2;
109
- if (Math.sqrt(distanceSquared) > policy.maxDisplacement + 1e-9) continue;
110
- candidates.push({ ...offset, distanceSquared });
127
+ addCandidate(x * step, y * step);
128
+ }
129
+ }
130
+ if (limit > localLimit) {
131
+ const localRadius = localLimit * step;
132
+ for (let ring = 1; ring <= DISTANT_RING_COUNT; ring += 1) {
133
+ const radius = localRadius + (displacement - localRadius) *
134
+ ring / DISTANT_RING_COUNT;
135
+ if (policy.axis === "x" || policy.axis === "y") {
136
+ addCandidate(
137
+ policy.axis === "x" ? radius : 0,
138
+ policy.axis === "y" ? radius : 0
139
+ );
140
+ addCandidate(
141
+ policy.axis === "x" ? -radius : 0,
142
+ policy.axis === "y" ? -radius : 0
143
+ );
144
+ continue;
145
+ }
146
+ for (let angle = 0; angle < DISTANT_ANGLE_COUNT; angle += 1) {
147
+ const radians = angle / DISTANT_ANGLE_COUNT * Math.PI * 2;
148
+ addCandidate(radius * Math.cos(radians), radius * Math.sin(radians));
149
+ }
111
150
  }
112
151
  }
113
152
  candidates.sort((first, second) =>
@@ -125,6 +164,8 @@ function textBounds(item, offset = { x: 0, y: 0 }) {
125
164
  y: item.y + offset.y,
126
165
  text: item.text,
127
166
  fontSize: item.fontSize,
167
+ fontFamily: item.fontFamily,
168
+ fontWeight: item.fontWeight,
128
169
  textAlign: item.textAlign,
129
170
  textBaseline: item.textBaseline,
130
171
  rotation: item.rotation
@@ -177,6 +218,10 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
177
218
  const ids = new Set();
178
219
  for (const item of items) validateItem(item, ids);
179
220
  const candidates = enumerateLabelOffsets(policy);
221
+ validateWorkLimit(
222
+ items.length ** 2 * candidates.length,
223
+ "Label layout work"
224
+ );
180
225
  const base = items.map(item => ({
181
226
  id: item.id,
182
227
  collisionBounds: expanded(textBounds(item), policy.padding)
@@ -231,9 +276,33 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
231
276
  });
232
277
  }
233
278
 
234
- function contains(bounds, point) {
235
- return point.x >= bounds.left && point.x <= bounds.right &&
236
- point.y >= bounds.top && point.y <= bounds.bottom;
279
+ export function assertPolarTextLayout({ canvas, items, label }) {
280
+ const dimensions = canvas?.properties;
281
+ if (
282
+ ![dimensions?.width, dimensions?.height].every(Number.isFinite) ||
283
+ dimensions.width <= 0 || dimensions.height <= 0
284
+ ) {
285
+ throw new Error("Polar text layout requires finite Canvas dimensions.");
286
+ }
287
+ const bounds = items.map(resolveTextBounds);
288
+ const outside = bounds.some(item => !textBoundsFitCanvas(item, dimensions));
289
+ let overlap = false;
290
+ for (let first = 0; first < bounds.length && !overlap; first += 1) {
291
+ for (let second = first + 1; second < bounds.length; second += 1) {
292
+ if (
293
+ (Math.abs(items[first].x - items[second].x) > 1e-9 ||
294
+ Math.abs(items[first].y - items[second].y) > 1e-9) &&
295
+ textBoundsIntersect(bounds[first], bounds[second])
296
+ ) {
297
+ overlap = true;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ if (outside || overlap) {
303
+ const verb = label.endsWith("labels") ? "require" : "requires";
304
+ throw new Error(`${label} ${verb} sufficient non-overlapping Canvas space.`);
305
+ }
237
306
  }
238
307
 
239
308
  export function resolveLabelLeader(item) {
@@ -242,10 +311,17 @@ export function resolveLabelLeader(item) {
242
311
  }
243
312
  if (item.dx === 0 && item.dy === 0) return undefined;
244
313
  const source = { x: item.sourceX, y: item.sourceY };
245
- if (contains(item.bounds, source)) return undefined;
314
+ if (
315
+ source.x >= item.bounds.left && source.x <= item.bounds.right &&
316
+ source.y >= item.bounds.top && source.y <= item.bounds.bottom
317
+ ) return undefined;
318
+ const midpoint = (start, end) => {
319
+ const sum = start + end;
320
+ return Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
321
+ };
246
322
  const center = {
247
- x: (item.bounds.left + item.bounds.right) / 2,
248
- y: (item.bounds.top + item.bounds.bottom) / 2
323
+ x: midpoint(item.bounds.left, item.bounds.right),
324
+ y: midpoint(item.bounds.top, item.bounds.bottom)
249
325
  };
250
326
  const dx = center.x - source.x;
251
327
  const dy = center.y - source.y;
@@ -1,3 +1,8 @@
1
+ import {
2
+ formatVisibleText,
3
+ measureTextWidth
4
+ } from "../core/textMetrics.js";
5
+
1
6
  export function measureLegendSymbolHeight(config) {
2
7
  return Math.max(...config.symbol.layers.map(layer => {
3
8
  if (layer.type === "swatch") return layer.height;
@@ -6,41 +11,38 @@ export function measureLegendSymbolHeight(config) {
6
11
  }));
7
12
  }
8
13
 
9
- export function measureLegendTextWidth(value) {
10
- return String(value).length * 7;
14
+ export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
15
+ return measureTextWidth(formatVisibleText(value), style);
11
16
  }
12
17
 
13
18
  export function resolveLegendGrid(config, width, count) {
14
- const labels = config.domain.map(String);
19
+ const labels = config.domain.map(formatVisibleText);
15
20
  const itemWidths = labels.map(
16
- label => width + config.labels.offset + label.length * 7
21
+ label => width + config.labels.offset +
22
+ measureLegendTextWidth(label, config.labels)
17
23
  );
18
24
  const columnCount = Math.min(config.columns ?? count, count);
19
25
  const rowCount = Math.ceil(count / columnCount);
26
+ const columnWidths = [];
20
27
  const cells = Array.from({ length: count }, (_, index) => {
21
- if (config.direction === "horizontal") {
22
- return { column: index % columnCount, row: Math.floor(index / columnCount) };
23
- }
24
- return {
25
- column: Math.floor(index / rowCount),
26
- row: index % rowCount
27
- };
28
+ const cell = config.direction === "horizontal"
29
+ ? { column: index % columnCount, row: Math.floor(index / columnCount) }
30
+ : { column: Math.floor(index / rowCount), row: index % rowCount };
31
+ columnWidths[cell.column] = Math.max(
32
+ columnWidths[cell.column] ?? 0,
33
+ itemWidths[index]
34
+ );
35
+ return cell;
28
36
  });
29
- const actualColumns = Math.max(...cells.map(cell => cell.column)) + 1;
30
- const actualRows = Math.max(...cells.map(cell => cell.row)) + 1;
31
- const columnWidths = Array.from({ length: actualColumns }, (_, column) =>
32
- Math.max(...cells.map((cell, index) =>
33
- cell.column === column ? itemWidths[index] : 0
34
- ))
35
- );
37
+ const actualColumns = columnWidths.length;
36
38
  const gridWidth = columnWidths.reduce((sum, value) => sum + value, 0) +
37
39
  config.itemGap * Math.max(0, actualColumns - 1);
38
40
  const rowHeight = Math.max(
39
41
  config.labels.fontSize,
40
42
  measureLegendSymbolHeight(config)
41
43
  );
42
- const gridHeight = rowHeight * actualRows +
43
- config.itemGap * Math.max(0, actualRows - 1);
44
+ const gridHeight = rowHeight * rowCount +
45
+ config.itemGap * Math.max(0, rowCount - 1);
44
46
  return { cells, columnWidths, gridWidth, gridHeight, rowHeight };
45
47
  }
46
48
 
@@ -4,13 +4,19 @@ export const SIDE_LEGEND_SYMBOL_CENTER = 16;
4
4
  export const SIDE_LEGEND_LABEL_START = 44;
5
5
  export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
6
6
 
7
+ function midpoint(start, end) {
8
+ const sum = start + end;
9
+ const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
10
+ return result === 0 ? 0 : result;
11
+ }
12
+
7
13
  function unionBounds(bounds) {
8
- return {
9
- left: Math.min(...bounds.map(item => item.left)),
10
- right: Math.max(...bounds.map(item => item.right)),
11
- top: Math.min(...bounds.map(item => item.top)),
12
- bottom: Math.max(...bounds.map(item => item.bottom))
13
- };
14
+ return bounds.reduce((union, item) => ({
15
+ left: Math.min(union.left, item.left),
16
+ right: Math.max(union.right, item.right),
17
+ top: Math.min(union.top, item.top),
18
+ bottom: Math.max(union.bottom, item.bottom)
19
+ }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
14
20
  }
15
21
 
16
22
  function decoration(border) {
@@ -20,19 +26,20 @@ function decoration(border) {
20
26
  }
21
27
 
22
28
  function horizontalExtent(blocks) {
23
- const left = [0];
24
- const right = [0];
29
+ let left = 0;
30
+ let right = 0;
25
31
  for (const block of blocks) {
26
- if (block.title !== undefined) right.push(block.title.width);
27
- left.push(
32
+ if (block.title !== undefined) right = Math.max(right, block.title.width);
33
+ left = Math.min(left,
28
34
  SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX
29
35
  );
30
- right.push(
31
- SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX
36
+ right = Math.max(
37
+ right,
38
+ SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX,
39
+ SIDE_LEGEND_LABEL_START + block.labels.width
32
40
  );
33
- right.push(SIDE_LEGEND_LABEL_START + block.labels.width);
34
41
  }
35
- return { left: Math.min(...left), right: Math.max(...right) };
42
+ return { left, right };
36
43
  }
37
44
 
38
45
  function placeBlock(block, cursor) {
@@ -81,7 +88,10 @@ export function resolveSideLegendLane({
81
88
  const blocks = groups.flatMap(group => group.blocks);
82
89
  if (blocks.length < 2) return undefined;
83
90
  const laneExtent = horizontalExtent(blocks);
84
- const offset = Math.max(...blocks.map(block => block.offset));
91
+ const offset = blocks.reduce(
92
+ (maximum, block) => Math.max(maximum, block.offset),
93
+ -Infinity
94
+ );
85
95
  const titleStartX = side === "right"
86
96
  ? plot.x + plot.width + offset
87
97
  : plot.x - offset - laneExtent.right;
@@ -160,21 +170,12 @@ export function resolveSideLegendLane({
160
170
  };
161
171
  }
162
172
 
163
- function translateBounds(bounds, dy) {
164
- return {
165
- left: bounds.left,
166
- right: bounds.right,
167
- top: bounds.top + dy,
168
- bottom: bounds.bottom + dy
169
- };
170
- }
171
-
172
- function translateBoundsX(bounds, dx) {
173
+ function translateBounds(bounds, dx, dy) {
173
174
  return {
174
175
  left: bounds.left + dx,
175
176
  right: bounds.right + dx,
176
- top: bounds.top,
177
- bottom: bounds.bottom
177
+ top: bounds.top + dy,
178
+ bottom: bounds.bottom + dy
178
179
  };
179
180
  }
180
181
 
@@ -198,13 +199,13 @@ function packHorizontalRows(groups, plot) {
198
199
  throw new Error("Horizontal legend block requires more plot width.");
199
200
  }
200
201
  let dx = cursor - interval.left;
201
- let placed = translateBoundsX(interval, dx);
202
+ let placed = translateBounds(interval, dx, 0);
202
203
  if (row.length > 0 && placed.right > plot.x + plot.width) {
203
204
  rows.push(row);
204
205
  row = [];
205
206
  cursor = plot.x;
206
207
  dx = cursor - interval.left;
207
- placed = translateBoundsX(interval, dx);
208
+ placed = translateBounds(interval, dx, 0);
208
209
  }
209
210
  row.push({ group, interval: placed, dx });
210
211
  cursor = placed.right + HORIZONTAL_LEGEND_BLOCK_GAP;
@@ -220,15 +221,22 @@ function normalizeHorizontalRow(entries) {
220
221
  const commonTitleY = stacked[0]?.group.title.y;
221
222
  const titleDescent = stacked.length === 0
222
223
  ? 0
223
- : Math.max(...stacked.map(
224
- entry => entry.group.title.bounds.bottom - entry.group.title.y
225
- ));
224
+ : stacked.reduce((maximum, entry) => Math.max(
225
+ maximum,
226
+ entry.group.title.bounds.bottom - entry.group.title.y
227
+ ), -Infinity);
226
228
  const elementAnchor = commonTitleY === undefined
227
- ? (entries[0].group.element.top + entries[0].group.element.bottom) / 2
229
+ ? midpoint(
230
+ entries[0].group.element.top,
231
+ entries[0].group.element.bottom
232
+ )
228
233
  : commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
229
234
  return entries.map(({ group, dx }) => {
230
235
  const contentDy = commonTitleY === undefined
231
- ? elementAnchor - (group.element.top + group.element.bottom) / 2
236
+ ? elementAnchor - midpoint(
237
+ group.element.top,
238
+ group.element.bottom
239
+ )
232
240
  : elementAnchor - group.element.top;
233
241
  const titleDy = group.title === undefined
234
242
  ? 0
@@ -238,8 +246,8 @@ function normalizeHorizontalRow(entries) {
238
246
  const foreground = unionBounds([
239
247
  ...(group.title === undefined
240
248
  ? []
241
- : [translateBoundsX(translateBounds(group.title.bounds, titleDy), dx)]),
242
- translateBoundsX(translateBounds(group.content, contentDy), dx)
249
+ : [translateBounds(group.title.bounds, dx, titleDy)]),
250
+ translateBounds(group.content, dx, contentDy)
243
251
  ]);
244
252
  return {
245
253
  id: group.id,
@@ -259,8 +267,8 @@ function translateHorizontalPlacement(placement, dy) {
259
267
  ...placement,
260
268
  titleDy: placement.titleDy + dy,
261
269
  contentDy: placement.contentDy + dy,
262
- foreground: translateBounds(placement.foreground, dy),
263
- occupied: translateBounds(placement.occupied, dy)
270
+ foreground: translateBounds(placement.foreground, 0, dy),
271
+ occupied: translateBounds(placement.occupied, 0, dy)
264
272
  };
265
273
  }
266
274
 
@@ -285,8 +293,14 @@ export function resolveHorizontalLegendLane({
285
293
  let dy;
286
294
  if (index === 0) {
287
295
  const anchor = edge === "top"
288
- ? Math.max(...packed[index].map(item => item.group.horizontal.bottom))
289
- : Math.min(...packed[index].map(item => item.group.horizontal.top));
296
+ ? packed[index].reduce((maximum, item) => Math.max(
297
+ maximum,
298
+ item.group.horizontal.bottom
299
+ ), -Infinity)
300
+ : packed[index].reduce((minimum, item) => Math.min(
301
+ minimum,
302
+ item.group.horizontal.top
303
+ ), Infinity);
290
304
  dy = edge === "top"
291
305
  ? anchor - rowBounds.bottom
292
306
  : anchor - rowBounds.top;
@@ -44,7 +44,7 @@ export function buildTitleReadingBlock({ text, subtitle }, config) {
44
44
  subtitleLines,
45
45
  titleCenters,
46
46
  subtitleCenters,
47
- width: Math.max(...widths),
47
+ width: widths.reduce((maximum, width) => Math.max(maximum, width), -Infinity),
48
48
  height
49
49
  };
50
50
  }
@@ -74,12 +74,12 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
74
74
  }
75
75
 
76
76
  function unionBounds(bounds) {
77
- return {
78
- left: Math.min(...bounds.map(item => item.left)),
79
- right: Math.max(...bounds.map(item => item.right)),
80
- top: Math.min(...bounds.map(item => item.top)),
81
- bottom: Math.max(...bounds.map(item => item.bottom))
82
- };
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
83
  }
84
84
 
85
85
  export function resolveTitleComponentBounds(component, style) {
@@ -11,13 +11,12 @@ import {
11
11
  import {
12
12
  isDiscretePositionScaleType,
13
13
  mapContinuousScaleValues,
14
- mapOrdinalPositionValues,
15
- mapSequentialColors
14
+ mapOrdinalPositionValues
16
15
  } from "../../grammar/scales/index.js";
16
+ import { mapScaleConsumerValues } from "../scales/map.js";
17
17
  import {
18
18
  DEFAULT_BAR_FILL,
19
- DEFAULT_BAR_STROKE,
20
- DEFAULT_BAR_STROKE_WIDTH
19
+ resolveBarAppearance
21
20
  } from "./resolve.js";
22
21
  import { deriveGroupedRectangles } from "./grouped.js";
23
22
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
@@ -72,14 +71,8 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
72
71
  const baseline = DEFAULT_SERIES_BASELINE;
73
72
  const continuousColor = colorEncoding?.fieldType === "quantitative";
74
73
  if (continuousColor) {
75
- const mappedColors = mapSequentialColors(
76
- cells.map(cell => cell.color),
77
- colorScale.domain,
78
- colorScale.range,
79
- {
80
- interpolation: colorScale.interpolate,
81
- clamp: colorScale.clamp ?? false
82
- }
74
+ const mappedColors = mapScaleConsumerValues(
75
+ cells.map(cell => cell.color), colorScale, "color"
83
76
  );
84
77
  const cellByCategory = new Map(
85
78
  cells.map((cell, index) => [cell[channels.category], { cell, index }])
@@ -97,8 +90,14 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
97
90
  [categoryValue],
98
91
  categoryScale
99
92
  )[0];
93
+ const [segment] = layoutSeriesPartition(
94
+ [cell[channels.measure]],
95
+ layout,
96
+ { baseline }
97
+ );
98
+ if (segment === undefined) return [];
100
99
  const [start, end] = mapContinuousScaleValues(
101
- [baseline, cell[channels.measure]],
100
+ [segment.start, segment.end],
102
101
  measureScale
103
102
  );
104
103
  return [{
@@ -107,19 +106,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
107
106
  width: vertical ? thickness : Math.abs(start - end),
108
107
  height: vertical ? Math.abs(start - end) : thickness,
109
108
  fill: mappedColors[index],
110
- stroke: appearance.stroke === false
111
- ? "transparent"
112
- : appearance.stroke ?? config.stroke ??
113
- existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
114
- strokeWidth: appearance.stroke === false
115
- ? 0
116
- : appearance.strokeWidth ?? config.strokeWidth ??
117
- existing[index]?.properties.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
118
- ...((appearance.opacity ?? config.opacity) === undefined
119
- ? (existing[index]?.properties.opacity === undefined
120
- ? {}
121
- : { opacity: existing[index].properties.opacity })
122
- : { opacity: appearance.opacity ?? config.opacity })
109
+ ...resolveBarAppearance(config, existing[index]?.properties)
123
110
  }];
124
111
  });
125
112
  }
@@ -168,20 +155,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
168
155
  fill: segment.color === undefined
169
156
  ? appearance.fill ?? config.fill ?? colors.get(segment.color)
170
157
  : colors.get(segment.color),
171
- stroke: appearance.stroke === false
172
- ? "transparent"
173
- : appearance.stroke ?? config.stroke ?? existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
174
- strokeWidth:
175
- appearance.stroke === false
176
- ? 0
177
- : appearance.strokeWidth ?? config.strokeWidth ??
178
- existing[index]?.properties.strokeWidth ??
179
- DEFAULT_BAR_STROKE_WIDTH,
180
- ...((appearance.opacity ?? config.opacity) === undefined
181
- ? (existing[index]?.properties.opacity === undefined
182
- ? {}
183
- : { opacity: existing[index].properties.opacity })
184
- : { opacity: appearance.opacity ?? config.opacity })
158
+ ...resolveBarAppearance(config, existing[index]?.properties)
185
159
  };
186
160
  });
187
161
  }
@@ -5,11 +5,11 @@ import {
5
5
  } from "../../grammar/scales/index.js";
6
6
  import { sameOrderedValues } from "../../core/validation.js";
7
7
  import {
8
- DEFAULT_BAR_STROKE,
9
- DEFAULT_BAR_STROKE_WIDTH
8
+ resolveBarAppearance
10
9
  } from "./resolve.js";
11
10
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
12
11
  import { DEFAULT_SERIES_BASELINE } from "../../grammar/seriesLayout.js";
12
+ import { finiteMidpoint } from "../../grammar/numeric.js";
13
13
  import {
14
14
  BAR_ORIENTATIONS,
15
15
  resolveBarChannels,
@@ -58,7 +58,10 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
58
58
  colorScale.range[index % colorScale.range.length]
59
59
  ])
60
60
  );
61
- const offsetMidpoint = (offsetScale.range[0] + offsetScale.range[1]) / 2;
61
+ const offsetMidpoint = finiteMidpoint(
62
+ offsetScale.range[0],
63
+ offsetScale.range[1]
64
+ );
62
65
  const categoryDirection = Math.sign(
63
66
  categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]
64
67
  ) || 1;
@@ -78,7 +81,6 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
78
81
  );
79
82
  const existing = resolved.graphicSpec.objects[layer.id].items;
80
83
  const config = resolved.markConfigs[layer.id] ?? {};
81
- const appearance = config.barAppearance ?? {};
82
84
 
83
85
  return cells.map((cell, index) => {
84
86
  const categoryValue = cell[channels.category];
@@ -136,20 +138,7 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
136
138
  return {
137
139
  ...geometry,
138
140
  fill: colors.get(cell.color),
139
- stroke: appearance.stroke === false
140
- ? "transparent"
141
- : appearance.stroke ?? config.stroke ?? existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
142
- strokeWidth:
143
- appearance.stroke === false
144
- ? 0
145
- : appearance.strokeWidth ?? config.strokeWidth ??
146
- existing[index]?.properties.strokeWidth ??
147
- DEFAULT_BAR_STROKE_WIDTH,
148
- ...((appearance.opacity ?? config.opacity) === undefined
149
- ? (existing[index]?.properties.opacity === undefined
150
- ? {}
151
- : { opacity: existing[index].properties.opacity })
152
- : { opacity: appearance.opacity ?? config.opacity })
141
+ ...resolveBarAppearance(config, existing[index]?.properties)
153
142
  };
154
143
  });
155
144
  }
@@ -13,8 +13,7 @@ import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
13
13
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
14
14
  import {
15
15
  DEFAULT_BAR_FILL,
16
- DEFAULT_BAR_STROKE,
17
- DEFAULT_BAR_STROKE_WIDTH
16
+ resolveBarAppearance
18
17
  } from "./resolve.js";
19
18
 
20
19
  export function deriveHistogramSegments({
@@ -141,23 +140,7 @@ export function deriveHistogramRectangles(required, resolved) {
141
140
  config.fill ??
142
141
  existing[index]?.properties.fill ??
143
142
  DEFAULT_BAR_FILL,
144
- stroke:
145
- appearance.stroke === false
146
- ? "transparent"
147
- : appearance.stroke ?? config.stroke ??
148
- existing[index]?.properties.stroke ??
149
- DEFAULT_BAR_STROKE,
150
- strokeWidth:
151
- appearance.stroke === false
152
- ? 0
153
- : appearance.strokeWidth ?? config.strokeWidth ??
154
- existing[index]?.properties.strokeWidth ??
155
- DEFAULT_BAR_STROKE_WIDTH,
156
- ...((appearance.opacity ?? config.opacity) === undefined
157
- ? (existing[index]?.properties.opacity === undefined
158
- ? {}
159
- : { opacity: existing[index].properties.opacity })
160
- : { opacity: appearance.opacity ?? config.opacity })
143
+ ...resolveBarAppearance(config, existing[index]?.properties)
161
144
  };
162
145
  });
163
146
  }