ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  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/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -0,0 +1,525 @@
1
+ import { action } from "../../../../core/action.js";
2
+ import { isPlainObject } from "../../../../core/immutable.js";
3
+ import { validateKeys, validateOptionObject } from
4
+ "../../../../core/validation.js";
5
+ import { formatDiscretizedIntervals } from
6
+ "../../../../grammar/scales/index.js";
7
+ import { inverseLerp, interpolateNumber } from
8
+ "../../../../grammar/numeric.js";
9
+ import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
10
+ import { resolveLegendGraphicPlacement } from
11
+ "../../../../materialization/graphicHierarchy.js";
12
+ import { mapScaleConsumerValues } from
13
+ "../../../../materialization/scales/map.js";
14
+ import { findLayer } from "../../../../selectors/layers.js";
15
+ import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
16
+ import {
17
+ assertLegendBoundsInsideCanvas,
18
+ editGraphicProperties,
19
+ editLegendBackground,
20
+ formatContinuousValues,
21
+ normalizeContinuousLegend,
22
+ requireResolvedLegendScale,
23
+ resolveContinuousBounds,
24
+ resolveContinuousColorLayer,
25
+ resolveLegendBackgroundFromBounds,
26
+ resolveLegendTextBounds,
27
+ sampleContinuousValues,
28
+ styleContinuousText,
29
+ validateNonNegative,
30
+ validatePositive
31
+ } from "./common.js";
32
+ import { DEFAULT_GRADIENT_SIZE } from "./gradient.js";
33
+ import { normalizeIntervalLegend } from "./interval.js";
34
+ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
35
+
36
+ const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
37
+
38
+ function requireChannels(args, operation) {
39
+ if (
40
+ !Array.isArray(args.channels) ||
41
+ args.channels.length !== 1 ||
42
+ args.channels[0] !== "stroke"
43
+ ) {
44
+ throw new Error(`${operation} requires channels: ["stroke"].`);
45
+ }
46
+ }
47
+
48
+ function strokeWidth(program, target) {
49
+ const config = program.markConfigs[target] ?? {};
50
+ return config.barAppearance?.strokeWidth ?? config.strokeWidth ?? ({
51
+ point: 1,
52
+ line: 2,
53
+ area: 1,
54
+ bar: 0.5,
55
+ rect: 1,
56
+ arc: 1,
57
+ rule: 2,
58
+ tick: 2
59
+ }[findLayer(program, target)?.mark?.type] ?? 1);
60
+ }
61
+
62
+ function sampleFill(program, target) {
63
+ const graphic = program.graphicSpec.objects[target];
64
+ const properties = graphic?.items?.[0]?.properties ?? graphic?.properties;
65
+ return properties?.fill ?? DEFAULT_COLORS.mark;
66
+ }
67
+
68
+ function resolveGradientConfig(program, stored) {
69
+ const layer = resolveContinuousColorLayer(program, stored.target, "stroke");
70
+ const encoding = layer.encoding.stroke;
71
+ if (!["quantitative", "temporal"].includes(encoding.fieldType)) {
72
+ throw new Error("Stroke gradient legend requires quantitative or temporal stroke.");
73
+ }
74
+ const scale = requireResolvedLegendScale(program, encoding.scale, "sequential");
75
+ return {
76
+ encoding,
77
+ scale,
78
+ config: {
79
+ ...stored,
80
+ target: layer.id,
81
+ scale: encoding.scale,
82
+ fieldType: encoding.fieldType,
83
+ title: stored.inferredTitle ? encoding.field : stored.title,
84
+ domain: scale.domain
85
+ }
86
+ };
87
+ }
88
+
89
+ function resolveGradientLayout(program, config, scale) {
90
+ const { plot, canvas } = resolveContinuousBounds(program);
91
+ const vertical = ["right", "left"].includes(config.position);
92
+ const { length, thickness } = config.gradient;
93
+ const titleGap = config.blockGap ?? 12;
94
+ let x;
95
+ let y;
96
+ if (config.position === "right") {
97
+ x = plot.x + plot.width + config.offset;
98
+ y = plot.y + 46 + titleGap - 12;
99
+ } else if (config.position === "left") {
100
+ x = plot.x - config.offset - thickness;
101
+ y = plot.y + 46 + titleGap - 12;
102
+ } else {
103
+ x = config.align === "left" ? plot.x
104
+ : config.align === "right" ? plot.x + plot.width - length
105
+ : plot.x + (plot.width - length) / 2;
106
+ y = config.position === "top"
107
+ ? plot.y - config.offset - thickness - config.labels.offset -
108
+ config.labels.fontSize
109
+ : plot.y + plot.height + config.offset +
110
+ (config.titleVisible === false ? 0 : config.titleStyle.fontSize + titleGap);
111
+ }
112
+ const title = vertical
113
+ ? { x, y: plot.y + 20, align: "left" }
114
+ : { x: x + length / 2, y: y - titleGap - config.titleStyle.fontSize / 2,
115
+ align: "center" };
116
+ const values = [...sampleContinuousValues(scale.domain, config.count)];
117
+ if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
118
+ values.push(scale.midpoint);
119
+ values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
120
+ }
121
+ const texts = formatContinuousValues(
122
+ values,
123
+ scale.domain,
124
+ config.fieldType,
125
+ config.labels.format
126
+ );
127
+ const fractions = values.map((value, index) => scale.midpoint === undefined
128
+ ? index / (values.length - 1)
129
+ : inverseLerp(value, ...scale.domain));
130
+ const labels = vertical
131
+ ? fractions.map(fraction => ({
132
+ x: config.position === "right"
133
+ ? x + thickness + config.labels.offset
134
+ : x - config.labels.offset,
135
+ y: y + length * (1 - fraction),
136
+ align: config.position === "right" ? "left" : "right"
137
+ }))
138
+ : fractions.map(fraction => ({
139
+ x: x + length * fraction,
140
+ y: y + thickness + config.labels.offset + config.labels.fontSize / 2,
141
+ align: "center"
142
+ }));
143
+ const ticks = vertical
144
+ ? labels.map(label => ({
145
+ x1: config.position === "right" ? x + thickness : x,
146
+ y1: label.y,
147
+ x2: config.position === "right" ? x + thickness + 6 : x - 6,
148
+ y2: label.y
149
+ }))
150
+ : labels.map(label => ({
151
+ x1: label.x,
152
+ y1: y + thickness,
153
+ x2: label.x,
154
+ y2: y + thickness + 6
155
+ }));
156
+ const occupied = [
157
+ { left: x, right: x + (vertical ? thickness : length), top: y,
158
+ bottom: y + (vertical ? length : thickness) },
159
+ ...ticks.map(tick => ({
160
+ left: Math.min(tick.x1, tick.x2) - 0.5,
161
+ right: Math.max(tick.x1, tick.x2) + 0.5,
162
+ top: Math.min(tick.y1, tick.y2) - 0.5,
163
+ bottom: Math.max(tick.y1, tick.y2) + 0.5
164
+ })),
165
+ ...labels.map((label, index) =>
166
+ resolveLegendTextBounds(label, texts[index], config.labels)),
167
+ ...(config.titleVisible === false ? [] : [
168
+ resolveLegendTextBounds(title, config.title, config.titleStyle)
169
+ ])
170
+ ];
171
+ assertLegendBoundsInsideCanvas(occupied, canvas, "Stroke gradient legend layout", config);
172
+ return {
173
+ vertical,
174
+ x,
175
+ y,
176
+ length,
177
+ thickness,
178
+ values,
179
+ texts,
180
+ labels,
181
+ ticks,
182
+ title,
183
+ background: resolveLegendBackgroundFromBounds(
184
+ occupied,
185
+ config.border,
186
+ canvas,
187
+ "Stroke gradient legend",
188
+ config
189
+ )
190
+ };
191
+ }
192
+
193
+ export function resolveStrokeGradientLegendCreation(program, args = {}) {
194
+ requireChannels(args, "Stroke gradient legend");
195
+ const config = normalizeContinuousLegend(args, "gradient");
196
+ if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
197
+ throw new TypeError("createLegend.gradient must be a plain object.");
198
+ }
199
+ validateKeys(args.gradient ?? {}, GRADIENT_OPTIONS, "createLegend.gradient");
200
+ config.gradient = {
201
+ length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
202
+ thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
203
+ };
204
+ config.titleVisible = true;
205
+ validatePositive(config.gradient.length, "Gradient length");
206
+ validatePositive(config.gradient.thickness, "Gradient thickness");
207
+ return resolveGradientConfig(program, config);
208
+ }
209
+
210
+ export const rematerializeStrokeGradientLegend = action(
211
+ {
212
+ op: "rematerializeStrokeGradientLegend",
213
+ description: "Rematerialize a continuous stroke gradient legend."
214
+ },
215
+ function (args = {}) {
216
+ validateKeys(args, [], "rematerializeStrokeGradientLegend");
217
+ const stored = this.guideConfigs.legend?.strokeGradient;
218
+ if (stored === undefined) {
219
+ throw new Error("Stroke gradient legend requires stored configuration.");
220
+ }
221
+ const { encoding, scale, config: currentConfig } = resolveGradientConfig(this, stored);
222
+ const config = resolveEffectiveLegendBlockConfig(this, "strokeGradient", currentConfig);
223
+ const layout = resolveGradientLayout(this, config, scale);
224
+ const fill = sampleFill(this, config.target);
225
+ const width = strokeWidth(this, config.target);
226
+ const stripCount = 60;
227
+ const stripSize = layout.length / stripCount;
228
+ const strips = Array.from({ length: stripCount }, (_, index) => {
229
+ const fraction = (index + 0.5) / stripCount;
230
+ const position = layout.vertical ? 1 - fraction : fraction;
231
+ const samplingScale = scale.midpoint === undefined
232
+ ? { ...scale, domain: [0, 1] }
233
+ : scale;
234
+ const value = scale.midpoint === undefined
235
+ ? position
236
+ : interpolateNumber(...scale.domain, position);
237
+ const [color] = mapScaleConsumerValues([value], samplingScale, "stroke");
238
+ return {
239
+ x: layout.x + (layout.vertical ? 0 : index * stripSize),
240
+ y: layout.y + (layout.vertical ? index * stripSize : 0),
241
+ width: layout.vertical ? layout.thickness : stripSize,
242
+ height: layout.vertical ? stripSize : layout.thickness,
243
+ fill,
244
+ stroke: color,
245
+ strokeWidth: width
246
+ };
247
+ });
248
+ let next = this
249
+ .editSemantic({ property: "guide.legend.stroke.scale", value: encoding.scale })
250
+ .editSemantic({ property: "guide.legend.stroke.title", value: config.title })
251
+ ._withLegendConfig("strokeGradient", currentConfig);
252
+ next = editLegendBackground(
253
+ next,
254
+ "strokeGradientBackground",
255
+ layout.background,
256
+ config.border
257
+ );
258
+ next = editGraphicProperties(next, "strokeGradientStrips", {
259
+ length: strips.length,
260
+ x: strips.map(strip => strip.x),
261
+ y: strips.map(strip => strip.y),
262
+ width: strips.map(strip => strip.width),
263
+ height: strips.map(strip => strip.height),
264
+ fill: strips.map(strip => strip.fill),
265
+ stroke: strips.map(strip => strip.stroke),
266
+ strokeWidth: strips.map(strip => strip.strokeWidth)
267
+ });
268
+ next = editGraphicProperties(next, "strokeGradientTicks", {
269
+ length: layout.ticks.length,
270
+ x1: layout.ticks.map(tick => tick.x1),
271
+ y1: layout.ticks.map(tick => tick.y1),
272
+ x2: layout.ticks.map(tick => tick.x2),
273
+ y2: layout.ticks.map(tick => tick.y2),
274
+ stroke: DEFAULT_COLORS.mutedText,
275
+ strokeWidth: 1
276
+ });
277
+ next = editGraphicProperties(next, "strokeGradientLabels", {
278
+ length: layout.labels.length,
279
+ x: layout.labels.map(label => label.x),
280
+ y: layout.labels.map(label => label.y),
281
+ text: layout.texts
282
+ });
283
+ next = styleContinuousText(next, "strokeGradientLabels", config.labels, {
284
+ align: layout.labels[0].align
285
+ });
286
+ if (config.titleVisible === false) return next;
287
+ next = editGraphicProperties(next, "strokeGradientTitle", {
288
+ x: layout.title.x,
289
+ y: layout.title.y,
290
+ text: config.title
291
+ });
292
+ return styleContinuousText(next, "strokeGradientTitle", config.titleStyle, {
293
+ align: layout.title.align
294
+ });
295
+ }
296
+ );
297
+
298
+ export function createStrokeGradientLegendFromConfig(program, config) {
299
+ const resolved = resolveGradientConfig(program, config);
300
+ resolveGradientLayout(program, resolved.config, resolved.scale);
301
+ let next = program
302
+ .editSemantic({
303
+ property: "guide.legend.stroke.scale",
304
+ value: resolved.encoding.scale
305
+ })
306
+ .editSemantic({
307
+ property: "guide.legend.stroke.title",
308
+ value: resolved.config.title
309
+ })
310
+ ._withLegendConfig("strokeGradient", resolved.config);
311
+ const placement = resolveLegendGraphicPlacement(next);
312
+ if (resolved.config.border !== false) {
313
+ next = next.createGraphics({
314
+ id: "strokeGradientBackground",
315
+ type: "rect",
316
+ ...placement
317
+ });
318
+ }
319
+ next = next
320
+ .createGraphics({
321
+ id: "strokeGradientStrips",
322
+ type: "rect",
323
+ length: 0,
324
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
325
+ ? {}
326
+ : { after: "strokeGradientBackground" })
327
+ })
328
+ .createGraphics({ id: "strokeGradientTicks", type: "line", length: 0,
329
+ ...resolveLegendGraphicPlacement(next) })
330
+ .createGraphics({ id: "strokeGradientLabels", type: "text", length: 0,
331
+ ...resolveLegendGraphicPlacement(next) });
332
+ if (resolved.config.titleVisible !== false) {
333
+ next = next.createGraphics({ id: "strokeGradientTitle", type: "text",
334
+ ...resolveLegendGraphicPlacement(next) });
335
+ }
336
+ return next.rematerializeStrokeGradientLegend();
337
+ }
338
+
339
+ export const createStrokeGradientLegend = action(
340
+ {
341
+ op: "createStrokeGradientLegend",
342
+ description: "Create a continuous stroke gradient legend."
343
+ },
344
+ function (args = {}) {
345
+ const resolved = resolveStrokeGradientLegendCreation(this, args);
346
+ if (this.guideConfigs.legend?.strokeGradient !== undefined) {
347
+ throw new Error("createStrokeGradientLegend requires a missing stroke legend.");
348
+ }
349
+ return createStrokeGradientLegendFromConfig(this, resolved.config);
350
+ }
351
+ );
352
+
353
+ export function resolveStrokeIntervalConfig(program, stored) {
354
+ const layer = resolveContinuousColorLayer(program, stored.target, "stroke");
355
+ const encoding = layer.encoding.stroke;
356
+ if (encoding.fieldType !== "quantitative") {
357
+ throw new Error("Stroke interval legend requires quantitative stroke.");
358
+ }
359
+ const scale = program.resolvedScales[encoding.scale];
360
+ if (!["quantize", "quantile", "threshold"].includes(scale?.type)) {
361
+ throw new Error(
362
+ `Stroke interval legend requires a resolved discretized scale "${encoding.scale}".`
363
+ );
364
+ }
365
+ return { encoding, scale, config: {
366
+ ...stored,
367
+ target: layer.id,
368
+ scale: encoding.scale,
369
+ title: stored.inferredTitle ? encoding.field : stored.title,
370
+ ...(stored.inferredStrokeWidth === true ? {
371
+ symbol: { ...stored.symbol, strokeWidth: strokeWidth(program, layer.id) }
372
+ } : {})
373
+ } };
374
+ }
375
+
376
+ export function resolveStrokeIntervalLayout(program, config, scale) {
377
+ const { plot, canvas } = resolveContinuousBounds(program);
378
+ const labels = formatDiscretizedIntervals(scale.thresholds, config.labels.format);
379
+ const layout = resolveLegendItemLayout(plot, config, labels, config.symbol);
380
+ assertLegendBoundsInsideCanvas(
381
+ layout.bounds,
382
+ canvas,
383
+ "Stroke interval legend layout",
384
+ config
385
+ );
386
+ return {
387
+ labels,
388
+ ...layout,
389
+ background: resolveLegendBackgroundFromBounds(
390
+ layout.bounds,
391
+ config.border,
392
+ canvas,
393
+ "Stroke interval legend",
394
+ config
395
+ )
396
+ };
397
+ }
398
+
399
+ export function resolveStrokeIntervalLegendCreation(program, args = {}) {
400
+ requireChannels(args, "Stroke interval legend");
401
+ const config = normalizeIntervalLegend({ ...args, channels: undefined });
402
+ const layer = resolveContinuousColorLayer(program, config.target, "stroke");
403
+ config.symbol = {
404
+ ...config.symbol,
405
+ ...(args.symbol?.strokeWidth === undefined
406
+ ? { strokeWidth: strokeWidth(program, layer.id) }
407
+ : {})
408
+ };
409
+ config.inferredStrokeWidth = args.symbol?.strokeWidth === undefined;
410
+ validateNonNegative(config.symbol.strokeWidth, "Legend symbol strokeWidth");
411
+ return resolveStrokeIntervalConfig(program, config);
412
+ }
413
+
414
+ export const rematerializeStrokeIntervalLegend = action(
415
+ {
416
+ op: "rematerializeStrokeIntervalLegend",
417
+ description: "Rematerialize a discretized stroke interval legend."
418
+ },
419
+ function (args = {}) {
420
+ validateKeys(args, [], "rematerializeStrokeIntervalLegend");
421
+ const stored = this.guideConfigs.legend?.strokeInterval;
422
+ if (stored === undefined) {
423
+ throw new Error("Stroke interval legend requires stored configuration.");
424
+ }
425
+ const { encoding, scale, config: currentConfig } = resolveStrokeIntervalConfig(this, stored);
426
+ const config = resolveEffectiveLegendBlockConfig(this, "strokeInterval", currentConfig);
427
+ const layout = resolveStrokeIntervalLayout(this, config, scale);
428
+ const fill = config.blockSymbol?.fill ?? sampleFill(this, config.target);
429
+ let next = this
430
+ .editSemantic({ property: "guide.legend.stroke.scale", value: encoding.scale })
431
+ .editSemantic({ property: "guide.legend.stroke.title", value: config.title })
432
+ ._withLegendConfig("strokeInterval", currentConfig);
433
+ next = editGraphicProperties(next, "strokeIntervalSymbols", {
434
+ length: scale.range.length,
435
+ x: layout.symbolX,
436
+ y: layout.itemY.map(value => value - config.symbol.height / 2),
437
+ width: config.symbol.width,
438
+ height: config.symbol.height,
439
+ fill,
440
+ stroke: scale.range,
441
+ strokeWidth: config.symbol.strokeWidth,
442
+ ...(config.blockSymbol?.opacity === undefined
443
+ ? {}
444
+ : { opacity: config.blockSymbol.opacity })
445
+ });
446
+ next = editGraphicProperties(next, "strokeIntervalLabels", {
447
+ length: layout.labels.length,
448
+ x: layout.labelX,
449
+ y: layout.itemY,
450
+ text: layout.labels
451
+ });
452
+ next = editLegendBackground(
453
+ next,
454
+ "strokeIntervalBackground",
455
+ layout.background,
456
+ config.border
457
+ );
458
+ next = styleContinuousText(next, "strokeIntervalLabels", config.labels);
459
+ if (config.titleVisible === false) return next;
460
+ next = editGraphicProperties(next, "strokeIntervalTitle", {
461
+ x: layout.title.x,
462
+ y: layout.title.y,
463
+ text: config.title
464
+ });
465
+ return styleContinuousText(next, "strokeIntervalTitle", config.titleStyle, {
466
+ align: layout.title.align
467
+ });
468
+ }
469
+ );
470
+
471
+ export function createStrokeIntervalLegendFromConfig(program, config) {
472
+ const resolved = resolveStrokeIntervalConfig(program, config);
473
+ resolveStrokeIntervalLayout(program, resolved.config, resolved.scale);
474
+ let next = program
475
+ .editSemantic({
476
+ property: "guide.legend.stroke.scale",
477
+ value: resolved.encoding.scale
478
+ })
479
+ .editSemantic({
480
+ property: "guide.legend.stroke.title",
481
+ value: resolved.config.title
482
+ })
483
+ ._withLegendConfig("strokeInterval", resolved.config);
484
+ const placement = resolveLegendGraphicPlacement(next);
485
+ if (resolved.config.border !== false) {
486
+ next = next.createGraphics({ id: "strokeIntervalBackground", type: "rect",
487
+ ...placement });
488
+ }
489
+ next = next
490
+ .createGraphics({ id: "strokeIntervalSymbols", type: "rect", length: 0,
491
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
492
+ ? {}
493
+ : { after: "strokeIntervalBackground" }) })
494
+ .createGraphics({ id: "strokeIntervalLabels", type: "text", length: 0,
495
+ ...placement });
496
+ if (resolved.config.titleVisible !== false) {
497
+ next = next.createGraphics({ id: "strokeIntervalTitle", type: "text",
498
+ ...placement });
499
+ }
500
+ return next.rematerializeStrokeIntervalLegend();
501
+ }
502
+
503
+ export const createStrokeIntervalLegend = action(
504
+ {
505
+ op: "createStrokeIntervalLegend",
506
+ description: "Create a discretized stroke interval legend."
507
+ },
508
+ function (args = {}) {
509
+ validateOptionObject(args, undefined, "createStrokeIntervalLegend");
510
+ const resolved = resolveStrokeIntervalLegendCreation(this, args);
511
+ if (this.guideConfigs.legend?.strokeInterval !== undefined) {
512
+ throw new Error("createStrokeIntervalLegend requires a missing stroke legend.");
513
+ }
514
+ return createStrokeIntervalLegendFromConfig(this, resolved.config);
515
+ }
516
+ );
517
+
518
+ export function registerStrokeColorLegendActions(ProgramClass) {
519
+ Object.assign(ProgramClass.prototype, {
520
+ createStrokeGradientLegend,
521
+ rematerializeStrokeGradientLegend,
522
+ createStrokeIntervalLegend,
523
+ rematerializeStrokeIntervalLegend
524
+ });
525
+ }
@@ -4,6 +4,10 @@ import { resolveOpacityLegendCreation } from "./continuous/opacity.js";
4
4
  import { normalizeIntervalLegend, resolveIntervalConfig } from "./continuous/interval.js";
