ggaction 0.0.7 → 0.0.9

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 (83) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +33 -5
  3. package/knowledge/action-cards.json +10941 -0
  4. package/knowledge/intent-taxonomy.json +183 -0
  5. package/knowledge/mcp-resources.json +95 -0
  6. package/knowledge/task-packet.schema.json +159 -0
  7. package/knowledge/task-resolver.js +1230 -0
  8. package/package.json +10 -1
  9. package/src/actions/basic.js +3 -3
  10. package/src/actions/categoryOrder/index.js +111 -0
  11. package/src/actions/coordinates/actions.js +4 -0
  12. package/src/actions/data/index.js +6 -0
  13. package/src/actions/data/timeUnit.js +48 -0
  14. package/src/actions/encodings/angle.js +77 -0
  15. package/src/actions/encodings/color/index.js +17 -13
  16. package/src/actions/encodings/color/layout.js +2 -0
  17. package/src/actions/encodings/color/policy.js +3 -0
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/position/policies/area.js +40 -2
  20. package/src/actions/encodings/position/policies/bar.js +6 -0
  21. package/src/actions/encodings/position/policies/index.js +2 -0
  22. package/src/actions/encodings/position/policies/line.js +5 -1
  23. package/src/actions/encodings/position/policies/tick.js +19 -0
  24. package/src/actions/encodings/remove.js +1 -1
  25. package/src/actions/guides/legends/categorical/actions.js +23 -6
  26. package/src/actions/guides/legends/categorical/index.js +93 -2
  27. package/src/actions/guides/legends/categorical/symbols.js +2 -76
  28. package/src/actions/guides/legends/continuous/common.js +17 -5
  29. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  30. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  31. package/src/actions/guides/legends/edit.js +13 -8
  32. package/src/actions/guides/legends/lane.js +468 -0
  33. package/src/actions/guides/legends/remove.js +3 -7
  34. package/src/actions/index.js +3 -1
  35. package/src/actions/marks/area/actions.js +3 -1
  36. package/src/actions/marks/area/materialize.js +12 -3
  37. package/src/actions/marks/index.js +2 -0
  38. package/src/actions/marks/point/materialize.js +17 -5
  39. package/src/actions/marks/tick/actions.js +225 -0
  40. package/src/actions/marks/tick/index.js +1 -0
  41. package/src/actions/primitives/index.js +27 -0
  42. package/src/actions/primitives/semantic.js +22 -185
  43. package/src/actions/primitives/semanticAction.js +188 -0
  44. package/src/actions/primitives/semanticValidation/dataset.js +7 -3
  45. package/src/actions/primitives/semanticValidation/index.js +28 -15
  46. package/src/actions/primitives/semanticValidation/layer.js +22 -16
  47. package/src/actions/scales/consumers/index.js +8 -0
  48. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  49. package/src/actions/scales/materialize.js +2 -0
  50. package/src/actions/selection/actions.js +5 -2
  51. package/src/core/vocabulary.js +7 -3
  52. package/src/grammar/areaSeries.js +112 -2
  53. package/src/grammar/categoryOrder.js +138 -0
  54. package/src/grammar/direction.js +46 -0
  55. package/src/grammar/facets/index.js +1 -1
  56. package/src/grammar/pointShapes.js +34 -6
  57. package/src/grammar/positionCompatibility.js +4 -0
  58. package/src/grammar/schemas/semanticPath.js +2 -1
  59. package/src/grammar/seriesLayout.js +10 -2
  60. package/src/grammar/timeUnit.js +108 -0
  61. package/src/grammar/transformTopology.js +18 -0
  62. package/src/grammar/transforms.js +25 -18
  63. package/src/grammar/window.js +73 -7
  64. package/src/layout/legendLane.js +338 -0
  65. package/src/materialization/dataProvenance.js +2 -2
  66. package/src/materialization/marks/capabilities.js +9 -0
  67. package/src/materialization/marks/index.js +2 -1
  68. package/src/materialization/marks/pathOrder.js +2 -2
  69. package/src/materialization/marks/policies.js +13 -0
  70. package/src/materialization/scales/policies/series.js +9 -0
  71. package/src/materialization/scales/resolve.js +25 -3
  72. package/src/materialization/selection/items/index.js +1 -0
  73. package/src/materialization/selection/items/path.js +4 -1
  74. package/src/materialization/selection/items/tick.js +42 -0
  75. package/src/materialization/selection/policies/index.js +2 -0
  76. package/src/materialization/selection/policies/tick.js +10 -0
  77. package/src/mcp/adapter.js +206 -0
  78. package/src/mcp/cli.js +11 -0
  79. package/src/mcp/server.js +101 -0
  80. package/types/index.d.ts +10 -0
  81. package/types/program.d.ts +96 -3
  82. package/src/actions/coordinates/index.js +0 -5
  83. package/src/actions/primitives/semanticValue.js +0 -1
