ggaction 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,314 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { isPlainObject } from "../../../core/immutable.js";
4
+ import { validateKeys } from "../../../core/validation.js";
5
+ import { normalizeOptions } from "./categorical/options.js";
6
+ import { symbolGraphic } from "./categorical/layout.js";
7
+ import {
8
+ normalizeContinuousLegend,
9
+ validatePositive
10
+ } from "./continuous/common.js";
11
+ import { normalizeOpacitySymbol } from "./continuous/opacity.js";
12
+ import { normalizeIntervalLegend } from "./continuous/interval.js";
13
+ import { findLayer } from "../../../selectors/layers.js";
14
+ import { resolveLegendGraphicPlacement } from
15
+ "../../../materialization/graphicHierarchy.js";
16
+
17
+ const OPTIONS = Object.freeze([
18
+ "target", "position", "align", "direction", "columns", "offset",
19
+ "titlePosition", "title", "symbol", "labels", "titleStyle", "itemGap",
20
+ "border", "count", "gradient"
21
+ ]);
22
+
23
+ function mergeObject(previous, patch) {
24
+ return patch === undefined
25
+ ? previous
26
+ : { ...previous, ...patch };
27
+ }
28
+
29
+ function mergeBorder(previous, patch) {
30
+ if (patch === undefined) return previous;
31
+ if (patch === false || patch === true) return patch;
32
+ return previous === false ? patch : { ...previous, ...patch };
33
+ }
34
+
35
+ function resolveTarget(program, requested) {
36
+ const configs = program.guideConfigs.legend ?? {};
37
+ const targets = [...new Set(Object.values(configs)
38
+ .map(config => config?.target)
39
+ .filter(Boolean))];
40
+ if (requested !== undefined) {
41
+ const target = validateUserId(requested, "Legend target id");
42
+ if (!targets.includes(target)) {
43
+ throw new Error(`Unknown legend target "${target}".`);
44
+ }
45
+ return target;
46
+ }
47
+ if (targets.length !== 1) {
48
+ throw new Error("editLegend requires target when the legend is ambiguous.");
49
+ }
50
+ return targets[0];
51
+ }
52
+
53
+ function reconcileGraphic(program, id, shouldExist, definition) {
54
+ const exists = program.graphicSpec.objects[id] !== undefined;
55
+ if (exists && !shouldExist) return program.editGraphics({ target: id, remove: true });
56
+ if (!exists && shouldExist) {
57
+ return program.createGraphics({
58
+ id,
59
+ ...definition,
60
+ ...resolveLegendGraphicPlacement(program)
61
+ });
62
+ }
63
+ return program;
64
+ }
65
+
66
+ function editContinuous(program, kind, previous, args) {
67
+ const allowed = kind === "gradient"
68
+ ? ["target", "position", "align", "offset", "title", "labels",
69
+ "titleStyle", "border", "count", "gradient"]
70
+ : ["target", "position", "align", "offset", "title", "symbol", "labels",
71
+ "titleStyle", "itemGap", "border", "count"];
72
+ for (const key of Object.keys(args)) {
73
+ if (!allowed.includes(key)) throw new Error(`${kind} legend does not accept ${key}.`);
74
+ }
75
+ const titleMode = args.title;
76
+ const inferredTitle = titleMode === "auto"
77
+ ? true
78
+ : typeof titleMode === "string" ? false : previous.inferredTitle;
79
+ const titleVisible = titleMode === false ? false
80
+ : titleMode === undefined ? previous.titleVisible !== false : true;
81
+ const layer = findLayer(program, previous.target);
82
+ const inferred = layer?.encoding?.[kind === "gradient" ? "color" : "opacity"]?.field;
83
+ const title = titleMode === "auto"
84
+ ? inferred
85
+ : typeof titleMode === "string" ? titleMode : previous.title;
86
+ const normalized = normalizeContinuousLegend({
87
+ target: previous.target,
88
+ position: args.position ?? previous.position,
89
+ align: args.align ?? previous.align,
90
+ offset: args.offset ?? previous.offset,
91
+ count: args.count ?? previous.count,
92
+ title,
93
+ labels: mergeObject(previous.labels, args.labels),
94
+ titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
95
+ border: mergeBorder(previous.border, args.border),
96
+ ...(kind === "opacity" ? {
97
+ itemGap: args.itemGap ?? previous.itemGap,
98
+ symbol: args.symbol ?? previous.symbol
99
+ } : {})
100
+ }, kind);
101
+ const config = {
102
+ ...previous,
103
+ ...normalized,
104
+ inferredTitle,
105
+ titleVisible
106
+ };
107
+ if (kind === "gradient") {
108
+ const gradient = mergeObject(previous.gradient, args.gradient);
109
+ validatePositive(gradient.length, "Gradient length");
110
+ validatePositive(gradient.thickness, "Gradient thickness");
111
+ config.gradient = gradient;
112
+ } else {
113
+ config.symbol = normalizeOpacitySymbol(args.symbol ?? previous.symbol);
114
+ }
115
+ const prefix = kind === "gradient" ? "colorGradient" : "opacityLegend";
116
+ let next = program;
117
+ if (titleMode === "auto" || typeof titleMode === "string") {
118
+ next = next.editSemantic({
119
+ property: `guide.legend.${kind === "gradient" ? "color" : "opacity"}.title`,
120
+ value: title
121
+ });
122
+ }
123
+ next = next._withLegendConfig(kind, config);
124
+ next = reconcileGraphic(next, `${prefix}Background`, config.border !== false, {
125
+ type: "rect",
126
+ before: kind === "gradient" ? "colorGradientStrips" : "opacityLegendSymbols"
127
+ });
128
+ next = reconcileGraphic(next, `${prefix}Title`, titleVisible, { type: "text" });
129
+ return kind === "gradient"
130
+ ? next.rematerializeGradientLegend()
131
+ : next.rematerializeOpacityLegend();
132
+ }
133
+
134
+ function editInterval(program, previous, args) {
135
+ for (const key of Object.keys(args)) {
136
+ if (![
137
+ "target", "position", "align", "direction", "offset", "title",
138
+ "symbol", "labels", "titleStyle", "itemGap", "border"
139
+ ].includes(key)) {
140
+ throw new Error(`interval legend does not accept ${key}.`);
141
+ }
142
+ }
143
+ const titleMode = args.title;
144
+ const layer = findLayer(program, previous.target);
145
+ const inferredTitle = titleMode === "auto"
146
+ ? true
147
+ : typeof titleMode === "string" ? false : previous.inferredTitle;
148
+ const titleVisible = titleMode === false
149
+ ? false
150
+ : titleMode === undefined ? previous.titleVisible !== false : true;
151
+ const title = titleMode === "auto"
152
+ ? layer?.encoding?.color?.field
153
+ : typeof titleMode === "string" ? titleMode : previous.title;
154
+ const normalized = normalizeIntervalLegend({
155
+ target: previous.target,
156
+ position: args.position ?? previous.position,
157
+ align: args.align ?? previous.align,
158
+ direction: args.direction ?? previous.direction,
159
+ offset: args.offset ?? previous.offset,
160
+ title,
161
+ symbol: mergeObject(previous.symbol, args.symbol),
162
+ labels: mergeObject(previous.labels, args.labels),
163
+ titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
164
+ itemGap: args.itemGap ?? previous.itemGap,
165
+ border: mergeBorder(previous.border, args.border)
166
+ });
167
+ let next = program._withLegendConfig("interval", {
168
+ ...previous,
169
+ ...normalized,
170
+ inferredTitle,
171
+ titleVisible
172
+ });
173
+ if (titleMode === "auto" || typeof titleMode === "string") {
174
+ next = next.editSemantic({
175
+ property: "guide.legend.color.title",
176
+ value: title
177
+ });
178
+ }
179
+ next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
180
+ type: "text"
181
+ });
182
+ return next.rematerializeIntervalLegend();
183
+ }
184
+
185
+ function categoricalSymbolIds(config) {
186
+ return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
187
+ }
188
+
189
+ function editCategorical(program, kind, previous, size, args) {
190
+ if (args.gradient !== undefined) {
191
+ throw new Error("Categorical legends do not accept gradient.");
192
+ }
193
+ if (args.count !== undefined && size === undefined) {
194
+ throw new Error("Legend count requires an existing size legend.");
195
+ }
196
+ const titleMode = args.title;
197
+ const inferredTitle = titleMode === "auto"
198
+ ? true
199
+ : typeof titleMode === "string" ? false : previous.inferredTitle;
200
+ const titleVisible = titleMode === false ? false
201
+ : titleMode === undefined ? previous.titleVisible !== false : true;
202
+ const title = titleMode === "auto"
203
+ ? previous.field
204
+ : typeof titleMode === "string" ? titleMode : previous.title;
205
+ const normalized = normalizeOptions({
206
+ target: previous.target,
207
+ channels: previous.channels,
208
+ position: args.position ?? previous.position,
209
+ align: args.align ?? previous.align,
210
+ direction: args.direction ?? (args.position === "left"
211
+ ? "vertical"
212
+ : previous.direction),
213
+ ...(args.columns === undefined && previous.columns === undefined
214
+ ? {} : { columns: args.columns ?? previous.columns }),
215
+ offset: args.offset ?? previous.offset,
216
+ titlePosition: args.titlePosition ?? previous.titlePosition,
217
+ title,
218
+ symbol: args.symbol ?? previous.symbol,
219
+ labels: mergeObject(previous.labels, args.labels),
220
+ titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
221
+ itemGap: args.itemGap ?? previous.itemGap,
222
+ border: mergeBorder(previous.border, args.border)
223
+ }, kind);
224
+ const config = {
225
+ ...previous,
226
+ ...normalized,
227
+ inferredTitle,
228
+ titleVisible
229
+ };
230
+ const oldSymbols = categoricalSymbolIds(previous);
231
+ const newSymbols = categoricalSymbolIds(config);
232
+ let next = program;
233
+ if (titleMode === "auto" || typeof titleMode === "string") {
234
+ next = next.editSemantic({
235
+ property: `guide.legend.${kind}.title`,
236
+ value: title
237
+ });
238
+ }
239
+ next = next._withLegendConfig(kind, config);
240
+ for (const id of oldSymbols) {
241
+ if (!newSymbols.has(id) && next.graphicSpec.objects[id] !== undefined) {
242
+ next = next.editGraphics({ target: id, remove: true });
243
+ }
244
+ }
245
+ next = reconcileGraphic(next, `${kind === "series" ? "series" : "color"}LegendBackground`,
246
+ config.border !== false, {
247
+ type: "rect",
248
+ before: [...newSymbols][0]
249
+ });
250
+ for (const layer of config.symbol.layers) {
251
+ const id = symbolGraphic(config, layer.type);
252
+ if (next.graphicSpec.objects[id] === undefined) {
253
+ const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[layer.type];
254
+ next = next[`createLegendSymbol${suffix}`]();
255
+ }
256
+ }
257
+ const titleId = `${kind === "series" ? "series" : "color"}LegendTitle`;
258
+ next = reconcileGraphic(next, titleId, titleVisible, {
259
+ type: "text",
260
+ ...(next.graphicSpec.objects.sizeLegendSymbols === undefined
261
+ ? {} : { before: "sizeLegendSymbols" })
262
+ });
263
+ if (size !== undefined) {
264
+ if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 2)) {
265
+ throw new RangeError("Size legend count must be an integer of at least 2.");
266
+ }
267
+ next = next._withLegendConfig("size", {
268
+ ...size,
269
+ count: args.count ?? size.count,
270
+ inheritAppearance: true
271
+ });
272
+ }
273
+ return next.rematerializeLegend();
274
+ }
275
+
276
+ export const editLegend = action(
277
+ { op: "editLegend", description: "Edit one stable legend layout or appearance." },
278
+ function (args = {}) {
279
+ if (!isPlainObject(args)) {
280
+ throw new TypeError("editLegend options must be a plain object.");
281
+ }
282
+ validateKeys(args, OPTIONS, "editLegend");
283
+ const changes = Object.keys(args).filter(key => key !== "target");
284
+ if (changes.length === 0) {
285
+ throw new Error("editLegend requires at least one change.");
286
+ }
287
+ if (args.title !== undefined && args.title !== false && args.title !== "auto" && (
288
+ typeof args.title !== "string" || args.title.length === 0
289
+ )) {
290
+ throw new TypeError('editLegend title must be a non-empty string, "auto", or false.');
291
+ }
292
+ const target = resolveTarget(this, args.target);
293
+ const configs = this.guideConfigs.legend ?? {};
294
+ const categoricalKind = ["series", "color"].find(
295
+ kind => configs[kind]?.target === target
296
+ );
297
+ if (categoricalKind !== undefined) {
298
+ return editCategorical(
299
+ this,
300
+ categoricalKind,
301
+ configs[categoricalKind],
302
+ configs.size?.target === target ? configs.size : undefined,
303
+ args
304
+ );
305
+ }
306
+ if (configs.interval?.target === target) {
307
+ return editInterval(this, configs.interval, args);
308
+ }
309
+ const continuousKind = ["gradient", "opacity"].find(
310
+ kind => configs[kind]?.target === target
311
+ );
312
+ return editContinuous(this, continuousKind, configs[continuousKind], args);
313
+ }
314
+ );
@@ -0,0 +1,9 @@
1
+ import { registerCategoricalLegendActions } from "./categorical/index.js";
2
+ import { registerContinuousLegendActions } from "./continuous/index.js";
3
+ import { registerSizeLegendActions } from "./size.js";
4
+
5
+ export function registerLegendActions(ProgramClass) {
6
+ registerCategoricalLegendActions(ProgramClass);
7
+ registerContinuousLegendActions(ProgramClass);
8
+ registerSizeLegendActions(ProgramClass);
9
+ }
@@ -0,0 +1,220 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateKeys } from "../../../core/validation.js";
3
+ import { mapLinearValues } from "../../../grammar/scales.js";
4
+ import { resolveGraphicBounds } from "../../../layout/canvas.js";
5
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
6
+ "../../../theme/defaults.js";
7
+ import { resolveLayout as resolveCategoricalLayout } from
8
+ "./categorical/layout.js";
9
+ import { findLayer } from "../../../selectors/layers.js";
10
+ import { resolveLegendGraphicPlacement } from
11
+ "../../../materialization/graphicHierarchy.js";
12
+
13
+ const SIZE_OPTIONS = Object.freeze(["target", "count"]);
14
+
15
+ function resolvePoint(program, requested) {
16
+ const candidates = program.semanticSpec.layers.filter(layer =>
17
+ layer.mark?.type === "point" &&
18
+ layer.encoding?.color?.scale !== undefined &&
19
+ layer.encoding?.shape?.scale !== undefined
20
+ );
21
+ const layer = requested === undefined
22
+ ? candidates.length === 1 ? candidates[0] : undefined
23
+ : (() => {
24
+ const candidate = findLayer(program, requested);
25
+ return candidates.includes(candidate) ? candidate : undefined;
26
+ })();
27
+ if (layer === undefined) {
28
+ throw new Error(
29
+ requested === undefined
30
+ ? "Point series legend requires one eligible point mark."
31
+ : `Unknown point series legend target "${requested}".`
32
+ );
33
+ }
34
+ if (layer.encoding.color.field !== layer.encoding.shape.field) {
35
+ throw new Error("Point color and shape legends require the same field.");
36
+ }
37
+ return layer;
38
+ }
39
+
40
+ function requireScale(program, id, type) {
41
+ const scale = program.resolvedScales[id];
42
+ if (scale?.type !== type) {
43
+ throw new Error(`Legend requires resolved ${type} scale "${id}".`);
44
+ }
45
+ return scale;
46
+ }
47
+
48
+ function bounds(program) {
49
+ const value = resolveGraphicBounds(program);
50
+ const canvas = program.graphicSpec.objects.canvas;
51
+ if (
52
+ value === undefined ||
53
+ ![value.x, value.y, value.width, value.height].every(Number.isFinite) ||
54
+ !Number.isFinite(canvas?.properties.width)
55
+ ) {
56
+ throw new Error("Legend layout requires Canvas bounds and width.");
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function styleText(program, id, { title = false, style } = {}) {
62
+ const resolved = style ?? {
63
+ color: title ? DEFAULT_COLORS.strongText : DEFAULT_COLORS.text,
64
+ fontSize: title ? 13 : 12,
65
+ fontFamily: DEFAULT_FONT_FAMILY,
66
+ fontWeight: title ? 600 : "normal"
67
+ };
68
+ return program
69
+ .editGraphics({
70
+ target: id,
71
+ property: "fill",
72
+ value: resolved.color
73
+ })
74
+ .editGraphics({ target: id, property: "fontSize", value: resolved.fontSize })
75
+ .editGraphics({ target: id, property: "fontFamily", value: resolved.fontFamily })
76
+ .editGraphics({ target: id, property: "fontWeight", value: resolved.fontWeight })
77
+ .editGraphics({ target: id, property: "textAlign", value: "left" })
78
+ .editGraphics({ target: id, property: "textBaseline", value: "middle" });
79
+ }
80
+
81
+ export const rematerializeSizeLegend = action(
82
+ {
83
+ op: "rematerializeSizeLegend",
84
+ description: "Rematerialize a quantitative point-size legend."
85
+ },
86
+ function (args = {}) {
87
+ validateKeys(args, [], "rematerializeSizeLegend");
88
+ const config = this.guideConfigs.legend?.size;
89
+ if (config === undefined) throw new Error("Size legend requires stored configuration.");
90
+ const layer = findLayer(this, config.target);
91
+ const encoding = layer?.encoding?.size;
92
+ if (encoding?.scale === undefined) {
93
+ throw new Error("Size legend target requires a size encoding.");
94
+ }
95
+ const scale = requireScale(this, encoding.scale, "linear");
96
+ const title = config.inferredTitle === true ? encoding.field : config.title;
97
+ const currentConfig = {
98
+ ...config,
99
+ scale: encoding.scale,
100
+ title,
101
+ domain: scale.domain
102
+ };
103
+ const plot = bounds(this);
104
+ const categorical = this.guideConfigs.legend?.series ??
105
+ this.guideConfigs.legend?.color;
106
+ const leftLayout = categorical?.position === "left"
107
+ ? resolveCategoricalLayout(this, categorical).size
108
+ : undefined;
109
+ const originX = leftLayout?.titleX ?? plot.x + plot.width + 30;
110
+ const seriesCount = categorical?.domain.length ?? 0;
111
+ const titleY = leftLayout?.titleY ?? plot.y + 56 + seriesCount * 34 + 22;
112
+ const values = Array.from(
113
+ { length: currentConfig.count },
114
+ (_, index) => scale.domain[0] +
115
+ index / (currentConfig.count - 1) * (scale.domain[1] - scale.domain[0])
116
+ );
117
+ const areas = mapLinearValues(values, scale.domain, scale.range, {
118
+ clamp: scale.clamp ?? false
119
+ });
120
+ const itemY = leftLayout?.itemY ??
121
+ values.map((_, index) => titleY + 34 + index * 40);
122
+ const symbolX = leftLayout?.symbolX ?? values.map(() => originX + 16);
123
+ const labelX = leftLayout?.labelX ?? values.map(() => originX + 44);
124
+ let next = this
125
+ .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
126
+ .editSemantic({ property: "guide.legend.size.title", value: title })
127
+ ._withLegendConfig("size", currentConfig)
128
+ .editGraphics({ target: "sizeLegendSymbols", property: "length", value: values.length })
129
+ .editGraphics({ target: "sizeLegendSymbols", property: "x", value: symbolX })
130
+ .editGraphics({ target: "sizeLegendSymbols", property: "y", value: itemY })
131
+ .editGraphics({
132
+ target: "sizeLegendSymbols",
133
+ property: "radius",
134
+ value: areas.map(area => Math.sqrt(area / Math.PI))
135
+ })
136
+ .editGraphics({
137
+ target: "sizeLegendSymbols",
138
+ property: "fill",
139
+ value: DEFAULT_COLORS.sizeSymbol
140
+ })
141
+ .editGraphics({ target: "sizeLegendSymbols", property: "opacity", value: 0.7 })
142
+ .editGraphics({ target: "sizeLegendLabels", property: "length", value: values.length })
143
+ .editGraphics({ target: "sizeLegendLabels", property: "x", value: labelX })
144
+ .editGraphics({ target: "sizeLegendLabels", property: "y", value: itemY })
145
+ .editGraphics({
146
+ target: "sizeLegendLabels",
147
+ property: "text",
148
+ value: values.map(value => String(+value.toPrecision(3)))
149
+ })
150
+ .editGraphics({ target: "sizeLegendTitle", property: "x", value: originX })
151
+ .editGraphics({ target: "sizeLegendTitle", property: "y", value: titleY })
152
+ .editGraphics({ target: "sizeLegendTitle", property: "text", value: title });
153
+ next = styleText(next, "sizeLegendLabels", {
154
+ style: currentConfig.inheritAppearance === true
155
+ ? categorical.labels
156
+ : undefined
157
+ });
158
+ return styleText(next, "sizeLegendTitle", {
159
+ title: true,
160
+ style: currentConfig.inheritAppearance === true
161
+ ? categorical.titleStyle
162
+ : undefined
163
+ });
164
+ }
165
+ );
166
+
167
+ export const createSizeLegend = action(
168
+ {
169
+ op: "createSizeLegend",
170
+ description: "Create a quantitative equal-area point-size legend."
171
+ },
172
+ function (args = {}) {
173
+ validateKeys(args, [...SIZE_OPTIONS, "inheritAppearance"], "createSizeLegend");
174
+ const layer = resolvePoint(this, args.target);
175
+ const encoding = layer.encoding?.size;
176
+ if (encoding?.scale === undefined) {
177
+ throw new Error(`Point mark "${layer.id}" requires a size encoding.`);
178
+ }
179
+ const scale = requireScale(this, encoding.scale, "linear");
180
+ const count = args.count ?? 5;
181
+ if (!Number.isInteger(count) || count < 2) {
182
+ throw new RangeError("Size legend count must be an integer of at least 2.");
183
+ }
184
+ return this
185
+ .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
186
+ .editSemantic({ property: "guide.legend.size.title", value: encoding.field })
187
+ ._withLegendConfig("size", {
188
+ target: layer.id,
189
+ scale: encoding.scale,
190
+ title: encoding.field,
191
+ inferredTitle: true,
192
+ domain: scale.domain,
193
+ count,
194
+ inheritAppearance: args.inheritAppearance === true
195
+ })
196
+ .createGraphics({
197
+ id: "sizeLegendSymbols",
198
+ type: "circle",
199
+ length: count,
200
+ ...resolveLegendGraphicPlacement(this)
201
+ })
202
+ .createGraphics({
203
+ id: "sizeLegendLabels",
204
+ type: "text",
205
+ length: count,
206
+ ...resolveLegendGraphicPlacement(this)
207
+ })
208
+ .createGraphics({
209
+ id: "sizeLegendTitle",
210
+ type: "text",
211
+ ...resolveLegendGraphicPlacement(this)
212
+ })
213
+ .rematerializeSizeLegend();
214
+ }
215
+ );
216
+
217
+ export function registerSizeLegendActions(ProgramClass) {
218
+ ProgramClass.prototype.createSizeLegend = createSizeLegend;
219
+ ProgramClass.prototype.rematerializeSizeLegend = rematerializeSizeLegend;
220
+ }
@@ -0,0 +1,84 @@
1
+ import { resolveHistogramBins } from "../../grammar/histogram.js";
2
+ import {
3
+ isTransformedScaleType,
4
+ readQuantitativeField,
5
+ transformedTicks
6
+ } from "../../grammar/scales.js";
7
+ import { niceTicks, timeTicks } from "../../grammar/ticks.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findSemanticScale } from "../../selectors/scales.js";
10
+
11
+ export const DEFAULT_TICK_COUNT = 5;
12
+
13
+ export function inferHistogramBoundaries(program, channel, scaleId) {
14
+ if (channel !== "x") return undefined;
15
+
16
+ const consumers = program.semanticSpec.layers.filter(
17
+ layer =>
18
+ layer.encoding?.x?.scale === scaleId &&
19
+ layer.encoding.x.bin !== undefined
20
+ );
21
+ if (consumers.length === 0) return undefined;
22
+ if (consumers.length > 1) {
23
+ throw new Error(
24
+ `Guide values cannot infer shared histogram bins for scale "${scaleId}".`
25
+ );
26
+ }
27
+
28
+ const [layer] = consumers;
29
+ const encoding = layer.encoding.x;
30
+ const dataset = findDataset(program, layer.data);
31
+ const scale = findSemanticScale(program, scaleId);
32
+ if (dataset === undefined || scale === undefined) {
33
+ throw new Error(
34
+ `Guide values require histogram data and scale "${scaleId}".`
35
+ );
36
+ }
37
+
38
+ return resolveHistogramBins({
39
+ values: readQuantitativeField(dataset.values, encoding.field),
40
+ bin: encoding.bin,
41
+ domain: scale.domain,
42
+ nice: scale.nice ?? true,
43
+ zero: scale.zero ?? false
44
+ }).boundaries;
45
+ }
46
+
47
+ export function valuesFromTickConfig(program, config) {
48
+ if (config.mode === "values") return config.values;
49
+
50
+ const scale = program.resolvedScales[config.scale];
51
+ if (scale?.type === "time") return timeTicks(scale.domain, config.count);
52
+ if (scale?.type === "linear") return niceTicks(scale.domain, config.count);
53
+ if (isTransformedScaleType(scale?.type)) {
54
+ return transformedTicks(scale.type, scale.domain, config.count, {
55
+ ...(scale.base === undefined ? {} : { base: scale.base }),
56
+ ...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
57
+ ...(scale.constant === undefined ? {} : { constant: scale.constant })
58
+ });
59
+ }
60
+ throw new Error(
61
+ `Guide values require resolved continuous scale "${config.scale}".`
62
+ );
63
+ }
64
+
65
+ export function inferGridTickConfig(program, channel, scaleId) {
66
+ const axis = program.guideConfigs.axis?.[channel]?.ticks;
67
+ if (axis?.scale === scaleId) {
68
+ return axis.mode === "values"
69
+ ? { mode: "values", values: axis.values }
70
+ : { mode: "count", count: axis.count };
71
+ }
72
+
73
+ const boundaries = inferHistogramBoundaries(program, channel, scaleId);
74
+ const scale = program.resolvedScales[scaleId];
75
+ if (
76
+ boundaries === undefined &&
77
+ ["ordinal", "band", "point"].includes(scale?.type)
78
+ ) {
79
+ return { mode: "values", values: scale.domain };
80
+ }
81
+ return boundaries === undefined
82
+ ? { mode: "count", count: DEFAULT_TICK_COUNT }
83
+ : { mode: "values", values: boundaries };
84
+ }
@@ -0,0 +1,31 @@
1
+ import { registerCanvasActions } from "./canvas/index.js";
2
+ import { registerCoordinateActions } from "./coordinates/index.js";
3
+ import { registerDataActions } from "./data/index.js";
4
+ import { registerEncodingActions } from "./encodings/index.js";
5
+ import { registerErrorBandActions } from "./errorBands/index.js";
6
+ import { registerErrorBarActions } from "./errorBars/index.js";
7
+ import { registerBoxPlotActions } from "./boxPlots/index.js";
8
+ import { registerGuideActions } from "./guides/index.js";
9
+ import { registerMarkActions } from "./marks/index.js";
10
+ import { registerPrimitiveActions } from "./primitives/index.js";
11
+ import { registerRegressionActions } from "./regression/index.js";
12
+ import { registerScaleActions } from "./scales/index.js";
13
+ import { registerTitleActions } from "./titles/index.js";
14
+ import { registerSelectionActions } from "./selection/index.js";
15
+
16
+ export function registerActions(ProgramClass) {
17
+ registerPrimitiveActions(ProgramClass);
18
+ registerCanvasActions(ProgramClass);
19
+ registerDataActions(ProgramClass);
20
+ registerMarkActions(ProgramClass);
21
+ registerScaleActions(ProgramClass);
22
+ registerEncodingActions(ProgramClass);
23
+ registerCoordinateActions(ProgramClass);
24
+ registerGuideActions(ProgramClass);
25
+ registerTitleActions(ProgramClass);
26
+ registerRegressionActions(ProgramClass);
27
+ registerErrorBarActions(ProgramClass);
28
+ registerErrorBandActions(ProgramClass);
29
+ registerBoxPlotActions(ProgramClass);
30
+ registerSelectionActions(ProgramClass);
31
+ }