5
5
  import { resolveSizeLegendConfig } from "./size.js";
6
6
  import { resolveStrokeWidthLegendConfig } from "./strokeWidth.js";
7
+ import {
8
+ resolveStrokeGradientLegendCreation,
9
+ resolveStrokeIntervalLegendCreation
10
+ } from "./continuous/stroke.js";
7
11
 
8
12
  export function resolveLegendStepConfig(program, step) {
9
13
  switch (step.op) {
@@ -21,7 +25,16 @@ export function resolveLegendStepConfig(program, step) {
21
25
  return { kind: "size", config: resolveSizeLegendConfig(program, step.args) };
22
26
  case "createStrokeWidthLegend":
23
27
  return { kind: "strokeWidth", config: resolveStrokeWidthLegendConfig(program, step.args) };
28
+ case "createStrokeGradientLegend":
29
+ return {
30
+ kind: "strokeGradient",
31
+ config: resolveStrokeGradientLegendCreation(program, step.args).config
32
+ };
33
+ case "createStrokeIntervalLegend":
34
+ return {
35
+ kind: "strokeInterval",
36
+ config: resolveStrokeIntervalLegendCreation(program, step.args).config
37
+ };
24
38
  default: throw new Error(`Unsupported legend creation owner "${step.op}".`);
25
39
  }
26
40
  }
27
-