ggaction 0.0.15 → 0.0.16

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 (236) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +22 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +33 -6
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/boxPlots/components.js +6 -8
  13. package/src/actions/boxPlots/create.js +4 -5
  14. package/src/actions/boxPlots/edit.js +3 -4
  15. package/src/actions/boxPlots/materialize.js +1 -1
  16. package/src/actions/canvas/actions.js +3 -3
  17. package/src/actions/canvas/fitting.js +1 -1
  18. package/src/actions/categoryOrder/index.js +6 -8
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +1 -1
  21. package/src/actions/charts/beeswarm.js +1 -1
  22. package/src/actions/charts/density.js +1 -1
  23. package/src/actions/charts/ecdf.js +2 -2
  24. package/src/actions/charts/endpoints.js +4 -4
  25. package/src/actions/charts/heatmap.js +1 -1
  26. package/src/actions/charts/histogram.js +1 -1
  27. package/src/actions/charts/horizon.js +1 -1
  28. package/src/actions/charts/interval-regression.js +2 -2
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/parallel.js +1 -1
  31. package/src/actions/charts/pie.js +1 -1
  32. package/src/actions/charts/polar.js +2 -2
  33. package/src/actions/charts/radar.js +1 -1
  34. package/src/actions/charts/radial.js +2 -2
  35. package/src/actions/charts/raincloud.js +2 -2
  36. package/src/actions/charts/rug-strip.js +2 -2
  37. package/src/actions/charts/scatter.js +1 -1
  38. package/src/actions/composition/actions.js +20 -24
  39. package/src/actions/coordinates/actions.js +1 -1
  40. package/src/actions/coordinates/edit.js +1 -1
  41. package/src/actions/data/basicBin2d.js +4 -5
  42. package/src/actions/data/bin.js +2 -2
  43. package/src/actions/data/bin2d.js +6 -8
  44. package/src/actions/data/bin2dMaterialize.js +4 -5
  45. package/src/actions/data/box.js +4 -4
  46. package/src/actions/data/complete.js +2 -2
  47. package/src/actions/data/computed.js +2 -2
  48. package/src/actions/data/create.js +5 -12
  49. package/src/actions/data/density.js +3 -3
  50. package/src/actions/data/derived.js +10 -14
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +35 -23
  53. package/src/actions/data/filter.js +14 -20
  54. package/src/actions/data/fold.js +2 -2
  55. package/src/actions/data/gradientProfile.js +2 -2
  56. package/src/actions/data/horizon.js +2 -2
  57. package/src/actions/data/impute.js +2 -2
  58. package/src/actions/data/index.js +55 -54
  59. package/src/actions/data/interval.js +2 -2
  60. package/src/actions/data/intervalEdit.js +26 -18
  61. package/src/actions/data/normalize.js +2 -2
  62. package/src/actions/data/regression.js +5 -6
  63. package/src/actions/data/revise.js +48 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/stack.js +2 -2
  66. package/src/actions/data/summary.js +2 -2
  67. package/src/actions/data/timeUnit.js +2 -2
  68. package/src/actions/data/window.js +2 -2
  69. package/src/actions/encodings/angle.js +1 -1
  70. package/src/actions/encodings/appearance.js +4 -4
  71. package/src/actions/encodings/barWidth.js +1 -1
  72. package/src/actions/encodings/channels.js +2 -2
  73. package/src/actions/encodings/color/index.js +1 -1
  74. package/src/actions/encodings/density.js +2 -2
  75. package/src/actions/encodings/histogram.js +1 -1
  76. package/src/actions/encodings/horizon.js +6 -8
  77. package/src/actions/encodings/offset.js +2 -2
  78. package/src/actions/encodings/parallel.js +4 -5
  79. package/src/actions/encodings/pathOrder.js +6 -11
  80. package/src/actions/encodings/position/index.js +4 -4
  81. package/src/actions/encodings/ranged.js +5 -5
  82. package/src/actions/encodings/remove.js +4 -5
  83. package/src/actions/encodings/ruleAppearance.js +1 -1
  84. package/src/actions/encodings/seriesLayout.js +1 -1
  85. package/src/actions/encodings/stroke.js +1 -1
  86. package/src/actions/encodings/strokeDash.js +2 -2
  87. package/src/actions/encodings/text.js +1 -1
  88. package/src/actions/errorBands/create.js +11 -17
  89. package/src/actions/errorBands/edit.js +13 -131
  90. package/src/actions/errorBars/create.js +8 -13
  91. package/src/actions/errorBars/edit.js +10 -121
  92. package/src/actions/facets/actions.js +30 -16
  93. package/src/actions/facets/derive.js +8 -2
  94. package/src/actions/facets/guides.js +4 -5
  95. package/src/actions/facets/replay.js +4 -5
  96. package/src/actions/gradientPlots/components.js +5 -6
  97. package/src/actions/gradientPlots/create.js +4 -5
  98. package/src/actions/gradientPlots/edit.js +4 -5
  99. package/src/actions/gradientPlots/materialize.js +2 -2
  100. package/src/actions/gradientPlots/rebind.js +4 -5
  101. package/src/actions/guides/axes/axes.js +23 -4
  102. package/src/actions/guides/axes/axis.js +2 -2
  103. package/src/actions/guides/axes/edit.js +2 -2
  104. package/src/actions/guides/axes/labels.js +9 -9
  105. package/src/actions/guides/axes/lines.js +9 -7
  106. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  107. package/src/actions/guides/axes/parallel.js +5 -5
  108. package/src/actions/guides/axes/remove.js +4 -4
  109. package/src/actions/guides/axes/tickGroups.js +4 -4
  110. package/src/actions/guides/axes/ticks.js +13 -4
  111. package/src/actions/guides/axes/titles.js +10 -10
  112. package/src/actions/guides/grids/grid.js +18 -29
  113. package/src/actions/guides/guides.js +1 -1
  114. package/src/actions/guides/legends/blocks.js +1 -1
  115. package/src/actions/guides/legends/categorical/actions.js +4 -4
  116. package/src/actions/guides/legends/categorical/components.js +38 -33
  117. package/src/actions/guides/legends/categorical/layout.js +1 -1
  118. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  119. package/src/actions/guides/legends/continuous/common.js +46 -8
  120. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  121. package/src/actions/guides/legends/continuous/interval.js +13 -13
  122. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  123. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  124. package/src/actions/guides/legends/edit.js +1 -1
  125. package/src/actions/guides/legends/focused.js +5 -5
  126. package/src/actions/guides/legends/lane.js +6 -6
  127. package/src/actions/guides/legends/remove.js +4 -5
  128. package/src/actions/guides/legends/size.js +21 -82
  129. package/src/actions/guides/legends/strokeWidth.js +17 -77
  130. package/src/actions/guides/polar/axes/facade.js +4 -4
  131. package/src/actions/guides/polar/axes/labels.js +5 -5
  132. package/src/actions/guides/polar/axes/lines.js +9 -15
  133. package/src/actions/guides/polar/axes/ticks.js +9 -15
  134. package/src/actions/guides/polar/axes/titles.js +5 -5
  135. package/src/actions/guides/polar/grids.js +11 -13
  136. package/src/actions/index.js +2 -0
  137. package/src/actions/intervals/revision.js +126 -0
  138. package/src/actions/marks/arc/actions.js +3 -3
  139. package/src/actions/marks/area/actions.js +4 -4
  140. package/src/actions/marks/bar/create.js +1 -1
  141. package/src/actions/marks/bar/edit.js +1 -1
  142. package/src/actions/marks/bar/materialize.js +12 -38
  143. package/src/actions/marks/line/actions.js +3 -3
  144. package/src/actions/marks/point/create.js +1 -1
  145. package/src/actions/marks/point/edit.js +1 -1
  146. package/src/actions/marks/point/jitter.js +6 -8
  147. package/src/actions/marks/point/materialize.js +16 -24
  148. package/src/actions/marks/point/packing.js +6 -8
  149. package/src/actions/marks/rect/actions.js +3 -3
  150. package/src/actions/marks/references.js +8 -10
  151. package/src/actions/marks/remove.js +5 -49
  152. package/src/actions/marks/rule/actions.js +4 -4
  153. package/src/actions/marks/text/actions.js +5 -5
  154. package/src/actions/marks/text/layout.js +12 -20
  155. package/src/actions/marks/text/removal.js +1 -1
  156. package/src/actions/marks/text/selection.js +4 -5
  157. package/src/actions/marks/tick/actions.js +3 -3
  158. package/src/actions/primitives/createGraphics.js +5 -4
  159. package/src/actions/primitives/editGraphics.js +5 -4
  160. package/src/actions/primitives/graphicProperties.js +6 -0
  161. package/src/actions/primitives/semantic.js +10 -3
  162. package/src/actions/primitives/semanticAction.js +7 -4
  163. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  164. package/src/actions/regression/components.js +10 -14
  165. package/src/actions/regression/create.js +3 -4
  166. package/src/actions/regression/edit.js +3 -4
  167. package/src/actions/resources/remove.js +12 -11
  168. package/src/actions/scales/create.js +1 -1
  169. package/src/actions/scales/edit.js +1 -1
  170. package/src/actions/scales/materialize.js +1 -1
  171. package/src/actions/scales/offset.js +2 -2
  172. package/src/actions/scales/parallel.js +1 -1
  173. package/src/actions/scales/quantitativeColor.js +4 -5
  174. package/src/actions/selection/actions.js +25 -36
  175. package/src/actions/textMetrics/index.js +56 -0
  176. package/src/actions/theme/actions.js +2 -2
  177. package/src/actions/theme/reconcile.js +65 -121
  178. package/src/actions/titles/actions.js +8 -8
  179. package/src/actions/titles/resolve.js +3 -3
  180. package/src/actions/violinPlots/create.js +1 -1
  181. package/src/actions/violinPlots/edit.js +1 -1
  182. package/src/core/action.js +107 -35
  183. package/src/core/diagnostics.js +16 -0
  184. package/src/core/extensionRegistry.js +27 -0
  185. package/src/core/font.js +18 -0
  186. package/src/core/identifiers.js +5 -4
  187. package/src/core/immutable.js +10 -3
  188. package/src/core/optionRequirements.js +8 -0
  189. package/src/core/programState.js +1 -1
  190. package/src/core/resourceReferences.js +15 -6
  191. package/src/core/textMetricProfile.js +28 -0
  192. package/src/core/textMetrics.js +24 -3
  193. package/src/core/validation.js +22 -10
  194. package/src/diagnostics.js +1 -0
  195. package/src/grammar/impute.js +3 -2
  196. package/src/grammar/scales/types.js +8 -0
  197. package/src/grammar/schemas/graphicBounds.js +11 -11
  198. package/src/layout/labels.js +11 -10
  199. package/src/layout/legend.js +4 -4
  200. package/src/layout/legendItems.js +6 -6
  201. package/src/layout/text.js +10 -9
  202. package/src/layout/title.js +9 -9
  203. package/src/materialization/facetGuides/placement.js +1 -1
  204. package/src/materialization/facetGuides/preparation.js +1 -1
  205. package/src/materialization/facetHeaders.js +11 -10
  206. package/src/materialization/guides/layout.js +3 -3
  207. package/src/materialization/scaleGuideDependencies.js +10 -0
  208. package/src/materialization/selection/items/common.js +1 -1
  209. package/src/materialization/text.js +2 -2
  210. package/src/materialization/typography.js +31 -0
  211. package/src/mcp/cli.js +5 -3
  212. package/src/persistence/codec.js +65 -0
  213. package/src/persistence/validation.js +185 -0
  214. package/src/persistence.js +74 -0
  215. package/src/renderers/canvas/index.js +5 -4
  216. package/src/renderers/nativeBackend.js +10 -0
  217. package/src/renderers/options.js +13 -0
  218. package/src/renderers/pdf.js +25 -54
  219. package/src/renderers/png.js +22 -17
  220. package/src/renderers/svg.js +10 -14
  221. package/src/renderers/text.js +1 -11
  222. package/src/selectors/coordinates.js +3 -1
  223. package/src/selectors/datasets.js +8 -6
  224. package/src/selectors/layers.js +6 -4
  225. package/src/selectors/markOwners.js +46 -0
  226. package/src/selectors/scales.js +6 -4
  227. package/src/theme/defaults.js +2 -1
  228. package/src/version.js +2 -0
  229. package/types/accessibility.d.ts +37 -0
  230. package/types/basic.d.ts +4 -2
  231. package/types/diagnostics.d.ts +15 -0
  232. package/types/index.d.ts +5 -0
  233. package/types/pdf.d.ts +9 -0
  234. package/types/persistence.d.ts +7 -0
  235. package/types/png.d.ts +9 -0
  236. package/types/program.d.ts +34 -1
