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,273 @@
1
+ import { isPlainObject } from "../../../../core/immutable.js";
2
+ import {
3
+ validateKeys,
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validatePositiveFinite
7
+ } from "../../../../core/validation.js";
8
+ import { formatTimeTick } from "../../../../grammar/ticks.js";
9
+ import { resolveGraphicBounds } from "../../../../layout/canvas.js";
10
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
11
+ "../../../../theme/defaults.js";
12
+ import { findLayer } from "../../../../selectors/layers.js";
13
+
14
+ const OPTIONS = Object.freeze([
15
+ "target", "channels", "position", "align", "offset", "title", "count",
16
+ "gradient", "symbol", "labels", "titleStyle", "itemGap", "border",
17
+ "direction", "columns", "titlePosition"
18
+ ]);
19
+ const TEXT_OPTIONS = Object.freeze([
20
+ "offset", "color", "fontSize", "fontFamily", "fontWeight"
21
+ ]);
22
+ const BORDER_OPTIONS = Object.freeze([
23
+ "color", "lineWidth", "padding", "background"
24
+ ]);
25
+ const POSITIONS = Object.freeze(["right", "left", "top", "bottom"]);
26
+ const DEFAULT_LABELS = Object.freeze({
27
+ offset: 12,
28
+ color: DEFAULT_COLORS.text,
29
+ fontSize: 12,
30
+ fontFamily: DEFAULT_FONT_FAMILY,
31
+ fontWeight: "normal"
32
+ });
33
+ const DEFAULT_TITLE = Object.freeze({
34
+ color: DEFAULT_COLORS.text,
35
+ fontSize: 13,
36
+ fontFamily: DEFAULT_FONT_FAMILY,
37
+ fontWeight: 600
38
+ });
39
+ const DEFAULT_BORDER = Object.freeze({
40
+ color: DEFAULT_COLORS.border,
41
+ lineWidth: 1,
42
+ padding: 12,
43
+ background: "transparent"
44
+ });
45
+
46
+ export const validatePositive = validatePositiveFinite;
47
+ export const validateNonNegative = validateNonNegativeFinite;
48
+
49
+ function validateTextOptions(value, label, defaults) {
50
+ if (value === undefined) return { ...defaults };
51
+ if (!isPlainObject(value)) {
52
+ throw new TypeError(`${label} must be a plain object.`);
53
+ }
54
+ validateKeys(value, TEXT_OPTIONS, label);
55
+ const result = { ...defaults, ...value };
56
+ if (Object.hasOwn(result, "offset")) {
57
+ validateNonNegative(result.offset, `${label} offset`);
58
+ }
59
+ validatePositive(result.fontSize, `${label} fontSize`);
60
+ for (const key of ["color", "fontFamily"]) {
61
+ validateNonEmptyString(result[key], `${label} ${key}`);
62
+ }
63
+ if (
64
+ typeof result.fontWeight !== "string" &&
65
+ !Number.isFinite(result.fontWeight)
66
+ ) {
67
+ throw new TypeError(`${label} fontWeight must be a string or finite number.`);
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function normalizeBorder(value) {
73
+ if (value === undefined || value === false) return false;
74
+ if (value !== true && !isPlainObject(value)) {
75
+ throw new TypeError("createLegend.border must be a boolean or plain object.");
76
+ }
77
+ if (value !== true) validateKeys(value, BORDER_OPTIONS, "createLegend.border");
78
+ const border = { ...DEFAULT_BORDER, ...(value === true ? {} : value) };
79
+ for (const key of ["color", "background"]) {
80
+ validateNonEmptyString(border[key], `Legend border ${key}`);
81
+ }
82
+ validateNonNegative(border.lineWidth, "Legend border lineWidth");
83
+ validateNonNegative(border.padding, "Legend border padding");
84
+ return border;
85
+ }
86
+
87
+ export function normalizeContinuousLegend(args, kind) {
88
+ if (!isPlainObject(args)) {
89
+ throw new TypeError("createLegend options must be a plain object.");
90
+ }
91
+ validateKeys(args, OPTIONS, "createLegend");
92
+ const position = args.position ?? "right";
93
+ if (!POSITIONS.includes(position)) {
94
+ throw new Error(`Unsupported legend position "${position}".`);
95
+ }
96
+ const align = args.align ?? "center";
97
+ if (!["left", "center", "right"].includes(align)) {
98
+ throw new Error(`Unsupported legend alignment "${align}".`);
99
+ }
100
+ const count = args.count ?? 5;
101
+ if (!Number.isInteger(count) || count < 2) {
102
+ throw new RangeError(
103
+ "Continuous legend count must be an integer of at least 2."
104
+ );
105
+ }
106
+ const offset = args.offset ?? 30;
107
+ validateNonNegative(offset, "Legend offset");
108
+ const itemGap = args.itemGap ?? 28;
109
+ validatePositive(itemGap, "Legend itemGap");
110
+ if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
111
+ if (args.titlePosition !== undefined && args.titlePosition !== "top") {
112
+ throw new Error("Continuous legends currently require top titlePosition.");
113
+ }
114
+ if (kind === "gradient") {
115
+ for (const key of ["symbol", "columns", "direction", "itemGap"]) {
116
+ if (Object.hasOwn(args, key)) {
117
+ throw new Error(`Gradient legend does not accept ${key}.`);
118
+ }
119
+ }
120
+ } else {
121
+ for (const key of ["columns", "direction", "gradient"]) {
122
+ if (Object.hasOwn(args, key)) {
123
+ throw new Error(`Opacity legend does not accept ${key}.`);
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ target: args.target,
129
+ position,
130
+ align,
131
+ offset,
132
+ count,
133
+ title: args.title,
134
+ inferredTitle: args.title === undefined,
135
+ labels: validateTextOptions(
136
+ args.labels,
137
+ "createLegend.labels",
138
+ DEFAULT_LABELS
139
+ ),
140
+ titleStyle: validateTextOptions(
141
+ args.titleStyle,
142
+ "createLegend.titleStyle",
143
+ DEFAULT_TITLE
144
+ ),
145
+ itemGap,
146
+ border: normalizeBorder(args.border)
147
+ };
148
+ }
149
+
150
+ export function resolveContinuousPoint(program, requested, channel) {
151
+ const candidates = program.semanticSpec.layers.filter(layer =>
152
+ layer.mark?.type === "point" &&
153
+ layer.encoding?.[channel]?.scale !== undefined
154
+ );
155
+ const layer = requested === undefined
156
+ ? candidates.length === 1 ? candidates[0] : undefined
157
+ : (() => {
158
+ const candidate = findLayer(program, requested);
159
+ return candidates.includes(candidate) ? candidate : undefined;
160
+ })();
161
+ if (layer === undefined) {
162
+ throw new Error(
163
+ requested === undefined
164
+ ? `${channel} legend requires one eligible point mark.`
165
+ : `Unknown ${channel} legend target "${requested}".`
166
+ );
167
+ }
168
+ return layer;
169
+ }
170
+
171
+ export function resolveContinuousColorLayer(program, requested) {
172
+ const candidates = program.semanticSpec.layers.filter(layer =>
173
+ ["point", "bar"].includes(layer.mark?.type) &&
174
+ layer.encoding?.color?.scale !== undefined
175
+ );
176
+ const layer = requested === undefined
177
+ ? candidates.length === 1 ? candidates[0] : undefined
178
+ : (() => {
179
+ const candidate = findLayer(program, requested);
180
+ return candidates.includes(candidate) ? candidate : undefined;
181
+ })();
182
+ if (layer === undefined) {
183
+ throw new Error(
184
+ requested === undefined
185
+ ? "color legend requires one eligible point or bar mark."
186
+ : `Unknown color legend target "${requested}".`
187
+ );
188
+ }
189
+ return layer;
190
+ }
191
+
192
+ export function requireResolvedLegendScale(program, id, type) {
193
+ const scale = program.resolvedScales[id];
194
+ if (scale?.type !== type) {
195
+ throw new Error(`Legend requires resolved ${type} scale "${id}".`);
196
+ }
197
+ return scale;
198
+ }
199
+
200
+ export function resolveContinuousBounds(program) {
201
+ const plot = resolveGraphicBounds(program);
202
+ const canvas = program.graphicSpec.objects.canvas;
203
+ if (
204
+ plot === undefined ||
205
+ ![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
206
+ canvas?.type !== "canvas" ||
207
+ !Number.isFinite(canvas.properties.width) ||
208
+ !Number.isFinite(canvas.properties.height)
209
+ ) {
210
+ throw new Error("Continuous legend layout requires Canvas bounds.");
211
+ }
212
+ return { plot, canvas: canvas.properties };
213
+ }
214
+
215
+ export function sampleContinuousValues(domain, count) {
216
+ return Array.from({ length: count }, (_, index) =>
217
+ domain[0] + index / (count - 1) * (domain[1] - domain[0])
218
+ );
219
+ }
220
+
221
+ export function formatContinuousValues(values, domain, fieldType) {
222
+ return values.map(value => fieldType === "temporal"
223
+ ? formatTimeTick(value, domain)
224
+ : String(+value.toPrecision(3))
225
+ );
226
+ }
227
+
228
+ export function styleContinuousText(
229
+ program,
230
+ id,
231
+ style,
232
+ { align = "left" } = {}
233
+ ) {
234
+ return program
235
+ .editGraphics({ target: id, property: "fill", value: style.color })
236
+ .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
237
+ .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
238
+ .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
239
+ .editGraphics({ target: id, property: "textAlign", value: align })
240
+ .editGraphics({ target: id, property: "textBaseline", value: "middle" });
241
+ }
242
+
243
+ export function assertLegendInsideCanvas(items, canvas, label) {
244
+ if (items.some(item =>
245
+ item.x < 0 || item.y < 0 || item.x > canvas.width || item.y > canvas.height
246
+ )) {
247
+ throw new Error(`${label} requires more Canvas margin space.`);
248
+ }
249
+ }
250
+
251
+ export function resolveLegendBackgroundBounds(points, border, canvas, label) {
252
+ if (border === false) return undefined;
253
+ const x = Math.min(...points.map(point => point.x)) - border.padding;
254
+ const y = Math.min(...points.map(point => point.y)) - border.padding;
255
+ const right = Math.max(...points.map(point => point.x)) + border.padding;
256
+ const bottom = Math.max(...points.map(point => point.y)) + border.padding;
257
+ if (x < 0 || y < 0 || right > canvas.width || bottom > canvas.height) {
258
+ throw new Error(`${label} background requires more Canvas margin space.`);
259
+ }
260
+ return { x, y, width: right - x, height: bottom - y };
261
+ }
262
+
263
+ export function editLegendBackground(program, id, bounds, border) {
264
+ if (bounds === undefined) return program;
265
+ return program
266
+ .editGraphics({ target: id, property: "x", value: bounds.x })
267
+ .editGraphics({ target: id, property: "y", value: bounds.y })
268
+ .editGraphics({ target: id, property: "width", value: bounds.width })
269
+ .editGraphics({ target: id, property: "height", value: bounds.height })
270
+ .editGraphics({ target: id, property: "fill", value: border.background })
271
+ .editGraphics({ target: id, property: "stroke", value: border.color })
272
+ .editGraphics({ target: id, property: "strokeWidth", value: border.lineWidth });
273
+ }
@@ -0,0 +1,363 @@
1
+ import { action } from "../../../../core/action.js";
2
+ import { isPlainObject } from "../../../../core/immutable.js";
3
+ import { validateKeys } from "../../../../core/validation.js";
4
+ import { interpolateColorStops } from "../../../../grammar/scales/color.js";
5
+ import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
+ import {
7
+ assertLegendInsideCanvas,
8
+ editLegendBackground,
9
+ formatContinuousValues,
10
+ normalizeContinuousLegend,
11
+ requireResolvedLegendScale,
12
+ resolveContinuousBounds,
13
+ resolveContinuousColorLayer,
14
+ resolveLegendBackgroundBounds,
15
+ sampleContinuousValues,
16
+ styleContinuousText,
17
+ validatePositive
18
+ } from "./common.js";
19
+ import { resolveLegendGraphicPlacement } from
20
+ "../../../../materialization/graphicHierarchy.js";
21
+
22
+ const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
23
+
24
+ function resolveGradientLayout(program, config, scale) {
25
+ const { plot, canvas } = resolveContinuousBounds(program);
26
+ const vertical = ["right", "left"].includes(config.position);
27
+ const length = config.gradient.length;
28
+ const thickness = config.gradient.thickness;
29
+ let x;
30
+ let y;
31
+ if (config.position === "right") {
32
+ x = plot.x + plot.width + config.offset;
33
+ y = plot.y + 46;
34
+ } else if (config.position === "left") {
35
+ x = plot.x - config.offset - thickness;
36
+ y = plot.y + 46;
37
+ } else {
38
+ x = config.align === "left" ? plot.x
39
+ : config.align === "right" ? plot.x + plot.width - length
40
+ : plot.x + (plot.width - length) / 2;
41
+ y = config.position === "top"
42
+ ? plot.y - config.offset - thickness
43
+ : plot.y + plot.height + config.offset;
44
+ }
45
+ const title = vertical
46
+ ? { x, y: plot.y + 20, align: "left" }
47
+ : { x: x + length / 2, y: y - 20, align: "center" };
48
+ const values = sampleContinuousValues(scale.domain, config.count);
49
+ const fractions = values.map((_, index) => index / (values.length - 1));
50
+ const labelOffset = config.labels.offset;
51
+ const labels = vertical
52
+ ? fractions.map(fraction => ({
53
+ x: config.position === "right"
54
+ ? x + thickness + labelOffset
55
+ : x - labelOffset,
56
+ y: y + length * (1 - fraction),
57
+ align: config.position === "right" ? "left" : "right"
58
+ }))
59
+ : fractions.map(fraction => ({
60
+ x: x + length * fraction,
61
+ y: config.position === "top"
62
+ ? y - labelOffset
63
+ : y + thickness + labelOffset,
64
+ align: "center"
65
+ }));
66
+ const ticks = vertical
67
+ ? labels.map(label => ({
68
+ x1: config.position === "right" ? x + thickness : x,
69
+ y1: label.y,
70
+ x2: config.position === "right" ? x + thickness + 6 : x - 6,
71
+ y2: label.y
72
+ }))
73
+ : labels.map(label => ({
74
+ x1: label.x,
75
+ y1: config.position === "top" ? y : y + thickness,
76
+ x2: label.x,
77
+ y2: config.position === "top" ? y - 6 : y + thickness + 6
78
+ }));
79
+ assertLegendInsideCanvas([
80
+ title,
81
+ ...labels,
82
+ ...ticks.flatMap(tick => [
83
+ { x: tick.x1, y: tick.y1 },
84
+ { x: tick.x2, y: tick.y2 }
85
+ ])
86
+ ], canvas, "Gradient legend layout");
87
+ if (
88
+ x < 0 ||
89
+ y < 0 ||
90
+ x + (vertical ? thickness : length) > canvas.width ||
91
+ y + (vertical ? length : thickness) > canvas.height
92
+ ) {
93
+ throw new Error("Gradient legend layout requires more Canvas margin space.");
94
+ }
95
+ const background = resolveLegendBackgroundBounds([
96
+ { x, y },
97
+ {
98
+ x: x + (vertical ? thickness : length),
99
+ y: y + (vertical ? length : thickness)
100
+ },
101
+ title,
102
+ ...labels.map(label => ({
103
+ x: label.x + (
104
+ label.align === "left" ? 42 : label.align === "right" ? -42 : 0
105
+ ),
106
+ y: label.y
107
+ }))
108
+ ], config.border, canvas, "Gradient legend");
109
+ return {
110
+ vertical, x, y, length, thickness, values, labels, ticks, title, background
111
+ };
112
+ }
113
+
114
+ function resolveGradientConfig(program, config) {
115
+ const layer = resolveContinuousColorLayer(program, config.target);
116
+ const encoding = layer.encoding.color;
117
+ if (!["quantitative", "temporal"].includes(encoding.fieldType)) {
118
+ throw new Error("Gradient legend requires quantitative or temporal color.");
119
+ }
120
+ const scale = requireResolvedLegendScale(
121
+ program,
122
+ encoding.scale,
123
+ "sequential"
124
+ );
125
+ return {
126
+ layer,
127
+ encoding,
128
+ scale,
129
+ config: {
130
+ ...config,
131
+ target: layer.id,
132
+ scale: encoding.scale,
133
+ fieldType: encoding.fieldType,
134
+ title: config.inferredTitle ? encoding.field : config.title,
135
+ domain: scale.domain
136
+ }
137
+ };
138
+ }
139
+
140
+ export const rematerializeGradientLegend = action(
141
+ {
142
+ op: "rematerializeGradientLegend",
143
+ description: "Rematerialize a continuous color gradient legend."
144
+ },
145
+ function (args = {}) {
146
+ validateKeys(args, [], "rematerializeGradientLegend");
147
+ const stored = this.guideConfigs.legend?.gradient;
148
+ if (stored === undefined) {
149
+ throw new Error("Gradient legend requires stored configuration.");
150
+ }
151
+ const { scale, encoding, config } = resolveGradientConfig(this, stored);
152
+ const layout = resolveGradientLayout(this, config, scale);
153
+ const stripCount = 60;
154
+ const stripSize = layout.length / stripCount;
155
+ const strips = Array.from({ length: stripCount }, (_, index) => {
156
+ const fraction = (index + 0.5) / stripCount;
157
+ const color = interpolateColorStops(
158
+ scale.range,
159
+ layout.vertical ? 1 - fraction : fraction,
160
+ scale.interpolate
161
+ );
162
+ return {
163
+ x: layout.x + (layout.vertical ? 0 : index * stripSize),
164
+ y: layout.y + (layout.vertical ? index * stripSize : 0),
165
+ width: layout.vertical ? layout.thickness : stripSize,
166
+ height: layout.vertical ? stripSize : layout.thickness,
167
+ fill: color,
168
+ stroke: color,
169
+ strokeWidth: 0
170
+ };
171
+ });
172
+ let next = this
173
+ .editSemantic({
174
+ property: "guide.legend.color.scale",
175
+ value: encoding.scale
176
+ })
177
+ .editSemantic({
178
+ property: "guide.legend.color.title",
179
+ value: config.title
180
+ })
181
+ ._withLegendConfig("gradient", config)
182
+ .editGraphics({
183
+ target: "colorGradientStrips",
184
+ property: "length",
185
+ value: strips.length
186
+ });
187
+ next = editLegendBackground(
188
+ next,
189
+ "colorGradientBackground",
190
+ layout.background,
191
+ config.border
192
+ );
193
+ for (const property of [
194
+ "x", "y", "width", "height", "fill", "stroke", "strokeWidth"
195
+ ]) {
196
+ next = next.editGraphics({
197
+ target: "colorGradientStrips",
198
+ property,
199
+ value: strips.map(strip => strip[property])
200
+ });
201
+ }
202
+ next = next.editGraphics({
203
+ target: "colorGradientTicks",
204
+ property: "length",
205
+ value: layout.ticks.length
206
+ });
207
+ for (const property of ["x1", "y1", "x2", "y2"]) {
208
+ next = next.editGraphics({
209
+ target: "colorGradientTicks",
210
+ property,
211
+ value: layout.ticks.map(tick => tick[property])
212
+ });
213
+ }
214
+ next = next
215
+ .editGraphics({
216
+ target: "colorGradientTicks",
217
+ property: "stroke",
218
+ value: DEFAULT_COLORS.mutedText
219
+ })
220
+ .editGraphics({
221
+ target: "colorGradientTicks",
222
+ property: "strokeWidth",
223
+ value: 1
224
+ })
225
+ .editGraphics({
226
+ target: "colorGradientLabels",
227
+ property: "length",
228
+ value: layout.labels.length
229
+ })
230
+ .editGraphics({
231
+ target: "colorGradientLabels",
232
+ property: "x",
233
+ value: layout.labels.map(label => label.x)
234
+ })
235
+ .editGraphics({
236
+ target: "colorGradientLabels",
237
+ property: "y",
238
+ value: layout.labels.map(label => label.y)
239
+ })
240
+ .editGraphics({
241
+ target: "colorGradientLabels",
242
+ property: "text",
243
+ value: formatContinuousValues(
244
+ layout.values,
245
+ scale.domain,
246
+ config.fieldType
247
+ )
248
+ });
249
+ next = styleContinuousText(
250
+ next,
251
+ "colorGradientLabels",
252
+ config.labels,
253
+ { align: layout.labels[0].align }
254
+ );
255
+ if (config.titleVisible === false) return next;
256
+ next = next
257
+ .editGraphics({
258
+ target: "colorGradientTitle",
259
+ property: "x",
260
+ value: layout.title.x
261
+ })
262
+ .editGraphics({
263
+ target: "colorGradientTitle",
264
+ property: "y",
265
+ value: layout.title.y
266
+ })
267
+ .editGraphics({
268
+ target: "colorGradientTitle",
269
+ property: "text",
270
+ value: config.title
271
+ });
272
+ return styleContinuousText(
273
+ next,
274
+ "colorGradientTitle",
275
+ config.titleStyle,
276
+ { align: layout.title.align }
277
+ );
278
+ }
279
+ );
280
+
281
+ export const createGradientLegend = action(
282
+ {
283
+ op: "createGradientLegend",
284
+ description: "Create a continuous color gradient legend."
285
+ },
286
+ function (args = {}) {
287
+ const config = normalizeContinuousLegend(args, "gradient");
288
+ if (args.channels !== undefined && (
289
+ !Array.isArray(args.channels) ||
290
+ args.channels.length !== 1 ||
291
+ args.channels[0] !== "color"
292
+ )) {
293
+ throw new Error('Gradient legend requires channels: ["color"].');
294
+ }
295
+ if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
296
+ throw new TypeError("createLegend.gradient must be a plain object.");
297
+ }
298
+ validateKeys(
299
+ args.gradient ?? {},
300
+ GRADIENT_OPTIONS,
301
+ "createLegend.gradient"
302
+ );
303
+ config.gradient = {
304
+ length: args.gradient?.length ?? 120,
305
+ thickness: args.gradient?.thickness ?? 12
306
+ };
307
+ config.titleVisible = true;
308
+ validatePositive(config.gradient.length, "Gradient length");
309
+ validatePositive(config.gradient.thickness, "Gradient thickness");
310
+ const resolved = resolveGradientConfig(this, config);
311
+ resolveGradientLayout(this, resolved.config, resolved.scale);
312
+ if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
313
+ throw new Error(
314
+ "createGradientLegend requires a missing gradient legend."
315
+ );
316
+ }
317
+ let next = this
318
+ .editSemantic({
319
+ property: "guide.legend.color.scale",
320
+ value: resolved.encoding.scale
321
+ })
322
+ .editSemantic({
323
+ property: "guide.legend.color.title",
324
+ value: resolved.config.title
325
+ })
326
+ ._withLegendConfig("gradient", resolved.config);
327
+ if (resolved.config.border !== false) {
328
+ next = next.createGraphics({
329
+ id: "colorGradientBackground",
330
+ type: "rect",
331
+ ...resolveLegendGraphicPlacement(next)
332
+ });
333
+ }
334
+ return next
335
+ .createGraphics({
336
+ id: "colorGradientStrips",
337
+ type: "rect",
338
+ length: 0,
339
+ ...resolveLegendGraphicPlacement(next),
340
+ ...(resolved.config.border === false
341
+ ? {}
342
+ : { after: "colorGradientBackground" })
343
+ })
344
+ .createGraphics({
345
+ id: "colorGradientTicks",
346
+ type: "line",
347
+ length: 0,
348
+ ...resolveLegendGraphicPlacement(next)
349
+ })
350
+ .createGraphics({
351
+ id: "colorGradientLabels",
352
+ type: "text",
353
+ length: 0,
354
+ ...resolveLegendGraphicPlacement(next)
355
+ })
356
+ .createGraphics({
357
+ id: "colorGradientTitle",
358
+ type: "text",
359
+ ...resolveLegendGraphicPlacement(next)
360
+ })
361
+ .rematerializeGradientLegend();
362
+ }
363
+ );
@@ -0,0 +1,25 @@
1
+ import { createGradientLegend, rematerializeGradientLegend } from "./gradient.js";
2
+ import {
3
+ createOpacityLegend,
4
+ rematerializeOpacityLegend,
5
+ removeOpacityLegend
6
+ } from "./opacity.js";
7
+ import { createIntervalLegend, rematerializeIntervalLegend } from "./interval.js";
8
+
9
+ export { createGradientLegend, rematerializeGradientLegend } from "./gradient.js";
10
+ export {
11
+ createOpacityLegend,
12
+ rematerializeOpacityLegend,
13
+ removeOpacityLegend
14
+ } from "./opacity.js";
15
+ export { createIntervalLegend, rematerializeIntervalLegend } from "./interval.js";
16
+
17
+ export function registerContinuousLegendActions(ProgramClass) {
18
+ ProgramClass.prototype.createGradientLegend = createGradientLegend;
19
+ ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
20
+ ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
21
+ ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
22
+ ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
23
+ ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
24
+ ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
25
+ }