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
@@ -8,9 +8,15 @@ import { formatDiscretizedIntervals } from "../../../../grammar/scales/index.js"
8
8
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
9
9
  "../../../../theme/defaults.js";
10
10
  import {
11
- assertLegendInsideCanvas,
11
+ assertLegendBoundsInsideCanvas,
12
+ editGraphicProperties,
13
+ editLegendBackground,
14
+ normalizeLegendBorder,
15
+ normalizeLegendTextOptions,
12
16
  resolveContinuousBounds,
13
17
  resolveContinuousPoint,
18
+ resolveLegendBackgroundFromBounds,
19
+ resolveLegendTextBounds,
14
20
  styleContinuousText,
15
21
  validateNonNegative,
16
22
  validatePositive
@@ -18,27 +24,13 @@ import {
18
24
  import { resolveLegendGraphicPlacement } from
19
25
  "../../../../materialization/graphicHierarchy.js";
20
26
 
21
- const OPTIONS = Object.freeze([
27
+ const OPTIONS = [
22
28
  "target", "channels", "position", "align", "offset", "title",
23
29
  "symbol", "labels", "titleStyle", "itemGap", "direction", "border"
24
- ]);
25
- const SYMBOL_OPTIONS = Object.freeze([
30
+ ];
31
+ const SYMBOL_OPTIONS = [
26
32
  "width", "height", "stroke", "strokeWidth"
27
- ]);
28
- const TEXT_OPTIONS = Object.freeze([
29
- "offset", "color", "fontSize", "fontFamily", "fontWeight"
30
- ]);
31
-
32
- function textOptions(value, label, defaults) {
33
- if (value !== undefined && !isPlainObject(value)) {
34
- throw new TypeError(`${label} must be a plain object.`);
35
- }
36
- validateKeys(value ?? {}, TEXT_OPTIONS, label);
37
- const result = { ...defaults, ...(value ?? {}) };
38
- if (result.offset !== undefined) validateNonNegative(result.offset, `${label} offset`);
39
- validatePositive(result.fontSize, `${label} fontSize`);
40
- return result;
41
- }
33
+ ];
42
34
 
43
35
  export function normalizeIntervalLegend(args) {
44
36
  validateOptionObject(args, OPTIONS, "createLegend");
@@ -55,18 +47,16 @@ export function normalizeIntervalLegend(args) {
55
47
  if ((args.direction ?? "vertical") !== "vertical") {
56
48
  throw new Error('Interval legends currently support direction "vertical".');
57
49
  }
58
- if (args.border !== undefined && args.border !== false) {
59
- throw new Error("Interval legend border is not implemented yet.");
60
- }
61
50
  if (args.symbol !== undefined && !isPlainObject(args.symbol)) {
62
51
  throw new TypeError("createLegend.symbol must be a plain object.");
63
52
  }
64
- validateKeys(args.symbol ?? {}, SYMBOL_OPTIONS, "createLegend.symbol");
53
+ const source = args.symbol ?? {};
54
+ validateKeys(source, SYMBOL_OPTIONS, "createLegend.symbol");
65
55
  const symbol = {
66
- width: args.symbol?.width ?? 14,
67
- height: args.symbol?.height ?? 12,
68
- stroke: args.symbol?.stroke ?? "white",
69
- strokeWidth: args.symbol?.strokeWidth ?? 0.5
56
+ width: source.width ?? 14,
57
+ height: source.height ?? 12,
58
+ stroke: source.stroke ?? "white",
59
+ strokeWidth: source.strokeWidth ?? 0.5
70
60
  };
71
61
  validatePositive(symbol.width, "Legend symbol width");
72
62
  validatePositive(symbol.height, "Legend symbol height");
@@ -86,20 +76,20 @@ export function normalizeIntervalLegend(args) {
86
76
  inferredTitle: args.title === undefined,
87
77
  titleVisible: true,
88
78
  symbol,
89
- labels: textOptions(args.labels, "createLegend.labels", {
79
+ labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", {
90
80
  offset: 8,
91
81
  color: DEFAULT_COLORS.text,
92
82
  fontSize: 12,
93
83
  fontFamily: DEFAULT_FONT_FAMILY,
94
84
  fontWeight: "normal"
95
85
  }),
96
- titleStyle: textOptions(args.titleStyle, "createLegend.titleStyle", {
86
+ titleStyle: normalizeLegendTextOptions(args.titleStyle, "createLegend.titleStyle", {
97
87
  color: DEFAULT_COLORS.text,
98
88
  fontSize: 13,
99
89
  fontFamily: DEFAULT_FONT_FAMILY,
100
90
  fontWeight: 600
101
91
  }),
102
- border: false
92
+ border: normalizeLegendBorder(args.border)
103
93
  };
104
94
  }
105
95
 
@@ -113,17 +103,12 @@ function resolveIntervalConfig(program, stored) {
113
103
  if (!["quantize", "quantile", "threshold"].includes(scale?.type)) {
114
104
  throw new Error(`Interval legend requires a resolved discretized scale "${encoding.scale}".`);
115
105
  }
116
- return {
117
- layer,
118
- encoding,
119
- scale,
120
- config: {
106
+ return { encoding, scale, config: {
121
107
  ...stored,
122
108
  target: layer.id,
123
109
  scale: encoding.scale,
124
110
  title: stored.inferredTitle ? encoding.field : stored.title
125
- }
126
- };
111
+ } };
127
112
  }
128
113
 
129
114
  function resolveIntervalLayout(program, config, scale) {
@@ -133,14 +118,37 @@ function resolveIntervalLayout(program, config, scale) {
133
118
  const itemY = labels.map((_, index) => plot.y + 52 + index * config.itemGap);
134
119
  const labelX = symbolX + config.symbol.width + config.labels.offset;
135
120
  const title = { x: symbolX, y: plot.y + 20 };
136
- assertLegendInsideCanvas([
137
- title,
138
- ...itemY.flatMap(y => [
139
- { x: symbolX, y },
140
- { x: labelX, y }
141
- ])
142
- ], canvas, "Interval legend layout");
143
- return { labels, symbolX, labelX, itemY, title };
121
+ const strokeExtent = config.symbol.strokeWidth / 2;
122
+ const occupiedBounds = [
123
+ resolveLegendTextBounds(
124
+ { ...title, align: "left" },
125
+ config.title,
126
+ config.titleStyle
127
+ ),
128
+ ...labels.map((label, index) => resolveLegendTextBounds(
129
+ { x: labelX, y: itemY[index], align: "left" },
130
+ label,
131
+ config.labels
132
+ )),
133
+ ...itemY.map(y => ({
134
+ left: symbolX - strokeExtent,
135
+ right: symbolX + config.symbol.width + strokeExtent,
136
+ top: y - config.symbol.height / 2 - strokeExtent,
137
+ bottom: y + config.symbol.height / 2 + strokeExtent
138
+ }))
139
+ ];
140
+ assertLegendBoundsInsideCanvas(
141
+ occupiedBounds,
142
+ canvas,
143
+ "Interval legend layout"
144
+ );
145
+ const background = resolveLegendBackgroundFromBounds(
146
+ occupiedBounds,
147
+ config.border,
148
+ canvas,
149
+ "Interval legend"
150
+ );
151
+ return { labels, symbolX, labelX, itemY, title, background };
144
152
  }
145
153
 
146
154
  export const rematerializeIntervalLegend = action(
@@ -156,36 +164,38 @@ export const rematerializeIntervalLegend = action(
156
164
  }
157
165
  const { encoding, scale, config } = resolveIntervalConfig(this, stored);
158
166
  const layout = resolveIntervalLayout(this, config, scale);
159
- let next = this
167
+ let next = editGraphicProperties(this
160
168
  .editSemantic({ property: "guide.legend.color.scale", value: encoding.scale })
161
169
  .editSemantic({ property: "guide.legend.color.title", value: config.title })
162
- ._withLegendConfig("interval", config)
163
- .editGraphics({
164
- target: "colorLegendSymbols",
165
- property: "length",
166
- value: scale.range.length
167
- })
168
- .editGraphics({ target: "colorLegendSymbols", property: "x", value: layout.symbolX })
169
- .editGraphics({
170
- target: "colorLegendSymbols",
171
- property: "y",
172
- value: layout.itemY.map(value => value - config.symbol.height / 2)
173
- })
174
- .editGraphics({ target: "colorLegendSymbols", property: "width", value: config.symbol.width })
175
- .editGraphics({ target: "colorLegendSymbols", property: "height", value: config.symbol.height })
176
- .editGraphics({ target: "colorLegendSymbols", property: "fill", value: scale.range })
177
- .editGraphics({ target: "colorLegendSymbols", property: "stroke", value: config.symbol.stroke })
178
- .editGraphics({ target: "colorLegendSymbols", property: "strokeWidth", value: config.symbol.strokeWidth })
179
- .editGraphics({ target: "colorLegendLabels", property: "length", value: layout.labels.length })
180
- .editGraphics({ target: "colorLegendLabels", property: "x", value: layout.labelX })
181
- .editGraphics({ target: "colorLegendLabels", property: "y", value: layout.itemY })
182
- .editGraphics({ target: "colorLegendLabels", property: "text", value: layout.labels });
170
+ ._withLegendConfig("interval", config), "colorLegendSymbols", {
171
+ length: scale.range.length,
172
+ x: layout.symbolX,
173
+ y: layout.itemY.map(value => value - config.symbol.height / 2),
174
+ width: config.symbol.width,
175
+ height: config.symbol.height,
176
+ fill: scale.range,
177
+ stroke: config.symbol.stroke,
178
+ strokeWidth: config.symbol.strokeWidth
179
+ });
180
+ next = editGraphicProperties(next, "colorLegendLabels", {
181
+ length: layout.labels.length,
182
+ x: layout.labelX,
183
+ y: layout.itemY,
184
+ text: layout.labels
185
+ });
186
+ next = editLegendBackground(
187
+ next,
188
+ "colorLegendBackground",
189
+ layout.background,
190
+ config.border
191
+ );
183
192
  next = styleContinuousText(next, "colorLegendLabels", config.labels);
184
193
  if (config.titleVisible === false) return next;
185
- next = next
186
- .editGraphics({ target: "colorLegendTitle", property: "x", value: layout.title.x })
187
- .editGraphics({ target: "colorLegendTitle", property: "y", value: layout.title.y })
188
- .editGraphics({ target: "colorLegendTitle", property: "text", value: config.title });
194
+ next = editGraphicProperties(next, "colorLegendTitle", {
195
+ x: layout.title.x,
196
+ y: layout.title.y,
197
+ text: config.title
198
+ });
189
199
  return styleContinuousText(next, "colorLegendTitle", config.titleStyle);
190
200
  }
191
201
  );
@@ -202,7 +212,7 @@ export const createIntervalLegend = action(
202
212
  if (this.graphicSpec.objects.colorLegendSymbols !== undefined) {
203
213
  throw new Error("createIntervalLegend requires a missing interval legend.");
204
214
  }
205
- return this
215
+ let next = this
206
216
  .editSemantic({
207
217
  property: "guide.legend.color.scale",
208
218
  value: resolved.encoding.scale
@@ -211,23 +221,34 @@ export const createIntervalLegend = action(
211
221
  property: "guide.legend.color.title",
212
222
  value: resolved.config.title
213
223
  })
214
- ._withLegendConfig("interval", resolved.config)
224
+ ._withLegendConfig("interval", resolved.config);
225
+ const placement = resolveLegendGraphicPlacement(next);
226
+ if (resolved.config.border !== false) {
227
+ next = next.createGraphics({
228
+ id: "colorLegendBackground",
229
+ type: "rect",
230
+ ...placement
231
+ });
232
+ }
233
+ return next
215
234
  .createGraphics({
216
235
  id: "colorLegendSymbols",
217
236
  type: "rect",
218
237
  length: 0,
219
- ...resolveLegendGraphicPlacement(this)
238
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
239
+ ? {}
240
+ : { after: "colorLegendBackground" })
220
241
  })
221
242
  .createGraphics({
222
243
  id: "colorLegendLabels",
223
244
  type: "text",
224
245
  length: 0,
225
- ...resolveLegendGraphicPlacement(this)
246
+ ...placement
226
247
  })
227
248
  .createGraphics({
228
249
  id: "colorLegendTitle",
229
250
  type: "text",
230
- ...resolveLegendGraphicPlacement(this)
251
+ ...placement
231
252
  })
232
253
  .rematerializeIntervalLegend();
233
254
  }
@@ -5,14 +5,15 @@ import { mapLinearValues } from "../../../../grammar/scales/index.js";
5
5
  import { measureTextWidth } from "../../../../core/textMetrics.js";
6
6
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
7
7
  import {
8
- assertLegendInsideCanvas,
8
+ assertLegendBoundsInsideCanvas,
9
9
  editLegendBackground,
10
10
  formatContinuousValues,
11
11
  normalizeContinuousLegend,
12
12
  requireResolvedLegendScale,
13
13
  resolveContinuousBounds,
14
14
  resolveContinuousPoint,
15
- resolveLegendBackgroundBounds,
15
+ resolveLegendBackgroundFromBounds,
16
+ resolveLegendTextBounds,
16
17
  sampleContinuousValues,
17
18
  styleContinuousText,
18
19
  validateNonNegative,
@@ -170,45 +171,34 @@ function resolveOpacityLayout(program, config, scale) {
170
171
  align: "center"
171
172
  };
172
173
  }
173
- const titleWidth = measureTextWidth(config.title, config.titleStyle);
174
- const titleEdge = {
175
- x: title.x + (title.align === "left" ? titleWidth : -titleWidth),
176
- y: title.y
177
- };
178
- const labelEdges = labels.map((label, index) => {
179
- const width = measureTextWidth(texts[index], config.labels);
180
- return {
181
- x: label.x + (label.align === "left"
182
- ? width
183
- : label.align === "right" ? -width : width / 2),
184
- y: label.y
185
- };
186
- });
187
- assertLegendInsideCanvas(
188
- [title, titleEdge, ...symbols, ...labels, ...labelEdges],
174
+ const strokeExtent = (config.symbol.strokeWidth ?? 0) / 2;
175
+ const symbolExtent = radius + strokeExtent;
176
+ const titleBounds = resolveLegendTextBounds(
177
+ title,
178
+ config.title,
179
+ config.titleStyle
180
+ );
181
+ const labelBounds = labels.map((label, index) =>
182
+ resolveLegendTextBounds(label, texts[index], config.labels)
183
+ );
184
+ const symbolBounds = symbols.map(symbol => ({
185
+ left: symbol.x - symbolExtent,
186
+ right: symbol.x + symbolExtent,
187
+ top: symbol.y - symbolExtent,
188
+ bottom: symbol.y + symbolExtent
189
+ }));
190
+ const occupiedBounds = [titleBounds, ...labelBounds, ...symbolBounds];
191
+ assertLegendBoundsInsideCanvas(
192
+ occupiedBounds,
189
193
  canvas,
190
194
  "Opacity legend layout"
191
195
  );
192
- const background = resolveLegendBackgroundBounds([
193
- title,
194
- ...symbols.map(symbol => ({
195
- x: symbol.x + radius,
196
- y: symbol.y + radius
197
- })),
198
- ...symbols.map(symbol => ({
199
- x: symbol.x - radius,
200
- y: symbol.y - radius
201
- })),
202
- ...labels.map((label, index) => ({
203
- x: label.x + (label.align === "left"
204
- ? measureTextWidth(
205
- texts[index],
206
- config.labels
207
- )
208
- : label.align === "right" ? -42 : 0),
209
- y: label.y
210
- }))
211
- ], config.border, canvas, "Opacity legend");
196
+ const background = resolveLegendBackgroundFromBounds(
197
+ occupiedBounds,
198
+ config.border,
199
+ canvas,
200
+ "Opacity legend"
201
+ );
212
202
  return { values, texts, symbols, labels, title, background };
213
203
  }
214
204
 
@@ -1,5 +1,8 @@
1
1
  import { action } from "../../../core/action.js";
2
- import { validateOptionObject } from "../../../core/validation.js";
2
+ import {
3
+ validateGeneratedItemLimit,
4
+ validateOptionObject
5
+ } from "../../../core/validation.js";
3
6
  import { normalizeOptions } from "./categorical/options.js";
4
7
  import { symbolGraphic } from "./categorical/layout.js";
5
8
  import {
@@ -170,6 +173,12 @@ function editInterval(program, previous, args) {
170
173
  next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
171
174
  type: "text"
172
175
  });
176
+ next = reconcileGraphic(
177
+ next,
178
+ "colorLegendBackground",
179
+ normalized.border !== false,
180
+ { type: "rect", before: "colorLegendSymbols" }
181
+ );
173
182
  return next.rematerializeLegend();
174
183
  }
175
184
 
@@ -186,6 +195,7 @@ function editStrokeWidth(program, previous, args) {
186
195
  "Stroke-width legend count must be an integer of at least 2."
187
196
  );
188
197
  }
198
+ validateGeneratedItemLimit(count, "Stroke-width legend count");
189
199
  const layer = findLayer(program, previous.target);
190
200
  const titleMode = args.title;
191
201
  const inferredTitle = titleMode === "auto"
@@ -310,6 +320,9 @@ function editCategorical(program, kind, previous, size, args) {
310
320
  if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 2)) {
311
321
  throw new RangeError("Size legend count must be an integer of at least 2.");
312
322
  }
323
+ if (args.count !== undefined) {
324
+ validateGeneratedItemLimit(args.count, "Size legend count");
325
+ }
313
326
  next = next._withLegendConfig("size", {
314
327
  ...size,
315
328
  count: args.count ?? size.count,
@@ -1,5 +1,5 @@
1
- import { registerCategoricalLegendActions } from "./categorical/index.js";
2
1
  import { registerContinuousLegendActions } from "./continuous/index.js";
2
+ import { registerCategoricalLegendActions } from "./categorical/index.js";
3
3
  import { registerSizeLegendActions } from "./size.js";
4
4
  import { registerStrokeWidthLegendActions } from "./strokeWidth.js";
5
5
  import { registerFocusedLegendActions } from "./focused.js";
@@ -4,6 +4,7 @@ import {
4
4
  resolveConcreteGraphicBounds,
5
5
  unionConcreteGraphicBounds
6
6
  } from "../../../grammar/schemas/graphicBounds.js";
7
+ import { finiteMidpoint } from "../../../grammar/numeric.js";
7
8
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
8
9
  import {
9
10
  resolveHorizontalLegendLane,
@@ -120,7 +121,7 @@ function blockDescriptor(program, kind, config) {
120
121
  bounds,
121
122
  title,
122
123
  symbol: {
123
- centerX: (symbolAnchor.left + symbolAnchor.right) / 2,
124
+ centerX: finiteMidpoint(symbolAnchor.left, symbolAnchor.right),
124
125
  left: symbolBounds.left,
125
126
  right: symbolBounds.right,
126
127
  bounds: symbolAnchor
@@ -99,6 +99,14 @@ export function removeLegendKinds(program, kinds) {
99
99
  return next;
100
100
  }
101
101
 
102
+ export function removeOwnedColorLegends(program, target) {
103
+ const kinds = Object.entries(program.guideConfigs.legend ?? {})
104
+ .filter(([kind, config]) => config?.target === target && (
105
+ config.channels?.includes("color") || ["gradient", "interval"].includes(kind)
106
+ )).map(([kind]) => kind);
107
+ return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
108
+ }
109
+
102
110
  export const removeLegend = action(
103
111
  { op: "removeLegend", description: "Remove every legend block owned by one mark." },
104
112
  function (args = {}) {
@@ -1,5 +1,8 @@
1
1
  import { action } from "../../../core/action.js";
2
- import { validateKeys } from "../../../core/validation.js";
2
+ import {
3
+ validateGeneratedItemLimit,
4
+ validateKeys
5
+ } from "../../../core/validation.js";
3
6
  import { mapLinearValues } from "../../../grammar/scales/index.js";
4
7
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
5
8
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
@@ -9,6 +12,12 @@ import { resolveLayout as resolveCategoricalLayout } from
9
12
  import { findLayer } from "../../../selectors/layers.js";
10
13
  import { findCanvasGraphic, resolveLegendGraphicPlacement } from
11
14
  "../../../materialization/graphicHierarchy.js";
15
+ import {
16
+ formatContinuousValues,
17
+ sampleContinuousValues,
18
+ selectLegendLayer,
19
+ styleContinuousText
20
+ } from "./continuous/common.js";
12
21
 
13
22
  const SIZE_OPTIONS = Object.freeze(["target", "count"]);
14
23
 
@@ -18,13 +27,7 @@ export function isSizeLegendPoint(layer) {
18
27
  }
19
28
 
20
29
  export function resolveSizeLegendPoint(program, requested) {
21
- const candidates = program.semanticSpec.layers.filter(isSizeLegendPoint);
22
- const layer = requested === undefined
23
- ? candidates.length === 1 ? candidates[0] : undefined
24
- : (() => {
25
- const candidate = findLayer(program, requested);
26
- return candidates.includes(candidate) ? candidate : undefined;
27
- })();
30
+ const layer = selectLegendLayer(program, requested, isSizeLegendPoint);
28
31
  if (layer === undefined) {
29
32
  throw new Error(
30
33
  requested === undefined
@@ -56,26 +59,6 @@ function bounds(program) {
56
59
  return value;
57
60
  }
58
61
 
59
- function styleText(program, id, { title = false, style } = {}) {
60
- const resolved = style ?? {
61
- color: title ? DEFAULT_COLORS.strongText : DEFAULT_COLORS.text,
62
- fontSize: title ? 13 : 12,
63
- fontFamily: DEFAULT_FONT_FAMILY,
64
- fontWeight: title ? 600 : "normal"
65
- };
66
- return program
67
- .editGraphics({
68
- target: id,
69
- property: "fill",
70
- value: resolved.color
71
- })
72
- .editGraphics({ target: id, property: "fontSize", value: resolved.fontSize })
73
- .editGraphics({ target: id, property: "fontFamily", value: resolved.fontFamily })
74
- .editGraphics({ target: id, property: "fontWeight", value: resolved.fontWeight })
75
- .editGraphics({ target: id, property: "textAlign", value: "left" })
76
- .editGraphics({ target: id, property: "textBaseline", value: "middle" });
77
- }
78
-
79
62
  export const rematerializeSizeLegend = action(
80
63
  {
81
64
  op: "rematerializeSizeLegend",
@@ -107,11 +90,7 @@ export const rematerializeSizeLegend = action(
107
90
  const originX = leftLayout?.titleX ?? plot.x + plot.width + 30;
108
91
  const seriesCount = categorical?.domain.length ?? 0;
109
92
  const titleY = leftLayout?.titleY ?? plot.y + 56 + seriesCount * 34 + 22;
110
- const values = Array.from(
111
- { length: currentConfig.count },
112
- (_, index) => scale.domain[0] +
113
- index / (currentConfig.count - 1) * (scale.domain[1] - scale.domain[0])
114
- );
93
+ const values = sampleContinuousValues(scale.domain, currentConfig.count);
115
94
  const areas = mapLinearValues(values, scale.domain, scale.range, {
116
95
  clamp: scale.clamp ?? false
117
96
  });
@@ -143,22 +122,35 @@ export const rematerializeSizeLegend = action(
143
122
  .editGraphics({
144
123
  target: "sizeLegendLabels",
145
124
  property: "text",
146
- value: values.map(value => String(+value.toPrecision(3)))
125
+ value: formatContinuousValues(values, scale.domain, "quantitative")
147
126
  })
148
127
  .editGraphics({ target: "sizeLegendTitle", property: "x", value: originX })
149
128
  .editGraphics({ target: "sizeLegendTitle", property: "y", value: titleY })
150
129
  .editGraphics({ target: "sizeLegendTitle", property: "text", value: title });
151
- next = styleText(next, "sizeLegendLabels", {
152
- style: currentConfig.inheritAppearance === true
130
+ next = styleContinuousText(
131
+ next,
132
+ "sizeLegendLabels",
133
+ currentConfig.inheritAppearance === true
153
134
  ? categorical.labels
154
- : undefined
155
- });
156
- return styleText(next, "sizeLegendTitle", {
157
- title: true,
158
- style: currentConfig.inheritAppearance === true
135
+ : {
136
+ color: DEFAULT_COLORS.text,
137
+ fontSize: 12,
138
+ fontFamily: DEFAULT_FONT_FAMILY,
139
+ fontWeight: "normal"
140
+ }
141
+ );
142
+ return styleContinuousText(
143
+ next,
144
+ "sizeLegendTitle",
145
+ currentConfig.inheritAppearance === true
159
146
  ? categorical.titleStyle
160
- : undefined
161
- });
147
+ : {
148
+ color: DEFAULT_COLORS.strongText,
149
+ fontSize: 13,
150
+ fontFamily: DEFAULT_FONT_FAMILY,
151
+ fontWeight: 600
152
+ }
153
+ );
162
154
  }
163
155
  );
164
156
 
@@ -179,6 +171,7 @@ export const createSizeLegend = action(
179
171
  if (!Number.isInteger(count) || count < 2) {
180
172
  throw new RangeError("Size legend count must be an integer of at least 2.");
181
173
  }
174
+ validateGeneratedItemLimit(count, "Size legend count");
182
175
  return this
183
176
  .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
184
177
  .editSemantic({ property: "guide.legend.size.title", value: encoding.field })
@@ -1,5 +1,8 @@
1
1
  import { action } from "../../../core/action.js";
2
- import { validateKeys } from "../../../core/validation.js";
2
+ import {
3
+ validateGeneratedItemLimit,
4
+ validateKeys
5
+ } from "../../../core/validation.js";
3
6
  import { mapContinuousScaleValues } from "../../../grammar/scales/index.js";
4
7
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
5
8
  import { resolveLegendGraphicPlacement } from
@@ -7,6 +10,12 @@ import { resolveLegendGraphicPlacement } from
7
10
  import { findLayer } from "../../../selectors/layers.js";
8
11
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
9
12
  "../../../theme/defaults.js";
13
+ import {
14
+ formatContinuousValues,
15
+ sampleContinuousValues,
16
+ selectLegendLayer,
17
+ styleContinuousText
18
+ } from "./continuous/common.js";
10
19
 
11
20
  const OPTIONS = Object.freeze(["target", "count"]);
12
21
 
@@ -31,13 +40,11 @@ export function isStrokeWidthLegendLayer(layer) {
31
40
  }
32
41
 
33
42
  function resolveLayer(program, requested) {
34
- const candidates = program.semanticSpec.layers.filter(isStrokeWidthLegendLayer);
35
- const layer = requested === undefined
36
- ? candidates.length === 1 ? candidates[0] : undefined
37
- : (() => {
38
- const candidate = findLayer(program, requested);
39
- return candidates.includes(candidate) ? candidate : undefined;
40
- })();
43
+ const layer = selectLegendLayer(
44
+ program,
45
+ requested,
46
+ isStrokeWidthLegendLayer
47
+ );
41
48
  if (layer === undefined) {
42
49
  throw new Error(
43
50
  requested === undefined
@@ -56,16 +63,6 @@ function requireScale(program, id) {
56
63
  return scale;
57
64
  }
58
65
 
59
- function styleText(program, id, style) {
60
- return program
61
- .editGraphics({ target: id, property: "fill", value: style.color })
62
- .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
63
- .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
64
- .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
65
- .editGraphics({ target: id, property: "textAlign", value: "left" })
66
- .editGraphics({ target: id, property: "textBaseline", value: "middle" });
67
- }
68
-
69
66
  export const rematerializeStrokeWidthLegend = action(
70
67
  {
71
68
  op: "rematerializeStrokeWidthLegend",
@@ -93,11 +90,7 @@ export const rematerializeStrokeWidthLegend = action(
93
90
  if (plot === undefined) {
94
91
  throw new Error("Stroke-width legend requires resolved plot bounds.");
95
92
  }
96
- const values = Array.from(
97
- { length: config.count },
98
- (_, index) => scale.domain[0] +
99
- index / (config.count - 1) * (scale.domain[1] - scale.domain[0])
100
- );
93
+ const values = sampleContinuousValues(scale.domain, config.count);
101
94
  const widths = mapContinuousScaleValues(values, scale);
102
95
  const originX = plot.x + plot.width + 30;
103
96
  const titleY = plot.y + 28;
@@ -129,15 +122,15 @@ export const rematerializeStrokeWidthLegend = action(
129
122
  .editGraphics({
130
123
  target: "strokeWidthLegendLabels",
131
124
  property: "text",
132
- value: values.map(value => String(+value.toPrecision(3)))
125
+ value: formatContinuousValues(values, scale.domain, "quantitative")
133
126
  });
134
- next = styleText(next, "strokeWidthLegendLabels", config.labels);
127
+ next = styleContinuousText(next, "strokeWidthLegendLabels", config.labels);
135
128
  if (config.titleVisible === false) return next;
136
129
  next = next
137
130
  .editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: originX })
138
131
  .editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: titleY })
139
132
  .editGraphics({ target: "strokeWidthLegendTitle", property: "text", value: title });
140
- return styleText(next, "strokeWidthLegendTitle", config.titleStyle);
133
+ return styleContinuousText(next, "strokeWidthLegendTitle", config.titleStyle);
141
134
  }
142
135
  );
143
136
 
@@ -157,6 +150,7 @@ export const createStrokeWidthLegend = action(
157
150
  "Stroke-width legend count must be an integer of at least 2."
158
151
  );
159
152
  }
153
+ validateGeneratedItemLimit(count, "Stroke-width legend count");
160
154
  return this
161
155
  .editSemantic({ property: "guide.legend.strokeWidth.scale", value: encoding.scale })
162
156
  .editSemantic({ property: "guide.legend.strokeWidth.title", value: encoding.field })