@@ -1,4 +1,4 @@
1
- import { action } from "../../../../core/action.js";
1
+ import { action, closedAction } from "../../../../core/action.js";
2
2
  import { isPlainObject } from "../../../../core/immutable.js";
3
3
  import { validateKeys } from "../../../../core/validation.js";
4
4
  import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
@@ -7,6 +7,7 @@ import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
7
7
  import {
8
8
  assertLegendBoundsInsideCanvas,
9
9
  editLegendBackground,
10
+ editGraphicProperties,
10
11
  formatContinuousValues,
11
12
  normalizeContinuousLegend,
12
13
  requireResolvedLegendScale,
@@ -26,7 +27,7 @@ export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12
26
27
 
27
28
  const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
28
29
 
29
- export function resolveGradientLayout(program, config, scale) {
30
+ export function resolveGradientLayout(program, config, scale, label = "Gradient legend") {
30
31
  const { plot, canvas } = resolveContinuousBounds(program);
31
32
  const vertical = ["right", "left"].includes(config.position);
32
33
  const length = config.gradient.length;
@@ -111,25 +112,25 @@ export function resolveGradientLayout(program, config, scale) {
111
112
  bottom: Math.max(tick.y1, tick.y2) + 0.5
112
113
  }));
113
114
  const labelBounds = labels.map((label, index) =>
114
- resolveLegendTextBounds(label, texts[index], config.labels)
115
+ resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
115
116
  );
116
117
  const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
117
118
  title,
118
119
  config.title,
119
120
  config.titleStyle
120
- );
121
+ , program.materializationConfigs.textMetrics);
121
122
  const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
