ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -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 +20 -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 +8 -7
  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/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,6 +1,12 @@
1
+ import{validateLegendClip}from"../../../../grammar/legendClip.js";
2
+ import{hasLegendClipGraphics}from"../../../../materialization/legendClip.js";
3
+ import { normalizeColorSymbols, resolveColorSymbolsLayout, materializeColorSymbols } from "./colorSymbols.js";
4
+ import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
5
+ import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
6
+ import { mapContinuousScaleValues, transformedTicks } from "../../../../grammar/scales/index.js";
1
7
  import { action, closedAction } from "../../../../core/action.js";
2
8
  import { isPlainObject } from "../../../../core/immutable.js";
3
- import { validateKeys } from "../../../../core/validation.js";
9
+ import { normalizeLegendGradient, DEFAULT_LEGEND_GRADIENT } from "../../../../grammar/legendGradient.js";
4
10
  import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
5
11
  import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
6
12
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
@@ -16,49 +22,61 @@ import {
16
22
  resolveLegendBackgroundFromBounds,
17
23
  resolveLegendTextBounds,
18
24
  sampleContinuousValues,
19
- styleContinuousText,
20
- validatePositive
25
+ styleContinuousText
21
26
  } from "./common.js";
22
27
  import { resolveLegendGraphicPlacement } from
23
28
  "../../../../materialization/graphicHierarchy.js";
24
29
  import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
25
30
 
26
- export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12 });
31
+ export const DEFAULT_GRADIENT_SIZE = DEFAULT_LEGEND_GRADIENT;
27
32
 
