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
@@ -9,14 +9,20 @@ import {
9
9
  validateGeneratedItemLimit
10
10
  } from "../../../../core/validation.js";
11
11
  import { formatTimeTick } from "../../../../grammar/ticks.js";
12
+ import {
13
+ formatValue,
14
+ validateValueFormat
15
+ } from "../../../../grammar/valueFormat.js";
12
16
  import {
13
17
  formatDistinctNumericSamples,
14
18
  sampleNumericRange
15
19
  } from "../../../../grammar/numeric.js";
16
20
  import { resolveGraphicBounds } from "../../../../layout/canvas.js";
21
+ import { isHorizontalEdgeLegend } from "../../../../layout/legendLane.js";
17
22
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
18
23
  "../../../../theme/defaults.js";
19
24
  import { findLayer } from "../../../../selectors/layers.js";
25
+ import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
20
26
  import { findCanvasGraphic } from
21
27
  "../../../../materialization/graphicHierarchy.js";
22
28
 
@@ -26,7 +32,7 @@ const OPTIONS = [
26
32
  "direction", "columns", "titlePosition"
27
33
  ];
28
34
  const TEXT_OPTIONS = [
29
- "offset", "color", "fontSize", "fontFamily", "fontWeight"
35
+ "offset", "color", "fontSize", "fontFamily", "fontWeight", "format"
30
36
  ];
31
37
  const BORDER_OPTIONS = [
32
38
  "color", "lineWidth", "padding", "background"
@@ -82,9 +88,19 @@ export function normalizeLegendTextOptions(value, label, defaults) {
82
88
  ) {
83
89
  throw new TypeError(`${label} fontWeight must be a string or finite number.`);
84
90
  }
91
+ if (Object.hasOwn(result, "format")) {
92
+ result.format = validateValueFormat(result.format, `${label} format`);
93
+ }
85
94
  return result;
86
95
  }
87
96
 