122
123
  ...(config.titleVisible === false ? [] : [titleBounds])];
123
124
  assertLegendBoundsInsideCanvas(
124
125
  occupiedBounds,
125
126
  canvas,
126
- "Gradient legend layout", config
127
+ `${label} layout`, config
127
128
  );
128
129
  const background = resolveLegendBackgroundFromBounds(
129
130
  occupiedBounds,
130
131
  config.border,
131
132
  canvas,
132
- "Gradient legend", config
133
+ label, config
133
134
  );
134
135
  return {
135
136
  vertical, x, y, length, thickness, values, texts, labels, ticks, title,
@@ -137,11 +138,11 @@ export function resolveGradientLayout(program, config, scale) {
137
138
  };
138
139
  }
139
140
 
140
- function resolveGradientConfig(program, config) {
141
- const layer = resolveContinuousColorLayer(program, config.target);
142
- const encoding = layer.encoding.color;
141
+ export function resolveGradientConfig(program, config, channel = "color") {
142
+ const layer = resolveContinuousColorLayer(program, config.target, channel);
143
+ const encoding = layer.encoding[channel];
143
144
  if (!["quantitative", "temporal"].includes(encoding.fieldType)) {
144
- throw new Error("Gradient legend requires quantitative or temporal color.");
145
+ throw new Error(`${channel === "color" ? "Gradient" : "Stroke gradient"} legend requires quantitative or temporal ${channel}.`);
145
146
  }
146
147
  const scale = requireResolvedLegendScale(
147
148
  program,
@@ -163,153 +164,84 @@ function resolveGradientConfig(program, config) {
163
164
  };
164
165
  }
165
166
 
166
- export const rematerializeGradientLegend = action(
167
+ // Both channels use the same positions and width policy. Their paint and
168
+ // semantic/graphic namespaces remain explicit channel-owned choices.
169
+ export function materializeGradientLegend(program, resolved, appearance) {
170
+ const stroke = appearance !== undefined;
171
+ const channel = stroke ? "stroke" : "color";
172
+ const kind = stroke ? "strokeGradient" : "gradient";
173
+ const prefix = `${channel}Gradient`;
174
+ const { scale, encoding, config: currentConfig } = resolved;
175
+ const config = resolveEffectiveLegendBlockConfig(program, kind, currentConfig);
176
+ const layout = resolveGradientLayout(program, config, scale,
177
+ stroke ? "Stroke gradient legend" : "Gradient legend");
178
+ const stripCount = 60;
179
+ const stripSize = layout.length / stripCount;
180
+ const strips = Array.from({ length: stripCount }, (_, index) => {
181
+ const fraction = (index + 0.5) / stripCount;
182
+ const position = layout.vertical ? 1 - fraction : fraction;
183
+ const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
184
+ const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
185
+ const [color] = mapScaleConsumerValues([value], samplingScale, channel);
186
+ return {
187
+ x: layout.x + (layout.vertical ? 0 : index * stripSize),
188
+ y: layout.y + (layout.vertical ? index * stripSize : 0),
189
+ width: layout.vertical ? layout.thickness : stripSize,
190
+ height: layout.vertical ? stripSize : layout.thickness,
191
+ fill: stroke ? appearance.fill : color,
192
+ stroke: color,
193
+ strokeWidth: stroke ? appearance.strokeWidth : 0
194
+ };
195
+ });
196
+ let next = program
197
+ .editSemantic({ property: `guide.legend.${channel}.scale`, value: encoding.scale })
198
+ .editSemantic({ property: `guide.legend.${channel}.title`, value: config.title })
199
+ ._withLegendConfig(kind, currentConfig);
200
+ // Preserve each channel's established primitive trace order.
201
+ if (!stroke) next = next.editGraphics({ target: `${prefix}Strips`, property: "length", value: strips.length });
202
+ next = editLegendBackground(next, `${prefix}Background`, layout.background, config.border);
203
+ if (stroke) next = next.editGraphics({ target: `${prefix}Strips`, property: "length", value: strips.length });
204
+ for (const property of ["x", "y", "width", "height", "fill", "stroke", "strokeWidth"]) {
205
+ next = next.editGraphics({ target: `${prefix}Strips`, property, value: strips.map(strip => strip[property]) });
206
+ }
207
+ next = next.editGraphics({ target: `${prefix}Ticks`, property: "length", value: layout.ticks.length });
208
+ for (const property of ["x1", "y1", "x2", "y2"]) {
209
+ next = next.editGraphics({ target: `${prefix}Ticks`, property, value: layout.ticks.map(tick => tick[property]) });
210
+ }
211
+ next = editGraphicProperties(next, `${prefix}Ticks`, { stroke: DEFAULT_COLORS.mutedText, strokeWidth: 1 });
212
+ next = editGraphicProperties(next, `${prefix}Labels`, {
213
+ length: layout.labels.length, x: layout.labels.map(label => label.x),
214
+ y: layout.labels.map(label => label.y), text: layout.texts
215
+ });
216
+ next = styleContinuousText(next, `${prefix}Labels`, config.labels, { align: layout.labels[0].align });
217
+ if (config.titleVisible === false) return next;
218
+ next = editGraphicProperties(next, `${prefix}Title`, { x: layout.title.x, y: layout.title.y, text: config.title });
219
+ return styleContinuousText(next, `${prefix}Title`, config.titleStyle, { align: layout.title.align });
220
+ }
221
+
222
+ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
167
223
  {
168
224
  op: "rematerializeGradientLegend",
169
225
  description: "Rematerialize a continuous color gradient legend."
170
- },
226
+ }, [],
171
227
  function (args = {}) {
172
- validateKeys(args, [], "rematerializeGradientLegend");
173
228
  const stored = this.guideConfigs.legend?.gradient;
174
229
  if (stored === undefined) {
175
230
  throw new Error("Gradient legend requires stored configuration.");
176
231
  }
177
- const { scale, encoding, config: currentConfig } = resolveGradientConfig(this, stored);
178
- const config = resolveEffectiveLegendBlockConfig(this, "gradient", currentConfig);
179
- const layout = resolveGradientLayout(this, config, scale);
180
- const stripCount = 60;
181
- const stripSize = layout.length / stripCount;
182
- const strips = Array.from({ length: stripCount }, (_, index) => {
183
- const fraction = (index + 0.5) / stripCount;
184
- const position = layout.vertical ? 1 - fraction : fraction;
185
- // Keep legacy uniform samples exact without a value/domain round trip.
186
- const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
187
- const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
188
- const [color] = mapScaleConsumerValues([value], samplingScale, "color");
189
- return {
190
- x: layout.x + (layout.vertical ? 0 : index * stripSize),
191
- y: layout.y + (layout.vertical ? index * stripSize : 0),
192
- width: layout.vertical ? layout.thickness : stripSize,
193
- height: layout.vertical ? stripSize : layout.thickness,
194
- fill: color,
195
- stroke: color,
196
- strokeWidth: 0
197
- };
198
- });
199
- let next = this
200
- .editSemantic({
201
- property: "guide.legend.color.scale",
202
- value: encoding.scale
203
- })
204
- .editSemantic({
205
- property: "guide.legend.color.title",
206
- value: config.title
207
- })
208
- ._withLegendConfig("gradient", currentConfig)
209
- .editGraphics({
210
- target: "colorGradientStrips",
211
- property: "length",
212
- value: strips.length
213
- });
214
- next = editLegendBackground(
215
- next,
216
- "colorGradientBackground",
217
- layout.background,
218
- config.border
219
- );
220
- for (const property of [
221
- "x", "y", "width", "height", "fill", "stroke", "strokeWidth"
222
- ]) {
223
- next = next.editGraphics({
224
- target: "colorGradientStrips",
225
- property,
226
- value: strips.map(strip => strip[property])
227
- });
228
- }
229
- next = next.editGraphics({
230
- target: "colorGradientTicks",
231
- property: "length",
232
- value: layout.ticks.length
233
- });
234
- for (const property of ["x1", "y1", "x2", "y2"]) {
235
- next = next.editGraphics({
236
- target: "colorGradientTicks",
237
- property,
238
- value: layout.ticks.map(tick => tick[property])
239
- });
240
- }
241
- next = next
242
- .editGraphics({
243
- target: "colorGradientTicks",
244
- property: "stroke",
245
- value: DEFAULT_COLORS.mutedText
246
- })
247
- .editGraphics({
248
- target: "colorGradientTicks",
249
- property: "strokeWidth",
250
- value: 1
251
- })
252
- .editGraphics({
253
- target: "colorGradientLabels",
254
- property: "length",
255
- value: layout.labels.length
256
- })
257
- .editGraphics({
258
- target: "colorGradientLabels",
259
- property: "x",
260
- value: layout.labels.map(label => label.x)
261
- })
262
- .editGraphics({
263
- target: "colorGradientLabels",
264
- property: "y",
265
- value: layout.labels.map(label => label.y)
266
- })
267
- .editGraphics({
268
- target: "colorGradientLabels",
269
- property: "text",
270
- value: layout.texts
271
- });
272
- next = styleContinuousText(
273
- next,
274
- "colorGradientLabels",
275
- config.labels,
276
- { align: layout.labels[0].align }
277
- );
278
- if (config.titleVisible === false) return next;
279
- next = next
280
- .editGraphics({
281
- target: "colorGradientTitle",
282
- property: "x",
283
- value: layout.title.x
284
- })
285
- .editGraphics({
286
- target: "colorGradientTitle",
287
- property: "y",
288
- value: layout.title.y
289
- })
290
- .editGraphics({
291
- target: "colorGradientTitle",
292
- property: "text",
293
- value: config.title
294
- });
295
- return styleContinuousText(
296
- next,
297
- "colorGradientTitle",
298
- config.titleStyle,
299
- { align: layout.title.align }
300
- );
232
+ return materializeGradientLegend(this, resolveGradientConfig(this, stored));
301
233
  }
302
234
  );
303
235
 
304
236
 
305
- export function resolveGradientLegendCreation(program, args = {}) {
237
+ export function resolveGradientLegendCreation(program, args = {}, channel = "color") {
306
238
  const config = normalizeContinuousLegend(args, "gradient");
307
239
  if (args.channels !== undefined && (
308
240
  !Array.isArray(args.channels) ||
309
241
  args.channels.length !== 1 ||
310
- args.channels[0] !== "color"
242
+ args.channels[0] !== channel
311
243
  )) {
312
- throw new Error('Gradient legend requires channels: ["color"].');
244
+ throw new Error(`Gradient legend requires channels: ["${channel}"].`);
313
245
  }
314
246
  if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
315
247
  throw new TypeError("createLegend.gradient must be a plain object.");
@@ -326,62 +258,64 @@ export function resolveGradientLegendCreation(program, args = {}) {
326
258
  config.titleVisible = true;
327
259
  validatePositive(config.gradient.length, "Gradient length");
328
260
  validatePositive(config.gradient.thickness, "Gradient thickness");
329
- const resolved = resolveGradientConfig(program, config);
261
+ const resolved = resolveGradientConfig(program, config, channel);
330
262
  return resolved;
331
263
  }
332
264
 
333
- export function createGradientLegendFromConfig(program, config) {
334
- const resolved = resolveGradientConfig(program, config);
335
- resolveGradientLayout(program, resolved.config, resolved.scale);
265
+ export function createGradientLegendFromConfig(program, config, channel = "color") {
266
+ const prefix = `${channel}Gradient`;
267
+ const kind = channel === "color" ? "gradient" : "strokeGradient";
268
+ const resolved = resolveGradientConfig(program, config, channel);
269
+ resolveGradientLayout(program, resolved.config, resolved.scale, channel === "color" ? "Gradient legend" : "Stroke gradient legend");
336
270
  let next = program
337
271
  .editSemantic({
338
- property: "guide.legend.color.scale",
272
+ property: `guide.legend.${channel}.scale`,
339
273
  value: resolved.encoding.scale
340
274
  })
341
275
  .editSemantic({
342
- property: "guide.legend.color.title",
276
+ property: `guide.legend.${channel}.title`,
343
277
  value: resolved.config.title
344
278
  })
345
- ._withLegendConfig("gradient", resolved.config);
279
+ ._withLegendConfig(kind, resolved.config);
346
280
  if (resolved.config.border !== false) {
347
281
  next = next.createGraphics({
348
- id: "colorGradientBackground",
282
+ id: `${prefix}Background`,
349
283
  type: "rect",
350
284
  ...resolveLegendGraphicPlacement(next)
351
285
  });
352
286
  }
353
287
  next = next
354
288
  .createGraphics({
355
- id: "colorGradientStrips",
289
+ id: `${prefix}Strips`,
356
290
  type: "rect",
357
291
  length: 0,
358
292
  ...resolveLegendGraphicPlacement(next, resolved.config.border === false
359
293
  ? {}
360
- : { after: "colorGradientBackground" })
294
+ : { after: `${prefix}Background` })
361
295
  })
362
296
  .createGraphics({
363
- id: "colorGradientTicks",
297
+ id: `${prefix}Ticks`,
364
298
  type: "line",
365
299
  length: 0,
366
300
  ...resolveLegendGraphicPlacement(next)
367
301
  })
368
302
  .createGraphics({
369
- id: "colorGradientLabels",
303
+ id: `${prefix}Labels`,
370
304
  type: "text",
371
305
  length: 0,
372
306
  ...resolveLegendGraphicPlacement(next)
373
307
  });
374
308
  if (resolved.config.titleVisible !== false) {
375
309
  next = next.createGraphics({
376
- id: "colorGradientTitle",
310
+ id: `${prefix}Title`,
377
311
  type: "text",
378
312
  ...resolveLegendGraphicPlacement(next)
379
313
  });
380
314
  }
381
- return next.rematerializeGradientLegend();
315
+ return next[channel === "color" ? "rematerializeGradientLegend" : "rematerializeStrokeGradientLegend"]();
382
316
  }
383
317
 
384
- export const createGradientLegend = action(
318
+ export const createGradientLegend = /* @__PURE__ */ action(
385
319
  {
386
320
  op: "createGradientLegend",
387
321
  description: "Create a continuous color gradient legend."
@@ -1,5 +1,5 @@
1
1
  import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
2
- import { action } from "../../../../core/action.js";
2
+ import { action, closedAction } from "../../../../core/action.js";
3
3
  import { isPlainObject } from "../../../../core/immutable.js";
4
4
  import {
5
5
  validateKeys,
@@ -86,15 +86,16 @@ export function normalizeIntervalLegend(args) {
86
86
  };
87
87
  }
88
88
 
89
- export function resolveIntervalConfig(program, stored) {
90
- const layer = resolveContinuousColorLayer(program, stored.target);
91
- const encoding = layer.encoding.color;
89
+ export function resolveIntervalConfig(program, stored, channel = "color") {
90
+ const label = channel === "color" ? "Interval legend" : "Stroke interval legend";
91
+ const layer = resolveContinuousColorLayer(program, stored.target, channel);
92
+ const encoding = layer.encoding[channel];
92
93
  if (encoding.fieldType !== "quantitative") {
93
- throw new Error("Interval legend requires quantitative color.");
94
+ throw new Error(`${label} requires quantitative ${channel}.`);
94
95
  }
95
96
  const scale = program.resolvedScales[encoding.scale];
96
97
  if (!["quantize", "quantile", "threshold"].includes(scale?.type)) {
97
- throw new Error(`Interval legend requires a resolved discretized scale "${encoding.scale}".`);
98
+ throw new Error(`${label} requires a resolved discretized scale "${encoding.scale}".`);
98
99
  }
99
100
  return { encoding, scale, config: {
100
101
  ...stored,
@@ -104,32 +105,31 @@ export function resolveIntervalConfig(program, stored) {
104
105
  } };
105
106
  }
106
107
 
107
- export function resolveIntervalLayout(program, config, scale) {
108
+ export function resolveIntervalLayout(program, config, scale, label = "Interval legend") {
108
109
  const { plot, canvas } = resolveContinuousBounds(program);
109
110
  const labels = formatDiscretizedIntervals(scale.thresholds, config.labels.format);
110
- const layout = resolveLegendItemLayout(plot, config, labels, config.symbol);
111
+ const layout = resolveLegendItemLayout(plot, config, labels, config.symbol, undefined, program.materializationConfigs.textMetrics);
111
112
  const occupiedBounds = layout.bounds;
112
113
  assertLegendBoundsInsideCanvas(
113
114
  occupiedBounds,
114
115
  canvas,
115
- "Interval legend layout", config
116
+ `${label} layout`, config
116
117
  );
117
118
  const background = resolveLegendBackgroundFromBounds(
118
119
  occupiedBounds,
119
120
  config.border,
120
121
  canvas,
121
- "Interval legend", config
122
+ label, config
122
123
  );
123
124
  return { labels, ...layout, background };
124
125
  }
125
126
 
126
- export const rematerializeIntervalLegend = /* @__PURE__ */ action(
127
+ export const rematerializeIntervalLegend = /* @__PURE__ */ closedAction(
127
128
  {
128
129
  op: "rematerializeIntervalLegend",
129
130
  description: "Rematerialize a discretized color interval legend."
130
- },
131
+ }, [],
131
132
  function (args = {}) {
132
- validateKeys(args, [], "rematerializeIntervalLegend");
133
133
  const stored = this.guideConfigs.legend?.interval;
134
134
  if (stored === undefined) {
135
135
  throw new Error("Interval legend requires stored configuration.");
@@ -1,4 +1,5 @@
1
- import { action } from "../../../../core/action.js";
1
+ import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
2
+ import { action, closedAction } from "../../../../core/action.js";
2
3
  import { isPlainObject } from "../../../../core/immutable.js";
3
4
  import { validateKeys } from "../../../../core/validation.js";
4
5
  import { mapLinearValues } from "../../../../grammar/scales/index.js";
@@ -94,7 +95,7 @@ function resolveOpacityLayout(program, config, scale) {
94
95
  config.labels.format
95
96
  );
96
97
  const symbolExtent = config.symbol.radius + (config.symbol.strokeWidth ?? 0) / 2;
97
- const labelWidths = texts.map(text => measureTextWidth(text, config.labels));
98
+ const labelWidths = texts.map(text => measureTextWidth(text, config.labels, program.materializationConfigs.textMetrics));
98
99
  let symbols;
99
100
  let labels;
100
101
  let title;
@@ -124,7 +125,7 @@ function resolveOpacityLayout(program, config, scale) {
124
125
  align: config.position === "right" ? "left" : "right"
125
126
  };
126
127
  } else if (config.titlePosition === "left") {
127
- const titleWidth = config.titleVisible === false ? 0 : measureTextWidth(config.title, config.titleStyle);
128
+ const titleWidth = config.titleVisible === false ? 0 : measureTextWidth(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
128
129
  const titlePrefix = config.titleVisible === false ? 0 : titleWidth + 20;
129
130
  const samplesWidth = labelWidths.reduce(
130
131
  (sum, width) => sum + symbolExtent * 2 + config.labels.offset + width,
@@ -187,9 +188,9 @@ function resolveOpacityLayout(program, config, scale) {
187
188
  title,
188
189
  config.title,
189
190
  config.titleStyle
190
- );
191
+ , program.materializationConfigs.textMetrics);
191
192
  const labelBounds = labels.map((label, index) =>
192
- resolveLegendTextBounds(label, texts[index], config.labels)
193
+ resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
193
194
  );
194
195
  const symbolBounds = symbols.map(symbol => ({
195
196
  left: symbol.x - symbolExtent,
@@ -212,13 +213,12 @@ function resolveOpacityLayout(program, config, scale) {
212
213
  return { values, texts, symbols, labels, title, background };
213
214
  }
214
215
 
215
- export const rematerializeOpacityLegend = /* @__PURE__ */ action(
216
+ export const rematerializeOpacityLegend = /* @__PURE__ */ closedAction(
216
217
  {
217
218
  op: "rematerializeOpacityLegend",
218
219
  description: "Rematerialize a field-opacity sample legend."
219
- },
220
+ }, [],
220
221
  function (args = {}) {
221
- validateKeys(args, [], "rematerializeOpacityLegend");
222
222
  const stored = this.guideConfigs.legend?.opacity;
223
223
  if (stored === undefined) {
224
224
  throw new Error("Opacity legend requires stored configuration.");
@@ -229,7 +229,7 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ action(
229
229
  const opacities = mapLinearValues(layout.values, scale.domain, scale.range, {
230
230
  clamp: scale.clamp ?? false
231
231
  });
232
- let next = this
232
+ let next = editGraphicProperties(editGraphicProperties(editGraphicProperties(this
233
233
  .editSemantic({
234
234
  property: "guide.legend.opacity.scale",
235
235
  value: encoding.scale
@@ -238,57 +238,20 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ action(
238
238
  property: "guide.legend.opacity.title",
239
239
  value: config.title
240
240
  })
241
- ._withLegendConfig("opacity", currentConfig)
242
- .editGraphics({
243
- target: "opacityLegendSymbols",
244
- property: "length",
245
- value: layout.values.length
246
- })
247
- .editGraphics({
248
- target: "opacityLegendSymbols",
249
- property: "x",
250
- value: layout.symbols.map(symbol => symbol.x)
251
- })
252
- .editGraphics({
253
- target: "opacityLegendSymbols",
254
- property: "y",
255
- value: layout.symbols.map(symbol => symbol.y)
256
- })
257
- .editGraphics({
258
- target: "opacityLegendSymbols",
259
- property: "radius",
260
- value: config.symbol.radius
261
- })
262
- .editGraphics({
263
- target: "opacityLegendSymbols",
264
- property: "fill",
265
- value: config.symbol.fill
266
- })
267
- .editGraphics({
268
- target: "opacityLegendSymbols",
269
- property: "opacity",
270
- value: opacities
271
- })
272
- .editGraphics({
273
- target: "opacityLegendLabels",
274
- property: "length",
275
- value: layout.values.length
276
- })
277
- .editGraphics({
278
- target: "opacityLegendLabels",
279
- property: "x",
280
- value: layout.labels.map(label => label.x)
281
- })
282
- .editGraphics({
283
- target: "opacityLegendLabels",
284
- property: "y",
285
- value: layout.labels.map(label => label.y)
286
- })
287
- .editGraphics({
288
- target: "opacityLegendLabels",
289
- property: "text",
290
- value: layout.texts
291
- });
241
+ ._withLegendConfig("opacity", currentConfig), "opacityLegendSymbols", {
242
+ length: layout.values.length,
243
+ x: layout.symbols.map(symbol => symbol.x),
244
+ y: layout.symbols.map(symbol => symbol.y)
245
+ }), "opacityLegendSymbols", {
246
+ radius: config.symbol.radius,
247
+ fill: config.symbol.fill,
248
+ opacity: opacities
249
+ }), "opacityLegendLabels", {
250
+ length: layout.values.length,
251
+ x: layout.labels.map(label => label.x),
252
+ y: layout.labels.map(label => label.y),
253
+ text: layout.texts
254
+ });
292
255
  next = editLegendBackground(
293
256
  next,
294
257
  "opacityLegendBackground",
@@ -316,22 +279,11 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ action(
316
279
  { align: layout.labels[0].align }
317
280
  );
318
281
  if (config.titleVisible === false) return next;
319
- next = next
320
- .editGraphics({
321
- target: "opacityLegendTitle",
322
- property: "x",
323
- value: layout.title.x
324
- })
325
- .editGraphics({
326
- target: "opacityLegendTitle",
327
- property: "y",
328
- value: layout.title.y
329
- })
330
- .editGraphics({
331
- target: "opacityLegendTitle",
332
- property: "text",
333
- value: config.title
334
- });
282
+ next = editGraphicProperties(next, "opacityLegendTitle", {
283
+ x: layout.title.x,
284
+ y: layout.title.y,
285
+ text: config.title
286
+ });
335
287
  return styleContinuousText(
336
288
  next,
337
289
  "opacityLegendTitle",
@@ -419,13 +371,12 @@ export const createOpacityLegend = /* @__PURE__ */ action(
419
371
  }
420
372
  );
421
373
 
422
- export const removeOpacityLegend = /* @__PURE__ */ action(
374
+ export const removeOpacityLegend = /* @__PURE__ */ closedAction(
423
375
  {
424
376
  op: "removeOpacityLegend",
425
377
  description: "Remove a field-opacity legend after switching to constant opacity."
426
- },
378
+ }, [],
427
379
  function (args = {}) {
428
- validateKeys(args, [], "removeOpacityLegend");
429
380
  if (this.guideConfigs.legend?.opacity === undefined) return this;
430
381
  const targets = [
431
382
  "opacityLegendBackground",