@@ -1,3 +1,6 @@
1
+ import { action } from "../../../../core/action.js";
2
+ import { resolveStoredSelection } from
3
+ "../../../../materialization/selection/state.js";
1
4
  import {
2
5
  createCategoricalLegend,
3
6
  createLegend,
@@ -12,6 +15,7 @@ import {
12
15
  rematerializeLegendLabels,
13
16
  rematerializeLegendTitle
14
17
  } from "./components.js";
18
+ import { activeConfig, noOptions, symbolGraphic } from "./layout.js";
15
19
  import {
16
20
  createLegendSymbolLines,
17
21
  createLegendSymbolPoints,
@@ -21,9 +25,89 @@ import {
21
25
  rematerializeLegendSymbolPoints,
22
26
  rematerializeLegendSymbolSwatches,
23
27
  rematerializeLegendSymbols,
24
- rematerializeLegendHighlights
28
+ rematerializeBasicLegendHighlights
25
29
  } from "./symbols.js";
26
30
  import { editLegend } from "../edit.js";
31
+ import {
32
+ rematerializeHorizontalLegendLane,
33
+ rematerializeSideLegendLane
34
+ } from "../lane.js";
35
+
36
+ function exactLegendSelection(program, config, highlight) {
37
+ if (highlight.target !== config.target) return undefined;
38
+ const resolved = resolveStoredSelection(program, highlight.selection);
39
+ const selected = new Set(resolved.keys);
40
+ const groups = config.domain.map(value =>
41
+ resolved.items.filter(item => item.fields?.[config.field] === value)
42
+ );
43
+ if (
44
+ groups.some(group => group.length === 0) ||
45
+ resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
46
+ ) return undefined;
47
+ const states = groups.map(group => {
48
+ const count = group.filter(item => selected.has(item.key)).length;
49
+ return count === 0 ? false : count === group.length ? true : undefined;
50
+ });
51
+ return states.includes(undefined) ? undefined : states;
52
+ }
53
+
54
+ function legendLayerStyle(layer, style) {
55
+ const properties = layer.type === "line"
56
+ ? ["stroke", "strokeWidth", "strokeDash", "opacity"]
57
+ : ["fill", "stroke", "strokeWidth", "opacity"];
58
+ return Object.fromEntries(
59
+ properties.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
60
+ );
61
+ }
62
+
63
+ function createLegendHighlightAction() {
64
+ return action(
65
+ {
66
+ op: "rematerializeLegendHighlights",
67
+ description: "Reflect exact categorical mark highlights in legend symbols."
68
+ },
69
+ function (args = {}) {
70
+ noOptions(args, "rematerializeLegendHighlights");
71
+ const hasCategorical =
72
+ this.guideConfigs.legend?.series !== undefined ||
73
+ this.guideConfigs.legend?.color !== undefined;
74
+ if (!hasCategorical) return this;
75
+ const { config } = activeConfig(this);
76
+ const highlights = Object.values(
77
+ this.materializationConfigs.highlights ?? {}
78
+ ).map(highlight => ({
79
+ highlight,
80
+ states: exactLegendSelection(this, config, highlight)
81
+ })).filter(entry => entry.states !== undefined);
82
+ if (highlights.length === 0) return this;
83
+
84
+ let next = this.rematerializeLegendSymbols();
85
+ for (const { highlight, states } of highlights) {
86
+ for (const layer of config.symbol.layers) {
87
+ const id = symbolGraphic(config, layer.type);
88
+ const graphic = next.graphicSpec.objects[id];
89
+ const selectedStyle = legendLayerStyle(layer, highlight.style);
90
+ const dimOpacity = highlight.dimOthers === false
91
+ ? undefined
92
+ : highlight.dimOthers.opacity;
93
+ next = next.editGraphics({
94
+ target: id,
95
+ property: "items",
96
+ value: graphic.items.map((child, index) => ({
97
+ type: child.type ?? graphic.type,
98
+ properties: states[index]
99
+ ? { ...child.properties, ...selectedStyle }
100
+ : dimOpacity === undefined
101
+ ? child.properties
102
+ : { ...child.properties, opacity: dimOpacity }
103
+ }))
104
+ });
105
+ }
106
+ }
107
+ return next;
108
+ }
109
+ );
110
+ }
27
111
 
28
112
  export function registerBasicCategoricalLegendActions(ProgramClass) {
29
113
  ProgramClass.prototype.createLegend = createLegend;
@@ -33,7 +117,8 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
33
117
  ProgramClass.prototype.rematerializeLegendBackground = rematerializeLegendBackground;
34
118
  ProgramClass.prototype.createLegendSymbols = createLegendSymbols;
35
119
  ProgramClass.prototype.rematerializeLegendSymbols = rematerializeLegendSymbols;
36
- ProgramClass.prototype.rematerializeLegendHighlights = rematerializeLegendHighlights;
120
+ ProgramClass.prototype.rematerializeLegendHighlights =
121
+ rematerializeBasicLegendHighlights;
37
122
  ProgramClass.prototype.createLegendSymbolLines = createLegendSymbolLines;
38
123
  ProgramClass.prototype.rematerializeLegendSymbolLines = rematerializeLegendSymbolLines;
39
124
  ProgramClass.prototype.createLegendSymbolPoints = createLegendSymbolPoints;
@@ -45,9 +130,15 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
45
130
  ProgramClass.prototype.createLegendTitle = createLegendTitle;
46
131
  ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
47
132
  ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
133
+ ProgramClass.prototype.rematerializeSideLegendLane =
134
+ rematerializeSideLegendLane;
135
+ ProgramClass.prototype.rematerializeHorizontalLegendLane =
136
+ rematerializeHorizontalLegendLane;
48
137
  }
49
138
 
50
139
  export function registerCategoricalLegendActions(ProgramClass) {
51
140
  registerBasicCategoricalLegendActions(ProgramClass);
141
+ ProgramClass.prototype.rematerializeLegendHighlights =
142
+ createLegendHighlightAction();
52
143
  ProgramClass.prototype.editLegend = editLegend;
53
144
  }
@@ -12,44 +12,6 @@ import {
12
12
  import { resolveLegendGraphicPlacement } from
13
13
  "../../../../materialization/graphicHierarchy.js";
14
14
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
15
- import { resolveStoredSelection } from "../../../../materialization/selection/state.js";
16
-
17
- function exactLegendSelection(program, config, highlight) {
18
- if (highlight.target !== config.target) return undefined;
19
- const resolved = resolveStoredSelection(program, highlight.selection);
20
- const selected = new Set(resolved.keys);
21
- const groups = config.domain.map(value =>
22
- resolved.items.filter(item => item.fields?.[config.field] === value)
23
- );
24
- if (
25
- groups.some(group => group.length === 0) ||
26
- resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
27
- ) return undefined;
28
- const states = groups.map(group => {
29
- const count = group.filter(item => selected.has(item.key)).length;
30
- return count === 0 ? false : count === group.length ? true : undefined;
31
- });
32
- return states.includes(undefined) ? undefined : states;
33
- }
34
-
35
- function legendLayerStyle(layer, style) {
36
- if (layer.type === "line") {
37
- return Object.fromEntries(
38
- ["stroke", "strokeWidth", "strokeDash", "opacity"]
39
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
40
- );
41
- }
42
- if (layer.type === "swatch") {
43
- return Object.fromEntries(
44
- ["fill", "stroke", "strokeWidth", "opacity"]
45
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
46
- );
47
- }
48
- return Object.fromEntries(
49
- ["fill", "stroke", "strokeWidth", "opacity"]
50
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
51
- );
52
- }
53
15
 
54
16
  function makeEditSymbol(type) {
55
17
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
@@ -232,49 +194,13 @@ export const rematerializeLegendSymbols = action(
232
194
  }
233
195
  );
234
196
 
235
- export const rematerializeLegendHighlights = action(
197
+ export const rematerializeBasicLegendHighlights = action(
236
198
  {
237
199
  op: "rematerializeLegendHighlights",
238
200
  description: "Reflect exact categorical mark highlights in legend symbols."
239
201
  },
240
202
  function (args = {}) {
241
203
  noOptions(args, "rematerializeLegendHighlights");
242
- const hasCategorical =
243
- this.guideConfigs.legend?.series !== undefined ||
244
- this.guideConfigs.legend?.color !== undefined;
245
- if (!hasCategorical) return this;
246
- const { config } = activeConfig(this);
247
- const highlights = Object.values(
248
- this.materializationConfigs.highlights ?? {}
249
- ).map(highlight => ({
250
- highlight,
251
- states: exactLegendSelection(this, config, highlight)
252
- })).filter(entry => entry.states !== undefined);
253
- if (highlights.length === 0) return this;
254
-
255
- let next = this.rematerializeLegendSymbols();
256
- for (const { highlight, states } of highlights) {
257
- for (const layer of config.symbol.layers) {
258
- const id = symbolGraphic(config, layer.type);
259
- const graphic = next.graphicSpec.objects[id];
260
- const selectedStyle = legendLayerStyle(layer, highlight.style);
261
- const dimOpacity = highlight.dimOthers === false
262
- ? undefined
263
- : highlight.dimOthers.opacity;
264
- next = next.editGraphics({
265
- target: id,
266
- property: "items",
267
- value: graphic.items.map((child, index) => ({
268
- type: child.type ?? graphic.type,
269
- properties: states[index]
270
- ? { ...child.properties, ...selectedStyle }
271
- : dimOpacity === undefined
272
- ? child.properties
273
- : { ...child.properties, opacity: dimOpacity }
274
- }))
275
- });
276
- }
277
- }
278
- return next;
204
+ return this;
279
205
  }
280
206
  );
@@ -105,12 +105,21 @@ export function normalizeContinuousLegend(args, kind) {
105
105
  }
106
106
  const offset = args.offset ?? 30;
107
107
  validateNonNegative(offset, "Legend offset");
108
- const itemGap = args.itemGap ?? 28;
108
+ const titlePosition = args.titlePosition ?? "top";
109
+ if (!["top", "left"].includes(titlePosition)) {
110
+ throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
111
+ }
112
+ if (
113
+ titlePosition === "left" &&
114
+ (!["top", "bottom"].includes(position) || kind !== "opacity")
115
+ ) {
116
+ throw new Error(
117
+ "Only horizontal opacity legends support left titlePosition."
118
+ );
119
+ }
120
+ const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
109
121
  validatePositive(itemGap, "Legend itemGap");
110
122
  if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
111
- if (args.titlePosition !== undefined && args.titlePosition !== "top") {
112
- throw new Error("Continuous legends currently require top titlePosition.");
113
- }
114
123
  if (kind === "gradient") {
115
124
  for (const key of ["symbol", "columns", "direction", "itemGap"]) {
116
125
  if (Object.hasOwn(args, key)) {
@@ -135,7 +144,9 @@ export function normalizeContinuousLegend(args, kind) {
135
144
  labels: normalizeLegendTextOptions(
136
145
  args.labels,
137
146
  "createLegend.labels",
138
- DEFAULT_LABELS
147
+ titlePosition === "left"
148
+ ? { ...DEFAULT_LABELS, offset: 8 }
149
+ : DEFAULT_LABELS
139
150
  ),
140
151
  titleStyle: normalizeLegendTextOptions(
141
152
  args.titleStyle,
@@ -143,6 +154,7 @@ export function normalizeContinuousLegend(args, kind) {
143
154
  DEFAULT_TITLE
144
155
  ),
145
156
  itemGap,
157
+ titlePosition,
146
158
  border: normalizeBorder(args.border)
147
159
  };
148
160
  }
@@ -39,12 +39,18 @@ function resolveGradientLayout(program, config, scale) {
39
39
  : config.align === "right" ? plot.x + plot.width - length
40
40
  : plot.x + (plot.width - length) / 2;
41
41
  y = config.position === "top"
42
- ? plot.y - config.offset - thickness
43
- : plot.y + plot.height + config.offset;
42
+ ? plot.y - config.offset - thickness - config.labels.offset -
43
+ config.labels.fontSize
44
+ : plot.y + plot.height + config.offset +
45
+ config.titleStyle.fontSize + 12;
44
46
  }
45
47
  const title = vertical
46
48
  ? { x, y: plot.y + 20, align: "left" }
47
- : { x: x + length / 2, y: y - 20, align: "center" };
49
+ : {
50
+ x: x + length / 2,
51
+ y: y - 12 - config.titleStyle.fontSize / 2,
52
+ align: "center"
53
+ };
48
54
  const values = sampleContinuousValues(scale.domain, config.count);
49
55
  const fractions = values.map((_, index) => index / (values.length - 1));
50
56
  const labelOffset = config.labels.offset;
@@ -58,9 +64,7 @@ function resolveGradientLayout(program, config, scale) {
58
64
  }))
59
65
  : fractions.map(fraction => ({
60
66
  x: x + length * fraction,
61
- y: config.position === "top"
62
- ? y - labelOffset
63
- : y + thickness + labelOffset,
67
+ y: y + thickness + labelOffset + config.labels.fontSize / 2,
64
68
  align: "center"
65
69
  }));
66
70
  const ticks = vertical
@@ -72,9 +76,9 @@ function resolveGradientLayout(program, config, scale) {
72
76
  }))
73
77
  : labels.map(label => ({
74
78
  x1: label.x,
75
- y1: config.position === "top" ? y : y + thickness,
79
+ y1: y + thickness,
76
80
  x2: label.x,
77
- y2: config.position === "top" ? y - 6 : y + thickness + 6
81
+ y2: y + thickness + 6
78
82
  }));
79
83
  assertLegendInsideCanvas([
80
84
  title,
@@ -2,6 +2,7 @@ import { action } from "../../../../core/action.js";
2
2
  import { isPlainObject } from "../../../../core/immutable.js";
3
3
  import { validateKeys } from "../../../../core/validation.js";
4
4
  import { mapLinearValues } from "../../../../grammar/scales/index.js";
5
+ import { measureTextWidth } from "../../../../core/textMetrics.js";
5
6
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
7
  import {
7
8
  assertLegendInsideCanvas,
@@ -84,6 +85,7 @@ function resolveOpacityLayout(program, config, scale) {
84
85
  const { plot, canvas } = resolveContinuousBounds(program);
85
86
  const vertical = ["right", "left"].includes(config.position);
86
87
  const values = sampleContinuousValues(scale.domain, config.count);
88
+ const texts = formatContinuousValues(values, scale.domain, "quantitative");
87
89
  const radius = config.symbol.radius;
88
90
  let symbols;
89
91
  let labels;
@@ -109,29 +111,81 @@ function resolveOpacityLayout(program, config, scale) {
109
111
  y: plot.y + 20,
110
112
  align: config.position === "right" ? "left" : "right"
111
113
  };
114
+ } else if (config.titlePosition === "left") {
115
+ const titleWidth = measureTextWidth(config.title, config.titleStyle);
116
+ const labelWidths = texts.map(text =>
117
+ measureTextWidth(text, config.labels)
118
+ );
119
+ const samplesWidth = labelWidths.reduce(
120
+ (sum, width) => sum + radius * 2 + config.labels.offset + width,
121
+ 0
122
+ ) + config.itemGap * (values.length - 1);
123
+ const width = titleWidth + 20 + samplesWidth;
124
+ const startX = config.align === "left" ? plot.x
125
+ : config.align === "right" ? plot.x + plot.width - width
126
+ : plot.x + (plot.width - width) / 2;
127
+ const height = Math.max(
128
+ radius * 2,
129
+ config.labels.fontSize,
130
+ config.titleStyle.fontSize
131
+ );
132
+ const centerY = config.position === "top"
133
+ ? plot.y - config.offset - height / 2
134
+ : plot.y + plot.height + config.offset + height / 2;
135
+ title = { x: startX, y: centerY, align: "left" };
136
+ let cursor = startX + titleWidth + 20;
137
+ symbols = [];
138
+ labels = [];
139
+ for (let index = 0; index < values.length; index += 1) {
140
+ const symbolX = cursor + radius;
141
+ symbols.push({ x: symbolX, y: centerY });
142
+ const labelX = symbolX + radius + config.labels.offset;
143
+ labels.push({ x: labelX, y: centerY, align: "left" });
144
+ cursor = labelX + labelWidths[index] + config.itemGap;
145
+ }
112
146
  } else {
113
147
  const width = (values.length - 1) * Math.max(56, config.itemGap * 2);
114
148
  const startX = config.align === "left" ? plot.x
115
149
  : config.align === "right" ? plot.x + plot.width - width
116
150
  : plot.x + (plot.width - width) / 2;
151
+ const titleHalf = config.titleStyle.fontSize / 2;
152
+ const labelHalf = config.labels.fontSize / 2;
117
153
  const y = config.position === "top"
118
- ? plot.y - config.offset - radius
119
- : plot.y + plot.height + config.offset + radius;
154
+ ? plot.y - config.offset - config.labels.fontSize -
155
+ config.labels.offset - radius
156
+ : plot.y + plot.height + config.offset +
157
+ config.titleStyle.fontSize + 12 + radius;
120
158
  symbols = values.map((_, index) => ({
121
159
  x: startX + index * width / (values.length - 1),
122
160
  y
123
161
  }));
124
162
  labels = symbols.map(symbol => ({
125
163
  x: symbol.x,
126
- y: config.position === "top"
127
- ? symbol.y - radius - config.labels.offset
128
- : symbol.y + radius + config.labels.offset,
164
+ y: symbol.y + radius + config.labels.offset + labelHalf,
129
165
  align: "center"
130
166
  }));
131
- title = { x: startX + width / 2, y: y - 26, align: "center" };
167
+ title = {
168
+ x: startX + width / 2,
169
+ y: y - radius - 12 - titleHalf,
170
+ align: "center"
171
+ };
132
172
  }
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
+ });
133
187
  assertLegendInsideCanvas(
134
- [title, ...symbols, ...labels],
188
+ [title, titleEdge, ...symbols, ...labels, ...labelEdges],
135
189
  canvas,
136
190
  "Opacity legend layout"
137
191
  );
@@ -145,14 +199,17 @@ function resolveOpacityLayout(program, config, scale) {
145
199
  x: symbol.x - radius,
146
200
  y: symbol.y - radius
147
201
  })),
148
- ...labels.map(label => ({
149
- x: label.x + (
150
- label.align === "left" ? 42 : label.align === "right" ? -42 : 0
151
- ),
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),
152
209
  y: label.y
153
210
  }))
154
211
  ], config.border, canvas, "Opacity legend");