97
+ export function normalizeLegendTitleOptions(value, label, defaults) {
98
+ if (value !== undefined) {
99
+ validateOptionObject(value, ["color", "fontSize", "fontFamily", "fontWeight"], label);
100
+ }
101
+ return normalizeLegendTextOptions(value, label, defaults);
102
+ }
103
+
88
104
  export function normalizeLegendBorder(value) {
89
105
  if (value === undefined || value === false) return false;
90
106
  if (value !== true && !isPlainObject(value)) {
@@ -110,6 +126,9 @@ export function normalizeContinuousLegend(args, kind) {
110
126
  if (!["left", "center", "right"].includes(align)) {
111
127
  throw new Error(`Unsupported legend alignment "${align}".`);
112
128
  }
129
+ if (["left", "right"].includes(position) && align !== "center") {
130
+ throw new Error("Side continuous legends require center alignment.");
131
+ }
113
132
  const count = args.count ?? 5;
114
133
  if (!Number.isInteger(count) || count < 2) {
115
134
  throw new RangeError(
@@ -162,7 +181,7 @@ export function normalizeContinuousLegend(args, kind) {
162
181
  ? { ...DEFAULT_LABELS, offset: 8 }
163
182
  : DEFAULT_LABELS
164
183
  ),
165
- titleStyle: normalizeLegendTextOptions(
184
+ titleStyle: normalizeLegendTitleOptions(
166
185
  args.titleStyle,
167
186
  "createLegend.titleStyle",
168
187
  DEFAULT_TITLE
@@ -182,17 +201,17 @@ export function selectLegendLayer(program, requested, predicate) {
182
201
  return candidates.includes(candidate) ? candidate : undefined;
183
202
  }
184
203
 
185
- export function resolveContinuousPoint(program, requested, channel) {
204
+ export function resolveContinuousLegendLayer(program, requested, channel) {
186
205
  const layer = selectLegendLayer(
187
206
  program,
188
207
  requested,
189
- candidate => candidate.mark?.type === "point" &&
190
- candidate.encoding?.[channel]?.scale !== undefined
208
+ channel === "opacity" ? isOpacityLegendLayer : candidate =>
209
+ candidate.mark?.type === "point" && candidate.encoding?.[channel]?.scale !== undefined
191
210
  );
192
211
  if (layer === undefined) {
193
212
  throw new Error(
194
213
  requested === undefined
195
- ? `${channel} legend requires one eligible point mark.`
214
+ ? `${channel} legend requires one eligible ${channel === "opacity" ? "point or line" : "point"} mark.`
196
215
  : `Unknown ${channel} legend target "${requested}".`
197
216
  );
198
217
  }
@@ -251,10 +270,18 @@ export function sampleContinuousValues(domain, count) {
251
270
  );
252
271
  }
253
272
 
254
- export function formatContinuousValues(values, domain, fieldType) {
255
- return fieldType === "temporal"
256
- ? values.map(value => formatTimeTick(value, domain))
257
- : formatDistinctNumericSamples(values);
273
+ export function formatContinuousValues(values, domain, fieldType, format = "auto") {
274
+ const resolved = validateValueFormat(format, "Legend label format");
275
+ if (resolved === "auto") {
276
+ return fieldType === "temporal"
277
+ ? values.map(value => formatTimeTick(value, domain))
278
+ : formatDistinctNumericSamples(values);
279
+ }
280
+ return values.map(value => formatValue(value, {
281
+ format: resolved,
282
+ valueType: fieldType === "temporal" ? "temporal" : "quantitative",
283
+ label: "Legend label format"
284
+ }));
258
285
  }
259
286
 
260
287
  export function styleContinuousText(
@@ -284,7 +311,10 @@ export function resolveLegendTextBounds(position, text, style) {
284
311
  });
285
312
  }
286
313
 
287
- export function assertLegendBoundsInsideCanvas(bounds, canvas, label) {
314
+ export function assertLegendBoundsInsideCanvas(bounds, canvas, label, config) {
315
+ // Horizontal content is intrinsic until the shared lane places its actual
316
+ // concrete bounds. Only that final placement can determine Canvas fit.
317
+ if (isHorizontalEdgeLegend(config)) return;
288
318
  if (bounds.some(item =>
289
319
  item.left < 0 || item.right > canvas.width ||
290
320
  item.top < 0 || item.bottom > canvas.height
@@ -297,7 +327,8 @@ export function resolveLegendBackgroundFromBounds(
297
327
  bounds,
298
328
  border,
299
329
  canvas,
300
- label
330
+ label,
331
+ config
301
332
  ) {
302
333
  if (border === false) return undefined;
303
334
  const strokeExtent = border.lineWidth / 2;
@@ -315,11 +346,11 @@ export function resolveLegendBackgroundFromBounds(
315
346
  y -= border.padding;
316
347
  right += border.padding;
317
348
  bottom += border.padding;
318
- if (
349
+ if (!isHorizontalEdgeLegend(config) && (
319
350
  x - strokeExtent < 0 || y - strokeExtent < 0 ||
320
351
  right + strokeExtent > canvas.width ||
321
352
  bottom + strokeExtent > canvas.height
322
- ) {
353
+ )) {
323
354
  throw new Error(`${label} background requires more Canvas margin space.`);
324
355
  }
325
356
  return { x, y, width: right - x, height: bottom - y };
@@ -334,3 +365,27 @@ export function editLegendBackground(program, id, bounds, border) {
334
365
  strokeWidth: border.lineWidth
335
366
  });
336
367
  }
368
+
369
+ export function normalizeItemLegendLayout(args) {
370
+ const position = args.position ?? "right";
371
+ if (!["right", "left", "top", "bottom"].includes(position)) throw new Error(`Unsupported legend position "${position}".`);
372
+ const side = ["left", "right"].includes(position);
373
+ const layout = args.layout === undefined ? "edge" : args.layout;
374
+ if (layout !== "edge") throw new Error('This legend requires layout "edge".');
375
+ const align = args.align ?? "center";
376
+ if (!["left", "center", "right"].includes(align)) throw new Error(`Unsupported legend alignment "${align}".`);
377
+ const direction = args.direction ?? (side ? "vertical" : "horizontal");
378
+ if (!["horizontal", "vertical"].includes(direction)) throw new Error(`Unsupported legend direction "${direction}".`);
379
+ const columns = args.columns;
380
+ if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) throw new RangeError("Legend columns must be a positive integer.");
381
+ const titlePosition = args.titlePosition ?? "top";
382
+ if (!["top", "left"].includes(titlePosition)) throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
383
+ if (side && (direction !== "vertical" || align !== "center" || (columns !== undefined && columns !== 1) || titlePosition !== "top")) {
384
+ throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");
385
+ }
386
+ const offset = args.offset ?? 30;
387
+ const itemGap = args.itemGap ?? 28;
388
+ validateNonNegative(offset, "Legend offset");
389
+ validatePositive(itemGap, "Legend itemGap");
390
+ return { position, layout, align, direction, columns, titlePosition, offset, itemGap };
391
+ }
@@ -1,7 +1,8 @@
1
1
  import { action } from "../../../../core/action.js";
2
2
  import { isPlainObject } from "../../../../core/immutable.js";
3
3
  import { validateKeys } from "../../../../core/validation.js";
4
- import { interpolateColorStops } from "../../../../grammar/scales/color.js";
4
+ import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
5
+ import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
5
6
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
7
  import {
7
8
  assertLegendBoundsInsideCanvas,
@@ -20,6 +21,8 @@ import {
20
21
  import { resolveLegendGraphicPlacement } from
21
22
  "../../../../materialization/graphicHierarchy.js";
22
23
 
24
+ export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12 });
25
+
23
26
  const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
24
27
 
25
28
  function resolveGradientLayout(program, config, scale) {
@@ -43,7 +46,7 @@ function resolveGradientLayout(program, config, scale) {
43
46
  ? plot.y - config.offset - thickness - config.labels.offset -
44
47
  config.labels.fontSize
45
48
  : plot.y + plot.height + config.offset +
46
- config.titleStyle.fontSize + 12;
49
+ (config.titleVisible === false ? 0 : config.titleStyle.fontSize + 12);
47
50
  }
48
51
  const title = vertical
49
52
  ? { x, y: plot.y + 20, align: "left" }
@@ -52,9 +55,20 @@ function resolveGradientLayout(program, config, scale) {
52
55
  y: y - 12 - config.titleStyle.fontSize / 2,
53
56
  align: "center"
54
57
  };
55
- const values = sampleContinuousValues(scale.domain, config.count);
56
- const texts = formatContinuousValues(values, scale.domain, config.fieldType);
57
- const fractions = values.map((_, index) => index / (values.length - 1));
58
+ const values = [...sampleContinuousValues(scale.domain, config.count)];
59
+ if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
60
+ values.push(scale.midpoint);
61
+ values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
62
+ }
63
+ const texts = formatContinuousValues(
64
+ values,
65
+ scale.domain,
66
+ config.fieldType,
67
+ config.labels.format
68
+ );
69
+ const fractions = values.map((value, index) => scale.midpoint === undefined
70
+ ? index / (values.length - 1)
71
+ : inverseLerp(value, ...scale.domain));
58
72
  const labelOffset = config.labels.offset;
59
73
  const labels = vertical
60
74
  ? fractions.map(fraction => ({
@@ -97,22 +111,23 @@ function resolveGradientLayout(program, config, scale) {
97
111
  const labelBounds = labels.map((label, index) =>
98
112
  resolveLegendTextBounds(label, texts[index], config.labels)
99
113
  );
100
- const titleBounds = resolveLegendTextBounds(
114
+ const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
101
115
  title,
102
116
  config.title,
103
117
  config.titleStyle
104
118
  );
105
- const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds, titleBounds];
119
+ const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
120
+ ...(config.titleVisible === false ? [] : [titleBounds])];
106
121
  assertLegendBoundsInsideCanvas(
107
122
  occupiedBounds,
108
123
  canvas,
109
- "Gradient legend layout"
124
+ "Gradient legend layout", config
110
125
  );
111
126
  const background = resolveLegendBackgroundFromBounds(
112
127
  occupiedBounds,
113
128
  config.border,
114
129
  canvas,
115
- "Gradient legend"
130
+ "Gradient legend", config
116
131
  );
117
132
  return {
118
133
  vertical, x, y, length, thickness, values, texts, labels, ticks, title,
@@ -163,11 +178,11 @@ export const rematerializeGradientLegend = action(
163
178
  const stripSize = layout.length / stripCount;
164
179
  const strips = Array.from({ length: stripCount }, (_, index) => {
165
180
  const fraction = (index + 0.5) / stripCount;
166
- const color = interpolateColorStops(
167
- scale.range,
168
- layout.vertical ? 1 - fraction : fraction,
169
- scale.interpolate
170
- );
181
+ const position = layout.vertical ? 1 - fraction : fraction;
182
+ // Keep legacy uniform samples exact without a value/domain round trip.
183
+ const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
184
+ const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
185
+ const [color] = mapScaleConsumerValues([value], samplingScale, "color");
171
186
  return {
172
187
  x: layout.x + (layout.vertical ? 0 : index * stripSize),
173
188
  y: layout.y + (layout.vertical ? index * stripSize : 0),
@@ -283,86 +298,99 @@ export const rematerializeGradientLegend = action(
283
298
  }
284
299
  );
285
300
 
301
+
302
+ export function resolveGradientLegendCreation(program, args = {}) {
303
+ const config = normalizeContinuousLegend(args, "gradient");
304
+ if (args.channels !== undefined && (
305
+ !Array.isArray(args.channels) ||
306
+ args.channels.length !== 1 ||
307
+ args.channels[0] !== "color"
308
+ )) {
309
+ throw new Error('Gradient legend requires channels: ["color"].');
310
+ }
311
+ if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
312
+ throw new TypeError("createLegend.gradient must be a plain object.");
313
+ }
314
+ validateKeys(
315
+ args.gradient ?? {},
316
+ GRADIENT_OPTIONS,
317
+ "createLegend.gradient"
318
+ );
319
+ config.gradient = {
320
+ length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
321
+ thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
322
+ };
323
+ config.titleVisible = true;
324
+ validatePositive(config.gradient.length, "Gradient length");
325
+ validatePositive(config.gradient.thickness, "Gradient thickness");
326
+ const resolved = resolveGradientConfig(program, config);
327
+ return resolved;
328
+ }
329
+
330
+ export function createGradientLegendFromConfig(program, config) {
331
+ const resolved = resolveGradientConfig(program, config);
332
+ resolveGradientLayout(program, resolved.config, resolved.scale);
333
+ let next = program
334
+ .editSemantic({
335
+ property: "guide.legend.color.scale",
336
+ value: resolved.encoding.scale
337
+ })
338
+ .editSemantic({
339
+ property: "guide.legend.color.title",
340
+ value: resolved.config.title
341
+ })
342
+ ._withLegendConfig("gradient", resolved.config);
343
+ if (resolved.config.border !== false) {
344
+ next = next.createGraphics({
345
+ id: "colorGradientBackground",
346
+ type: "rect",
347
+ ...resolveLegendGraphicPlacement(next)
348
+ });
349
+ }
350
+ next = next
351
+ .createGraphics({
352
+ id: "colorGradientStrips",
353
+ type: "rect",
354
+ length: 0,
355
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
356
+ ? {}
357
+ : { after: "colorGradientBackground" })
358
+ })
359
+ .createGraphics({
360
+ id: "colorGradientTicks",
361
+ type: "line",
362
+ length: 0,
363
+ ...resolveLegendGraphicPlacement(next)
364
+ })
365
+ .createGraphics({
366
+ id: "colorGradientLabels",
367
+ type: "text",
368
+ length: 0,
369
+ ...resolveLegendGraphicPlacement(next)
370
+ });
371
+ if (resolved.config.titleVisible !== false) {
372
+ next = next.createGraphics({
373
+ id: "colorGradientTitle",
374
+ type: "text",
375
+ ...resolveLegendGraphicPlacement(next)
376
+ });
377
+ }
378
+ return next.rematerializeGradientLegend();
379
+ }
380
+
286
381
  export const createGradientLegend = action(
287
382
  {
288
383
  op: "createGradientLegend",
289
384
  description: "Create a continuous color gradient legend."
290
385
  },
291
386
  function (args = {}) {
292
- const config = normalizeContinuousLegend(args, "gradient");
293
- if (args.channels !== undefined && (
294
- !Array.isArray(args.channels) ||
295
- args.channels.length !== 1 ||
296
- args.channels[0] !== "color"
297
- )) {
298
- throw new Error('Gradient legend requires channels: ["color"].');
299
- }
300
- if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
301
- throw new TypeError("createLegend.gradient must be a plain object.");
302
- }
303
- validateKeys(
304
- args.gradient ?? {},
305
- GRADIENT_OPTIONS,
306
- "createLegend.gradient"
307
- );
308
- config.gradient = {
309
- length: args.gradient?.length ?? 120,
310
- thickness: args.gradient?.thickness ?? 12
311
- };
312
- config.titleVisible = true;
313
- validatePositive(config.gradient.length, "Gradient length");
314
- validatePositive(config.gradient.thickness, "Gradient thickness");
315
- const resolved = resolveGradientConfig(this, config);
387
+ const resolved = resolveGradientLegendCreation(this, args);
316
388
  resolveGradientLayout(this, resolved.config, resolved.scale);
317
389
  if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
318
390
  throw new Error(
319
391
  "createGradientLegend requires a missing gradient legend."
320
392
  );
321
393
  }
322
- let next = this
323
- .editSemantic({
324
- property: "guide.legend.color.scale",
325
- value: resolved.encoding.scale
326
- })
327
- .editSemantic({
328
- property: "guide.legend.color.title",
329
- value: resolved.config.title
330
- })
331
- ._withLegendConfig("gradient", resolved.config);
332
- if (resolved.config.border !== false) {
333
- next = next.createGraphics({
334
- id: "colorGradientBackground",
335
- type: "rect",
336
- ...resolveLegendGraphicPlacement(next)
337
- });
338
- }
339
- return next
340
- .createGraphics({
341
- id: "colorGradientStrips",
342
- type: "rect",
343
- length: 0,
344
- ...resolveLegendGraphicPlacement(next),
345
- ...(resolved.config.border === false
346
- ? {}
347
- : { after: "colorGradientBackground" })
348
- })
349
- .createGraphics({
350
- id: "colorGradientTicks",
351
- type: "line",
352
- length: 0,
353
- ...resolveLegendGraphicPlacement(next)
354
- })
355
- .createGraphics({
356
- id: "colorGradientLabels",
357
- type: "text",
358
- length: 0,
359
- ...resolveLegendGraphicPlacement(next)
360
- })
361
- .createGraphics({
362
- id: "colorGradientTitle",
363
- type: "text",
364
- ...resolveLegendGraphicPlacement(next)
365
- })
366
- .rematerializeGradientLegend();
394
+ return createGradientLegendFromConfig(this, resolved.config);
367
395
  }
368
396
  );
@@ -19,11 +19,15 @@ export function registerContinuousLegendActions(ProgramClass) {
19
19
  ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
20
20
  ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
21
21
  ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
22
- ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
23
- ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
22
+ registerIntervalLegendActions(ProgramClass);
24
23
  }
25
24
 
26
25
  export function registerGradientLegendActions(ProgramClass) {
27
26
  ProgramClass.prototype.createGradientLegend = createGradientLegend;
28
27
  ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
29
28
  }
29
+
30
+ export function registerIntervalLegendActions(ProgramClass) {
31
+ ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
32
+ ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
33
+ }