28
- const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
29
-
30
- export function resolveGradientLayout(program, config, scale, label = "Gradient legend") {
33
+ export function resolveGradientLayout(program, config, scale, label = "Gradient legend", stripStrokeWidth = 0) {
34
+ validateLegendClip("gradient",config);
35
+ if (config.representation === "symbols") return resolveColorSymbolsLayout(program, config, scale);
36
+ config = intrinsicLegendConfig(program, config);
31
37
  const { plot, canvas } = resolveContinuousBounds(program);
32
- const vertical = ["right", "left"].includes(config.position);
38
+ const vertical = config.gradientDirection === undefined
39
+ ? ["right", "left"].includes(config.position) : config.gradientDirection === "vertical";
40
+ const stripWidth = vertical ? config.gradient.thickness : config.gradient.length;
41
+ const stripHeight = vertical ? config.gradient.length : config.gradient.thickness;
33
42
  const length = config.gradient.length;
34
43
  const thickness = config.gradient.thickness;
44
+ const titleText = config.titleVisible === false ? { height: 0, extraHeight: 0 }
45
+ : resolveLegendTitleText(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
35
46
  const titleGap = config.blockGap ?? 12;
36
47
  let x;
37
48
  let y;
38
49
  if (config.position === "right") {
39
50
  x = plot.x + plot.width + config.offset;
40
- y = plot.y + 46 + titleGap - 12;
51
+ y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
41
52
  } else if (config.position === "left") {
42
- x = plot.x - config.offset - thickness;
43
- y = plot.y + 46 + titleGap - 12;
53
+ x = plot.x - config.offset - stripWidth;
54
+ y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
44
55
  } else {
45
56
  x = config.align === "left" ? plot.x
46
- : config.align === "right" ? plot.x + plot.width - length
47
- : plot.x + (plot.width - length) / 2;
57
+ : config.align === "right" ? plot.x + plot.width - stripWidth
58
+ : plot.x + (plot.width - stripWidth) / 2;
48
59
  y = config.position === "top"
49
- ? plot.y - config.offset - thickness - config.labels.offset -
60
+ ? plot.y - config.offset - stripHeight - config.labels.offset -
50
61
  config.labels.fontSize
51
62
  : plot.y + plot.height + config.offset +
52
- (config.titleVisible === false ? 0 : config.titleStyle.fontSize + titleGap);
63
+ (config.titleVisible === false ? 0 : titleText.height + titleGap);
53
64
  }
54
- const title = vertical
55
- ? { x, y: plot.y + 20, align: "left" }
65
+ let title = vertical
66
+ ? { x, y: ["right", "left"].includes(config.position) ? plot.y + 20
67
+ : y - titleGap - titleText.height / 2, align: "left" }
56
68
  : {
57
69
  x: x + length / 2,
58
- y: y - titleGap - config.titleStyle.fontSize / 2,
70
+ y: y - titleGap - titleText.height / 2,
59
71
  align: "center"
60
72
  };
61
- const values = [...sampleContinuousValues(scale.domain, config.count)];
73
+ const transformed = ["log", "pow", "sqrt", "symlog"].includes(scale.type);
74
+ const values = transformed
75
+ ? transformedTicks(scale.type, scale.domain, config.count,
76
+ scale.type === "log" ? {base: scale.base}
77
+ : scale.type === "pow" ? {exponent: scale.exponent}
78
+ : scale.type === "symlog" ? {constant: scale.constant} : {})
79
+ : [...sampleContinuousValues(scale.domain, config.count)];
62
80
  if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
63
81
  values.push(scale.midpoint);
64
82
  values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
@@ -69,17 +87,17 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
69
87
  config.fieldType,
70
88
  config.labels.format
71
89
  );
72
- const fractions = values.map((value, index) => scale.midpoint === undefined
90
+ const fractions = transformed ? mapContinuousScaleValues(values, {...scale, range: [0, 1]})
91
+ : values.map((value, index) => scale.midpoint === undefined
73
92
  ? index / (values.length - 1)
74
93
  : inverseLerp(value, ...scale.domain));
75
94
  const labelOffset = config.labels.offset;
76
95
  const labels = vertical
77
96
  ? fractions.map(fraction => ({
78
- x: config.position === "right"
79
- ? x + thickness + labelOffset
80
- : x - labelOffset,
97
+ x: config.position === "left"
98
+ ? x - labelOffset : x + thickness + labelOffset,
81
99
  y: y + length * (1 - fraction),
82
- align: config.position === "right" ? "left" : "right"
100
+ align: config.position === "left" ? "right" : "left"
83
101
  }))
84
102
  : fractions.map(fraction => ({
85
103
  x: x + length * fraction,
@@ -88,9 +106,9 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
88
106
  }));
89
107
  const ticks = vertical
90
108
  ? labels.map(label => ({
91
- x1: config.position === "right" ? x + thickness : x,
109
+ x1: config.position === "left" ? x : x + thickness,
92
110
  y1: label.y,
93
- x2: config.position === "right" ? x + thickness + 6 : x - 6,
111
+ x2: config.position === "left" ? x - 6 : x + thickness + 6,
94
112
  y2: label.y
95
113
  }))
96
114
  : labels.map(label => ({
@@ -114,11 +132,21 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
114
132
  const labelBounds = labels.map((label, index) =>
115
133
  resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
116
134
  );
135
+ if (config.titlePosition === "bottom" && config.titleVisible !== false) {
136
+ const bottom = Math.max(stripBounds.bottom + stripStrokeWidth / 2,
137
+ ...tickBounds.map(bounds => bounds.bottom), ...labelBounds.map(bounds => bounds.bottom));
138
+ title = { x: vertical ? x : x + length / 2,
139
+ y: bottom + titleGap + titleText.height / 2, align: vertical ? "left" : "center" };
140
+ }
141
+ if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
142
+ [{ left: stripBounds.left - stripStrokeWidth / 2, right: stripBounds.right + stripStrokeWidth / 2,
143
+ top: stripBounds.top - stripStrokeWidth / 2, bottom: stripBounds.bottom + stripStrokeWidth / 2 },
144
+ ...tickBounds, ...labelBounds], program.materializationConfigs.textMetrics);
117
145
  const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
118
146
  title,
119
147
  config.title,
120
148
  config.titleStyle
121
- , program.materializationConfigs.textMetrics);
149
+ , program.materializationConfigs.textMetrics, { hardBreaks: true });
122
150
  const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
123
151
  ...(config.titleVisible === false ? [] : [titleBounds])];
124
152
  assertLegendBoundsInsideCanvas(
@@ -173,14 +201,15 @@ export function materializeGradientLegend(program, resolved, appearance) {
173
201
  const prefix = `${channel}Gradient`;
174
202
  const { scale, encoding, config: currentConfig } = resolved;
175
203
  const config = resolveEffectiveLegendBlockConfig(program, kind, currentConfig);
204
+ if (!stroke && config.representation === "symbols") return materializeColorSymbols(program, resolved, config);
176
205
  const layout = resolveGradientLayout(program, config, scale,
177
- stroke ? "Stroke gradient legend" : "Gradient legend");
206
+ stroke ? "Stroke gradient legend" : "Gradient legend", appearance?.strokeWidth ?? 0);
178
207
  const stripCount = 60;
179
208
  const stripSize = layout.length / stripCount;
180
209
  const strips = Array.from({ length: stripCount }, (_, index) => {
181
210
  const fraction = (index + 0.5) / stripCount;
182
211
  const position = layout.vertical ? 1 - fraction : fraction;
183
- const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
212
+ const samplingScale = scale.midpoint === undefined ? { ...scale, type: "sequential", domain: [0, 1] } : scale;
184
213
  const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
185
214
  const [color] = mapScaleConsumerValues([value], samplingScale, channel);
186
215
  return {
@@ -204,6 +233,9 @@ export function materializeGradientLegend(program, resolved, appearance) {
204
233
  for (const property of ["x", "y", "width", "height", "fill", "stroke", "strokeWidth"]) {
205
234
  next = next.editGraphics({ target: `${prefix}Strips`, property, value: strips.map(strip => strip[property]) });
206
235
  }
236
+ if (config.gradient.opacity !== undefined) {
237
+ next = next.editGraphics({ target: `${prefix}Strips`, property: "opacity", value: config.gradient.opacity });
238
+ }
207
239
  next = next.editGraphics({ target: `${prefix}Ticks`, property: "length", value: layout.ticks.length });
208
240
  for (const property of ["x1", "y1", "x2", "y2"]) {
209
241
  next = next.editGraphics({ target: `${prefix}Ticks`, property, value: layout.ticks.map(tick => tick[property]) });
@@ -225,6 +257,7 @@ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
225
257
  description: "Rematerialize a continuous color gradient legend."
226
258
  }, [],
227
259
  function (args = {}) {
260
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
228
261
  const stored = this.guideConfigs.legend?.gradient;
229
262
  if (stored === undefined) {
230
263
  throw new Error("Gradient legend requires stored configuration.");
@@ -235,7 +268,10 @@ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
235
268
 
236
269
 
237
270
  export function resolveGradientLegendCreation(program, args = {}, channel = "color") {
238
- const config = normalizeContinuousLegend(args, "gradient");
271
+ const { representation, ...options } = args;
272
+ if (representation !== undefined && !["gradient", "symbols"].includes(representation)) throw new Error("Legend representation must be gradient or symbols.");
273
+ if (channel !== "color" && representation !== undefined) throw new Error("Legend representation requires continuous color.");
274
+ const config = representation === "symbols" ? normalizeColorSymbols(options) : normalizeContinuousLegend(options, "gradient");
239
275
  if (args.channels !== undefined && (
240
276
  !Array.isArray(args.channels) ||
241
277
  args.channels.length !== 1 ||
@@ -246,18 +282,7 @@ export function resolveGradientLegendCreation(program, args = {}, channel = "col
246
282
  if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
247
283
  throw new TypeError("createLegend.gradient must be a plain object.");
248
284
  }
249
- validateKeys(
250
- args.gradient ?? {},
251
- GRADIENT_OPTIONS,
252
- "createLegend.gradient"
253
- );
254
- config.gradient = {
255
- length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
256
- thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
257
- };
258
- config.titleVisible = true;
259
- validatePositive(config.gradient.length, "Gradient length");
260
- validatePositive(config.gradient.thickness, "Gradient thickness");
285
+ if (config.representation !== "symbols") config.gradient = normalizeLegendGradient(args.gradient);
261
286
  const resolved = resolveGradientConfig(program, config, channel);
262
287
  return resolved;
263
288
  }
@@ -284,7 +309,9 @@ export function createGradientLegendFromConfig(program, config, channel = "color
284
309
  ...resolveLegendGraphicPlacement(next)
285
310
  });
286
311
  }
287
- next = next
312
+ if (resolved.config.representation === "symbols") {
313
+ next = next.createGraphics({ id: `${prefix}Symbols`, type: "circle", length: 0, ...resolveLegendGraphicPlacement(next) });
314
+ } else next = next
288
315
  .createGraphics({
289
316
  id: `${prefix}Strips`,
290
317
  type: "rect",
@@ -298,8 +325,8 @@ export function createGradientLegendFromConfig(program, config, channel = "color
298
325
  type: "line",
299
326
  length: 0,
300
327
  ...resolveLegendGraphicPlacement(next)
301
- })
302
- .createGraphics({
328
+ });
329
+ next = next.createGraphics({
303
330
  id: `${prefix}Labels`,
304
331
  type: "text",
305
332
  length: 0,
@@ -323,7 +350,7 @@ export const createGradientLegend = /* @__PURE__ */ action(
323
350
  function (args = {}) {
324
351
  const resolved = resolveGradientLegendCreation(this, args);
325
352
  resolveGradientLayout(this, resolved.config, resolved.scale);
326
- if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
353
+ if (this.guideConfigs.legend?.gradient !== undefined) {
327
354
  throw new Error(
328
355
  "createGradientLegend requires a missing gradient legend."
329
356
  );
@@ -1,3 +1,4 @@
1
+ import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
1
2
  import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
2
3
  import { action, closedAction } from "../../../../core/action.js";
3
4
  import { isPlainObject } from "../../../../core/immutable.js";
@@ -13,6 +14,7 @@ import {
13
14
  assertLegendBoundsInsideCanvas,
14
15
  editGraphicProperties,
15
16
  editLegendBackground,
17
+ normalizeInitialLegendTitle,
16
18
  normalizeLegendBorder,
17
19
  normalizeItemLegendLayout,
18
20
  normalizeLegendTextOptions,
@@ -60,14 +62,11 @@ export function normalizeIntervalLegend(args) {
60
62
  validatePositive(symbol.width, "Legend symbol width");
61
63
  validatePositive(symbol.height, "Legend symbol height");
62
64
  validateNonNegative(symbol.strokeWidth, "Legend symbol strokeWidth");
63
- if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
64
65
  validateNonEmptyString(symbol.stroke, "Legend symbol stroke");
65
66
  return {
66
67
  target: args.target,
67
68
  ...layout,
68
- title: args.title,
69
- inferredTitle: args.title === undefined,
70
- titleVisible: true,
69
+ ...normalizeInitialLegendTitle(args.title),
71
70
  symbol,
72
71
  labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", {
73
72
  offset: 8,
@@ -106,6 +105,7 @@ export function resolveIntervalConfig(program, stored, channel = "color") {
106
105
  }
107
106
 
108
107
  export function resolveIntervalLayout(program, config, scale, label = "Interval legend") {
108
+ config = intrinsicLegendConfig(program, config);
109
109
  const { plot, canvas } = resolveContinuousBounds(program);
110
110
  const labels = formatDiscretizedIntervals(scale.thresholds, config.labels.format);
111
111
  const layout = resolveLegendItemLayout(plot, config, labels, config.symbol, undefined, program.materializationConfigs.textMetrics);
@@ -1,3 +1,7 @@
1
+ import{validateLegendClip}from"../../../../grammar/legendClip.js";
2
+ import{hasLegendClipGraphics,applyLegendClipGraphics}from"../../../../materialization/legendClip.js";
3
+ import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
4
+ import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
1
5
  import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
2
6
  import { action, closedAction } from "../../../../core/action.js";
3
7
  import { isPlainObject } from "../../../../core/immutable.js";
@@ -23,6 +27,7 @@ import { resolveLegendGraphicPlacement } from
23
27
  "../../../../materialization/graphicHierarchy.js";
24
28
  import { resolveLegendSampleValues } from "../sampling.js";
25
29
  import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
30
+ import { legendLabelLineHeight } from "../../../../layout/legend.js";
26
31
 
27
32
  const SYMBOL_OPTIONS = Object.freeze([
28
33
  "type", "radius", "fill", "stroke", "strokeWidth"
@@ -85,6 +90,8 @@ function resolveOpacityConfig(program, config) {
85
90
  }
86
91
 
87
92
  function resolveOpacityLayout(program, config, scale) {
93
+ validateLegendClip("opacity",config);
94
+ config = intrinsicLegendConfig(program, config);
88
95
  const { plot, canvas } = resolveContinuousBounds(program);
89
96
  const vertical = ["right", "left"].includes(config.position);
90
97
  const values = resolveLegendSampleValues(config, scale, "Opacity legend");
@@ -95,6 +102,9 @@ function resolveOpacityLayout(program, config, scale) {
95
102
  config.labels.format
96
103
  );
97
104
  const symbolExtent = config.symbol.radius + (config.symbol.strokeWidth ?? 0) / 2;
105
+ const visibleExtent=Math.min(symbolExtent,config.clipHeight===undefined?Infinity:config.clipHeight/2);
106
+ const titleText = config.titleVisible === false ? { width: 0, height: 0 }
107
+ : resolveLegendTitleText(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
98
108
  const labelWidths = texts.map(text => measureTextWidth(text, config.labels, program.materializationConfigs.textMetrics));
99
109
  let symbols;
100
110
  let labels;
@@ -103,10 +113,10 @@ function resolveOpacityLayout(program, config, scale) {
103
113
  const baseX = config.position === "right"
104
114
  ? plot.x + plot.width + config.offset
105
115
  : plot.x - config.offset;
106
- const itemHeight = Math.max(symbolExtent * 2, config.labels.fontSize);
116
+ const itemHeight = Math.max(visibleExtent * 2, legendLabelLineHeight(config.labels.fontSize));
107
117
  const pitch = Math.max(config.itemGap, itemHeight);
108
118
  const firstY = Math.max(plot.y + 46, config.titleVisible === false ? plot.y + 46
109
- : plot.y + 20 + config.titleStyle.fontSize / 2 + 12 + itemHeight / 2);
119
+ : plot.y + 20 + titleText.height / 2 + 12 + itemHeight / 2);
110
120
  const y = values.map((_, index) => firstY + index * pitch);
111
121
  symbols = y.map(itemY => ({
112
122
  x: config.position === "right" ? baseX + symbolExtent : baseX - symbolExtent,
@@ -125,7 +135,7 @@ function resolveOpacityLayout(program, config, scale) {
125
135
  align: config.position === "right" ? "left" : "right"
126
136
  };
127
137
  } else if (config.titlePosition === "left") {
128
- const titleWidth = config.titleVisible === false ? 0 : measureTextWidth(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
138
+ const titleWidth = titleText.width;
129
139
  const titlePrefix = config.titleVisible === false ? 0 : titleWidth + 20;
130
140
  const samplesWidth = labelWidths.reduce(
131
141
  (sum, width) => sum + symbolExtent * 2 + config.labels.offset + width,
@@ -136,9 +146,9 @@ function resolveOpacityLayout(program, config, scale) {
136
146
  : config.align === "right" ? plot.x + plot.width - width
137
147
  : plot.x + (plot.width - width) / 2;
138
148
  const height = Math.max(
139
- symbolExtent * 2,
149
+ visibleExtent * 2,
140
150
  config.labels.fontSize,
141
- config.titleVisible === false ? 0 : config.titleStyle.fontSize
151
+ titleText.height
142
152
  );
143
153
  const centerY = config.position === "top"
144
154
  ? plot.y - config.offset - height / 2
@@ -161,13 +171,13 @@ function resolveOpacityLayout(program, config, scale) {
161
171
  const startX = config.align === "left" ? plot.x
162
172
  : config.align === "right" ? plot.x + plot.width - width
163
173
  : plot.x + (plot.width - width) / 2;
164
- const titleHalf = config.titleStyle.fontSize / 2;
174
+ const titleHalf = titleText.height / 2;
165
175
  const labelHalf = config.labels.fontSize / 2;
166
176
  const y = config.position === "top"
167
177
  ? plot.y - config.offset - config.labels.fontSize -
168
- config.labels.offset - symbolExtent
178
+ config.labels.offset - visibleExtent
169
179
  : plot.y + plot.height + config.offset +
170
- (config.titleVisible === false ? 0 : config.titleStyle.fontSize + 12) + symbolExtent;
180
+ (config.titleVisible === false ? 0 : titleText.height + 12) + visibleExtent;
171
181
  const samplePitch = values.length === 1 ? 0 : width / (values.length - 1);
172
182
  symbols = values.map((_, index) => ({
173
183
  x: startX + index * samplePitch,
@@ -175,29 +185,31 @@ function resolveOpacityLayout(program, config, scale) {
175
185
  }));
176
186
  labels = symbols.map(symbol => ({
177
187
  x: symbol.x,
178
- y: symbol.y + symbolExtent + config.labels.offset + labelHalf,
188
+ y: symbol.y + visibleExtent + config.labels.offset + labelHalf,
179
189
  align: "center"
180
190
  }));
181
191
  title = {
182
192
  x: startX + width / 2,
183
- y: y - symbolExtent - 12 - titleHalf,
193
+ y: y - visibleExtent - 12 - titleHalf,
184
194
  align: "center"
185
195
  };
186
196
  }
187
- const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
188
- title,
189
- config.title,
190
- config.titleStyle
191
- , program.materializationConfigs.textMetrics);
192
197
  const labelBounds = labels.map((label, index) =>
193
198
  resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
194
199
  );
195
200
  const symbolBounds = symbols.map(symbol => ({
196
201
  left: symbol.x - symbolExtent,
197
202
  right: symbol.x + symbolExtent,
198
- top: symbol.y - symbolExtent,
199
- bottom: symbol.y + symbolExtent
203
+ top: symbol.y - visibleExtent,
204
+ bottom: symbol.y + visibleExtent
200
205
  }));
206
+ if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
207
+ [...labelBounds, ...symbolBounds], program.materializationConfigs.textMetrics);
208
+ const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
209
+ title,
210
+ config.title,
211
+ config.titleStyle
212
+ , program.materializationConfigs.textMetrics, { hardBreaks: true });
201
213
  const occupiedBounds = [...(config.titleVisible === false ? [] : [titleBounds]), ...labelBounds, ...symbolBounds];
202
214
  assertLegendBoundsInsideCanvas(
203
215
  occupiedBounds,
@@ -219,6 +231,7 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ closedAction(
219
231
  description: "Rematerialize a field-opacity sample legend."
220
232
  }, [],
221
233
  function (args = {}) {
234
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
222
235
  const stored = this.guideConfigs.legend?.opacity;
223
236
  if (stored === undefined) {
224
237
  throw new Error("Opacity legend requires stored configuration.");
@@ -278,18 +291,18 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ closedAction(
278
291
  config.labels,
279
292
  { align: layout.labels[0].align }
280
293
  );
281
- if (config.titleVisible === false) return next;
294
+ if (config.titleVisible === false) return applyLegendClipGraphics(next);
282
295
  next = editGraphicProperties(next, "opacityLegendTitle", {
283
296
  x: layout.title.x,
284
297
  y: layout.title.y,
285
298
  text: config.title
286
299
  });
287
- return styleContinuousText(
300
+ return applyLegendClipGraphics(styleContinuousText(
288
301
  next,
289
302
  "opacityLegendTitle",
290
303
  config.titleStyle,
291
304
  { align: layout.title.align }
292
- );
305
+ ));
293
306
  }
294
307
  );
295
308
 
@@ -304,7 +317,6 @@ export function resolveOpacityLegendCreation(program, args = {}) {
304
317
  throw new Error('Opacity legend requires channels: ["opacity"].');
305
318
  }
306
319
  config.symbol = normalizeOpacitySymbol(args.symbol);
307
- config.titleVisible = true;
308
320
  const resolved = resolveOpacityConfig(program, config);
309
321
  return resolved;
310
322
  }
@@ -1,3 +1,4 @@
1
+ import {resolveColorSizeConfig} from "./colorSize.js";
1
2
  import { resolveCategoricalLegendConfig } from "./categorical/actions.js";
2
3
  import { resolveGradientLegendCreation } from "./continuous/gradient.js";
3
4
  import { resolveOpacityLegendCreation } from "./continuous/opacity.js";
@@ -11,6 +12,7 @@ import {
11
12
 
12
13
  export function resolveLegendStepConfig(program, step) {
13
14
  switch (step.op) {
15
+ case "createColorSizeLegend": return {kind:"colorSize",config:resolveColorSizeConfig(program,step.args)};
14
16
  case "createCategoricalLegend": {
15
17
  const config = resolveCategoricalLegendConfig(program, step.args);
16
18
  return { kind: config.kind, config };