155
- return { values, symbols, labels, title, background };
212
+ return { values, texts, symbols, labels, title, background };
156
213
  }
157
214
 
158
215
  export const rematerializeOpacityLegend = action(
@@ -229,11 +286,7 @@ export const rematerializeOpacityLegend = action(
229
286
  .editGraphics({
230
287
  target: "opacityLegendLabels",
231
288
  property: "text",
232
- value: formatContinuousValues(
233
- layout.values,
234
- scale.domain,
235
- "quantitative"
236
- )
289
+ value: layout.texts
237
290
  });
238
291
  next = editLegendBackground(
239
292
  next,
@@ -54,7 +54,7 @@ function editContinuous(program, kind, previous, args) {
54
54
  ? ["target", "position", "align", "offset", "title", "labels",
55
55
  "titleStyle", "border", "count", "gradient"]
56
56
  : ["target", "position", "align", "offset", "title", "symbol", "labels",
57
- "titleStyle", "itemGap", "border", "count"];
57
+ "titleStyle", "titlePosition", "itemGap", "border", "count"];
58
58
  for (const key of Object.keys(args)) {
59
59
  if (!allowed.includes(key)) throw new Error(`${kind} legend does not accept ${key}.`);
60
60
  }
@@ -69,18 +69,25 @@ function editContinuous(program, kind, previous, args) {
69
69
  const title = titleMode === "auto"
70
70
  ? inferred
71
71
  : typeof titleMode === "string" ? titleMode : previous.title;
72
+ const titlePosition = args.titlePosition ?? previous.titlePosition;
73
+ const enteringInline = kind === "opacity" &&
74
+ titlePosition === "left" && previous.titlePosition !== "left";
75
+ const labels = mergeObject(previous.labels, args.labels);
72
76
  const normalized = normalizeContinuousLegend({
73
77
  target: previous.target,
74
78
  position: args.position ?? previous.position,
75
79
  align: args.align ?? previous.align,
76
80
  offset: args.offset ?? previous.offset,
77
81
  count: args.count ?? previous.count,
82
+ titlePosition,
78
83
  title,
79
- labels: mergeObject(previous.labels, args.labels),
84
+ labels: enteringInline && args.labels?.offset === undefined
85
+ ? { ...labels, offset: 8 }
86
+ : labels,
80
87
  titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
81
88
  border: mergeBorder(previous.border, args.border),
82
89
  ...(kind === "opacity" ? {
83
- itemGap: args.itemGap ?? previous.itemGap,
90
+ itemGap: args.itemGap ?? (enteringInline ? 20 : previous.itemGap),
84
91
  symbol: args.symbol ?? previous.symbol
85
92
  } : {})
86
93
  }, kind);
@@ -112,9 +119,7 @@ function editContinuous(program, kind, previous, args) {
112
119
  before: kind === "gradient" ? "colorGradientStrips" : "opacityLegendSymbols"
113
120
  });
114
121
  next = reconcileGraphic(next, `${prefix}Title`, titleVisible, { type: "text" });
115
- return kind === "gradient"
116
- ? next.rematerializeGradientLegend()
117
- : next.rematerializeOpacityLegend();
122
+ return next.rematerializeLegend();
118
123
  }
119
124
 
120
125
  function editInterval(program, previous, args) {
@@ -165,7 +170,7 @@ function editInterval(program, previous, args) {
165
170
  next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
166
171
  type: "text"
167
172
  });
168
- return next.rematerializeIntervalLegend();
173
+ return next.rematerializeLegend();
169
174
  }
170
175
 
171
176
  function editStrokeWidth(program, previous, args) {
@@ -220,7 +225,7 @@ function editStrokeWidth(program, previous, args) {
220
225
  next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
221
226
  type: "text"
222
227
  });
223
- return next.rematerializeStrokeWidthLegend();
228
+ return next.rematerializeLegend();
224
229
  }
225
230
 
226
231
  function categoricalSymbolIds(config) {