ggaction 0.0.17 → 0.0.18

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 (263) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -1,447 +1 @@
1
- import { isPlainObject } from "../../../../core/immutable.js";
2
- import { resolveTextBounds } from "../../../../core/textMetrics.js";
3
- import {
4
- validateKeys,
5
- validateOptionObject,
6
- validateNonEmptyString,
7
- validateNonNegativeFinite,
8
- validatePositiveFinite,
9
- validateGeneratedItemLimit
10
- } from "../../../../core/validation.js";
11
- import { formatTimeTick } from "../../../../grammar/ticks.js";
12
- import {
13
- formatValue,
14
- validateValueFormat
15
- } from "../../../../grammar/valueFormat.js";
16
- import {
17
- formatDistinctNumericSamples,
18
- sampleNumericRange
19
- } from "../../../../grammar/numeric.js";
20
- import { resolveGraphicBounds } from "../../../../layout/canvas.js";
21
- import { isHorizontalEdgeLegend } from "../../../../layout/legendLane.js";
22
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
23
- "../../../../theme/defaults.js";
24
- import { findLayer } from "../../../../selectors/layers.js";
25
- import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
26
- import { findCanvasGraphic } from
27
- "../../../../materialization/graphicHierarchy.js";
28
- import { normalizeLegendSampling } from "../sampling.js";
29
- import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
30
-
31
- const OPTIONS = [
32
- "target", "channels", "position", "align", "offset", "title", "count",
33
- "values",
34
- "gradient", "symbol", "labels", "titleStyle", "itemGap", "border",
35
- "direction", "columns", "titlePosition"
36
- ];
37
- const TEXT_OPTIONS = [
38
- "offset", "color", "fontSize", "fontFamily", "fontWeight", "format"
39
- ];
40
- const BORDER_OPTIONS = [
41
- "color", "lineWidth", "padding", "background"
42
- ];
43
- const POSITIONS = ["right", "left", "top", "bottom"];
44
- const DEFAULT_LABELS = {
45
- offset: 12,
46
- color: DEFAULT_COLORS.text,
47
- fontSize: 12,
48
- fontFamily: DEFAULT_FONT_FAMILY,
49
- fontWeight: "normal"
50
- };
51
- const DEFAULT_TITLE = {
52
- color: DEFAULT_COLORS.text,
53
- fontSize: 13,
54
- fontFamily: DEFAULT_FONT_FAMILY,
55
- fontWeight: 600
56
- };
57
- const DEFAULT_BORDER = {
58
- color: DEFAULT_COLORS.border,
59
- lineWidth: 1,
60
- padding: 12,
61
- background: "transparent"
62
- };
63
-
64
- export const validatePositive = validatePositiveFinite;
65
- export const validateNonNegative = validateNonNegativeFinite;
66
-
67
- export { editGraphicProperties };
68
-
69
- export function normalizeLegendTextOptions(value, label, defaults) {
70
- if (value === undefined) return { ...defaults };
71
- if (!isPlainObject(value)) {
72
- throw new TypeError(`${label} must be a plain object.`);
73
- }
74
- validateKeys(value, TEXT_OPTIONS, label);
75
- const result = { ...defaults, ...value };
76
- if (Object.hasOwn(result, "offset")) {
77
- validateNonNegative(result.offset, `${label} offset`);
78
- }
79
- validatePositive(result.fontSize, `${label} fontSize`);
80
- for (const key of ["color", "fontFamily"]) {
81
- validateNonEmptyString(result[key], `${label} ${key}`);
82
- }
83
- if (
84
- typeof result.fontWeight !== "string" &&
85
- !Number.isFinite(result.fontWeight)
86
- ) {
87
- throw new TypeError(`${label} fontWeight must be a string or finite number.`);
88
- }
89
- if (Object.hasOwn(result, "format")) {
90
- result.format = validateValueFormat(result.format, `${label} format`);
91
- }
92
- return result;
93
- }
94
-
95
- export function normalizeLegendTitleOptions(value, label, defaults) {
96
- if (value !== undefined) {
97
- validateOptionObject(value, ["color", "fontSize", "fontFamily", "fontWeight"], label);
98
- }
99
- return normalizeLegendTextOptions(value, label, defaults);
100
- }
101
-
102
- export function normalizeLegendBorder(value) {
103
- if (value === undefined || value === false) return false;
104
- if (value !== true && !isPlainObject(value)) {
105
- throw new TypeError("createLegend.border must be a boolean or plain object.");
106
- }
107
- if (value !== true) validateKeys(value, BORDER_OPTIONS, "createLegend.border");
108
- const border = { ...DEFAULT_BORDER, ...(value === true ? {} : value) };
109
- for (const key of ["color", "background"]) {
110
- validateNonEmptyString(border[key], `Legend border ${key}`);
111
- }
112
- validateNonNegative(border.lineWidth, "Legend border lineWidth");
113
- validateNonNegative(border.padding, "Legend border padding");
114
- return border;
115
- }
116
-
117
- export function normalizeContinuousLegend(args, kind) {
118
- validateOptionObject(args, OPTIONS, "createLegend");
119
- const position = args.position ?? "right";
120
- if (!POSITIONS.includes(position)) {
121
- throw new Error(`Unsupported legend position "${position}".`);
122
- }
123
- const align = args.align ?? "center";
124
- if (!["left", "center", "right"].includes(align)) {
125
- throw new Error(`Unsupported legend alignment "${align}".`);
126
- }
127
- if (["left", "right"].includes(position) && align !== "center") {
128
- throw new Error("Side continuous legends require center alignment.");
129
- }
130
- if (kind === "gradient" && Object.hasOwn(args, "values")) {
131
- throw new Error("Gradient legends do not support exact sample values.");
132
- }
133
- const sampling = kind === "opacity"
134
- ? normalizeLegendSampling(args, {
135
- operation: "create",
136
- label: "Opacity legend"
137
- })
138
- : undefined;
139
- const count = kind === "gradient" ? args.count ?? 5 : undefined;
140
- if (kind === "gradient") {
141
- if (!Number.isInteger(count) || count < 2) {
142
- throw new RangeError(
143
- "Continuous legend count must be an integer of at least 2."
144
- );
145
- }
146
- validateGeneratedItemLimit(count, "Continuous legend count");
147
- }
148
- const offset = args.offset ?? 30;
149
- validateNonNegative(offset, "Legend offset");
150
- const titlePosition = args.titlePosition ?? "top";
151
- if (!["top", "left"].includes(titlePosition)) {
152
- throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
153
- }
154
- if (
155
- titlePosition === "left" &&
156
- (!["top", "bottom"].includes(position) || kind !== "opacity")
157
- ) {
158
- throw new Error(
159
- "Only horizontal opacity legends support left titlePosition."
160
- );
161
- }
162
- const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
163
- validatePositive(itemGap, "Legend itemGap");
164
- if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
165
- if (kind === "gradient") {
166
- for (const key of ["symbol", "columns", "direction", "itemGap"]) {
167
- if (Object.hasOwn(args, key)) {
168
- throw new Error(`Gradient legend does not accept ${key}.`);
169
- }
170
- }
171
- } else {
172
- for (const key of ["columns", "direction", "gradient"]) {
173
- if (Object.hasOwn(args, key)) {
174
- throw new Error(`Opacity legend does not accept ${key}.`);
175
- }
176
- }
177
- }
178
- return {
179
- target: args.target,
180
- position,
181
- align,
182
- offset,
183
- ...(kind === "gradient" ? { count } : { sampling }),
184
- title: args.title,
185
- inferredTitle: args.title === undefined,
186
- labels: normalizeLegendTextOptions(
187
- args.labels,
188
- "createLegend.labels",
189
- titlePosition === "left"
190
- ? { ...DEFAULT_LABELS, offset: 8 }
191
- : DEFAULT_LABELS
192
- ),
193
- titleStyle: normalizeLegendTitleOptions(
194
- args.titleStyle,
195
- "createLegend.titleStyle",
196
- DEFAULT_TITLE
197
- ),
198
- itemGap,
199
- titlePosition,
200
- border: normalizeLegendBorder(args.border)
201
- };
202
- }
203
-
204
- export function selectLegendLayer(program, requested, predicate) {
205
- const candidates = program.semanticSpec.layers.filter(predicate);
206
- if (requested === undefined) return candidates.length === 1
207
- ? candidates[0]
208
- : undefined;
209
- const candidate = findLayer(program, requested);
210
- return candidates.includes(candidate) ? candidate : undefined;
211
- }
212
-
213
- export function resolveContinuousLegendLayer(program, requested, channel) {
214
- const layer = selectLegendLayer(
215
- program,
216
- requested,
217
- channel === "opacity" ? isOpacityLegendLayer : candidate =>
218
- candidate.mark?.type === "point" && candidate.encoding?.[channel]?.scale !== undefined
219
- );
220
- if (layer === undefined) {
221
- throw new Error(
222
- requested === undefined
223
- ? `${channel} legend requires one eligible ${channel === "opacity" ? "point or line" : "point"} mark.`
224
- : `Unknown ${channel} legend target "${requested}".`
225
- );
226
- }
227
- return layer;
228
- }
229
-
230
- export function resolveContinuousColorLayer(program, requested, channel = "color") {
231
- const layer = selectLegendLayer(
232
- program,
233
- requested,
234
- candidate => (channel === "stroke"
235
- ? ["point", "line", "area", "bar", "rect", "arc", "rule", "tick"]
236
- : ["point", "bar", "rect"]
237
- ).includes(candidate.mark?.type) &&
238
- candidate.encoding?.[channel]?.scale !== undefined
239
- );
240
- if (layer === undefined) {
241
- throw new Error(
242
- requested === undefined
243
- ? channel === "color"
244
- ? "color legend requires one eligible point, bar, or rect mark."
245
- : "stroke legend requires one eligible mark."
246
- : `Unknown ${channel} legend target "${requested}".`
247
- );
248
- }
249
- return layer;
250
- }
251
-
252
- export function requireResolvedLegendScale(program, id, type) {
253
- const scale = program.resolvedScales[id];
254
- if (scale?.type !== type) {
255
- throw new Error(`Legend requires resolved ${type} scale "${id}".`);
256
- }
257
- return scale;
258
- }
259
-
260
- export function resolveContinuousBounds(
261
- program,
262
- message = "Continuous legend layout requires Canvas bounds."
263
- ) {
264
- const plot = resolveGraphicBounds(program);
265
- const canvas = findCanvasGraphic(program);
266
- if (
267
- plot === undefined ||
268
- ![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
269
- canvas?.type !== "canvas" ||
270
- !Number.isFinite(canvas.properties.width) ||
271
- !Number.isFinite(canvas.properties.height)
272
- ) {
273
- throw new Error(message);
274
- }
275
- return { plot, canvas: canvas.properties };
276
- }
277
-
278
- export function sampleContinuousValues(domain, count) {
279
- return sampleNumericRange(
280
- domain[0],
281
- domain[1],
282
- count,
283
- "Continuous legend domain"
284
- );
285
- }
286
-
287
- export function formatContinuousValues(values, domain, fieldType, format = "auto") {
288
- const resolved = validateValueFormat(format, "Legend label format");
289
- if (resolved === "auto") {
290
- return fieldType === "temporal"
291
- ? values.map(value => formatTimeTick(value, domain))
292
- : formatDistinctNumericSamples(values);
293
- }
294
- return values.map(value => formatValue(value, {
295
- format: resolved,
296
- valueType: fieldType === "temporal" ? "temporal" : "quantitative",
297
- label: "Legend label format"
298
- }));
299
- }
300
-
301
- export function styleContinuousText(
302
- program,
303
- id,
304
- style,
305
- { align = "left" } = {}
306
- ) {
307
- return editGraphicProperties(program, id, {
308
- fill: style.color,
309
- fontSize: style.fontSize,
310
- fontFamily: style.fontFamily,
311
- fontWeight: style.fontWeight,
312
- textAlign: align,
313
- textBaseline: "middle"
314
- });
315
- }
316
-
317
- export function resolveLegendTextBounds(position, text, style, profile) {
318
- return resolveTextBounds({
319
- x: position.x,
320
- y: position.y,
321
- text: String(text),
322
- ...style,
323
- textAlign: position.align ?? "left",
324
- textBaseline: "middle"
325
- }, profile);
326
- }
327
-
328
- export function assertLegendBoundsInsideCanvas(bounds, canvas, label, config) {
329
- // Horizontal content is intrinsic until the shared lane places its actual
330
- // concrete bounds. Only that final placement can determine Canvas fit.
331
- if (isHorizontalEdgeLegend(config)) return;
332
- if (bounds.some(item =>
333
- item.left < 0 || item.right > canvas.width ||
334
- item.top < 0 || item.bottom > canvas.height
335
- )) {
336
- throw new Error(`${label} requires more Canvas margin space.`);
337
- }
338
- }
339
-
340
- export function resolveLegendBackgroundFromBounds(
341
- bounds,
342
- border,
343
- canvas,
344
- label,
345
- config
346
- ) {
347
- if (border === false) return undefined;
348
- const strokeExtent = border.lineWidth / 2;
349
- let x = Infinity;
350
- let y = Infinity;
351
- let right = -Infinity;
352
- let bottom = -Infinity;
353
- for (const item of bounds) {
354
- x = Math.min(x, item.left);
355
- y = Math.min(y, item.top);
356
- right = Math.max(right, item.right);
357
- bottom = Math.max(bottom, item.bottom);
358
- }
359
- x -= border.padding;
360
- y -= border.padding;
361
- right += border.padding;
362
- bottom += border.padding;
363
- if (!isHorizontalEdgeLegend(config) && (
364
- x - strokeExtent < 0 || y - strokeExtent < 0 ||
365
- right + strokeExtent > canvas.width ||
366
- bottom + strokeExtent > canvas.height
367
- )) {
368
- throw new Error(`${label} background requires more Canvas margin space.`);
369
- }
370
- return { x, y, width: right - x, height: bottom - y };
371
- }
372
-
373
- export function editLegendBackground(program, id, bounds, border) {
374
- if (bounds === undefined) return program;
375
- return editGraphicProperties(program, id, {
376
- ...bounds,
377
- fill: border.background,
378
- stroke: border.color,
379
- strokeWidth: border.lineWidth
380
- });
381
- }
382
-
383
- export function normalizeItemLegendLayout(args) {
384
- const position = args.position ?? "right";
385
- if (!["right", "left", "top", "bottom"].includes(position)) throw new Error(`Unsupported legend position "${position}".`);
386
- const side = ["left", "right"].includes(position);
387
- const layout = args.layout === undefined ? "edge" : args.layout;
388
- if (layout !== "edge") throw new Error('This legend requires layout "edge".');
389
- const align = args.align ?? "center";
390
- if (!["left", "center", "right"].includes(align)) throw new Error(`Unsupported legend alignment "${align}".`);
391
- const direction = args.direction ?? (side ? "vertical" : "horizontal");
392
- if (!["horizontal", "vertical"].includes(direction)) throw new Error(`Unsupported legend direction "${direction}".`);
393
- const columns = args.columns;
394
- if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) throw new RangeError("Legend columns must be a positive integer.");
395
- const titlePosition = args.titlePosition ?? "top";
396
- if (!["top", "left"].includes(titlePosition)) throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
397
- if (side && (direction !== "vertical" || align !== "center" || (columns !== undefined && columns !== 1) || titlePosition !== "top")) {
398
- throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");
399
- }
400
- const offset = args.offset ?? 30;
401
- const itemGap = args.itemGap ?? 28;
402
- validateNonNegative(offset, "Legend offset");
403
- validatePositive(itemGap, "Legend itemGap");
404
- return { position, layout, align, direction, columns, titlePosition, offset, itemGap };
405
- }
406
-
407
- export function materializeItemLegend(program, kind, config, layout, {
408
- symbols, optional = {}, text = layout.labels,
409
- labels = layout.config.labels, titleStyle = layout.config.titleStyle
410
- }) {
411
- const prefix = `${kind}Legend`;
412
- let next = program
413
- .editSemantic({ property: `guide.legend.${kind}.scale`, value: config.scale })
414
- .editSemantic({ property: `guide.legend.${kind}.title`, value: config.title })
415
- ._withLegendConfig(kind, config);
416
- next = editGraphicProperties(next, `${prefix}Symbols`, {
417
- length: text.length,
418
- ...symbols
419
- });
420
- next = editGraphicProperties(next, `${prefix}Labels`, {
421
- length: text.length, x: layout.labelX, y: layout.itemY, text
422
- });
423
- next = editLegendBackground(next, `${prefix}Background`, layout.background, config.border);
424
- next = editGraphicProperties(next, `${prefix}Symbols`, Object.fromEntries(
425
- Object.entries(optional).filter(([, value]) => value !== undefined)
426
- ));
427
- next = styleContinuousText(next, `${prefix}Labels`, labels);
428
- const effective = layout.config;
429
- if (effective.titleVisible === false) return next;
430
- next = editGraphicProperties(next, `${prefix}Title`, {
431
- x: layout.title.x, y: layout.title.y, text: effective.title
432
- });
433
- return styleContinuousText(next, `${prefix}Title`, titleStyle, { align: layout.title.align });
434
- }
435
-
436
- export function createItemLegendGraphics(program, kind, config, count, type, placement) {
437
- const prefix = `${kind}Legend`;
438
- let next = program
439
- .editSemantic({ property: `guide.legend.${kind}.scale`, value: config.scale })
440
- .editSemantic({ property: `guide.legend.${kind}.title`, value: config.title })
441
- ._withLegendConfig(kind, config);
442
- if (config.border !== false) next = next.createGraphics({ id: `${prefix}Background`, type: "rect", ...placement });
443
- next = next.createGraphics({ id: `${prefix}Symbols`, type, length: count, ...placement })
444
- .createGraphics({ id: `${prefix}Labels`, type: "text", length: count, ...placement });
445
- if (config.titleVisible !== false) next = next.createGraphics({ id: `${prefix}Title`, type: "text", ...placement });
446
- return next;
447
- }
1
+ import{LEGEND_TITLE_STYLE_KEYS as A,mergeLegendTitleStyle as W,validateLegendTitleAlignment as x}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as k,isPlotLegend as p}from"../../../../layout/legend.js";import{isContinuousColorScaleType as D}from"../../../../grammar/scales/types.js";import{isPlainObject as T}from"../../../../core/immutable.js";import{resolveTextBounds as U}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as R}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as M}from"../../../../materialization/legendText.js";import{validateKeys as S,validateFontWeight as q,validateOptionObject as O,validateNonEmptyString as h,validateNonNegativeFinite as V,validatePositiveFinite as Y,validateGeneratedItemLimit as K}from"../../../../core/validation.js";import{formatTimeTick as X}from"../../../../grammar/ticks.js";import{formatValue as H,validateValueFormat as $}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as J,sampleNumericRange as Q}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as Z,canvasOverflowError as v}from"../../../../layout/canvas.js";import{isHorizontalEdgeLegend as I}from"../../../../layout/legendLane.js";import{DEFAULT_COLORS as m,DEFAULT_FONT_FAMILY as G,DEFAULT_VERTICAL_LEGEND_ITEM_GAP as L}from"../../../../theme/defaults.js";import{findLayer as ee}from"../../../../selectors/layers.js";import{isOpacityLegendLayer as te}from"../../../../materialization/legends.js";import{findCanvasGraphic as ne}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as ie}from"../sampling.js";import{editGraphicProperties as u}from"../../../primitives/graphicProperties.js";const oe=["target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","border","direction","columns","titlePosition"],ze=Object.freeze(["target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","title","labels","titleStyle","border","overflow"]),re=["offset","color","fontSize","fontFamily","fontWeight","format"],le=["color","lineWidth","padding","background"],_=["right","left","top","bottom",...k],b={offset:12,color:m.text,fontSize:12,fontFamily:G,fontWeight:"normal"},z={color:m.text,fontSize:13,fontFamily:G,fontWeight:600},de=Object.freeze(b),se=Object.freeze({...z,color:m.strongText});function N(e,t){return e!==void 0&&e!==!1&&h(e,"Legend title"),{title:e===!1||e===void 0?t:e,inferredTitle:e===!1||e===void 0,titleVisible:e!==!1}}function Ne(e,t,n){const i=["right","left"].includes(e.position??"right")||p(e),r=(e.direction??(i?"vertical":"horizontal"))==="vertical"?L:n;return{...ce({...e,itemGap:e.itemGap??r}),...N(e.title,t.field),labels:w(e.labels,"createLegend.labels",de),titleStyle:C(e.titleStyle,"createLegend.titleStyle",se),border:F(e.border)}}const ae={color:m.border,lineWidth:1,padding:12,background:"transparent"},y=Y,g=V;function w(e,t,n){if(e===void 0)return{...n};if(!T(e))throw new TypeError(`${t} must be a plain object.`);S(e,re,t);const i={...n,...e};Object.hasOwn(i,"offset")&&g(i.offset,`${t} offset`),y(i.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])h(i[o],`${t} ${o}`);return q(i.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(i,"format")&&(i.format=$(i.format,`${t} format`)),i}function C(e,t,n,i=!1){e!==void 0&&O(e,A,t);const o=W(n,e,{edit:i}),{align:r,...d}=o;return{...w(d,t,{}),...r===void 0?{}:{align:r}}}function F(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!T(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&S(e,le,"createLegend.border");const t={...ae,...e===!0?{}:e};for(const n of["color","background"])h(t[n],`Legend border ${n}`);return g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding"),t}function Ce(e,t){O(e,oe,"createLegend");const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=e.align??"center";if(!["left","center","right"].includes(i))throw new Error(`Unsupported legend alignment "${i}".`);if(["left","right"].includes(n)&&i!=="center")throw new Error("Side continuous legends require center alignment.");if(t==="gradient"&&Object.hasOwn(e,"values"))throw new Error("Gradient legends do not support exact sample values.");const o=t==="opacity"?ie(e,{operation:"create",label:"Opacity legend"}):void 0,r=t==="gradient"?e.count??5:void 0;if(t==="gradient"){if(!Number.isInteger(r)||r<2)throw new RangeError("Continuous legend count must be an integer of at least 2.");K(r,"Continuous legend count")}const d=e.offset??(p({position:n})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(x(e.titleStyle,l),!["top","left"].includes(l))throw new Error(`Unsupported legend titlePosition "${l}".`);if(l==="left"&&(!["top","bottom"].includes(n)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const a=e.itemGap??(t==="opacity"&&(["right","left"].includes(n)||p({position:n}))?L:l==="left"?20:28);if(y(a,"Legend itemGap"),t==="gradient"){for(const s of["symbol","columns","direction","itemGap"])if(Object.hasOwn(e,s))throw new Error(`Gradient legend does not accept ${s}.`)}else for(const s of["columns","direction","gradient"])if(Object.hasOwn(e,s))throw new Error(`Opacity legend does not accept ${s}.`);return{target:e.target,position:n,align:i,offset:d,...t==="gradient"?{count:r}:{sampling:o},...N(e.title),labels:w(e.labels,"createLegend.labels",l==="left"?{...b,offset:8}:b),titleStyle:C(e.titleStyle,"createLegend.titleStyle",z),itemGap:a,titlePosition:l,border:F(e.border)}}function P(e,t,n){const i=e.semanticSpec.layers.filter(n);if(t===void 0)return i.length===1?i[0]:void 0;const o=ee(e,t);return i.includes(o)?o:void 0}function Fe(e,t,n){const i=P(e,t,n==="opacity"?te:o=>o.mark?.type==="point"&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?`${n} legend requires one eligible ${n==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${n} legend target "${t}".`);return i}function Pe(e,t,n="color"){const i=P(e,t,o=>(n==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","bar","rect","text"]).includes(o.mark?.type)&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?n==="color"?"color legend requires one eligible point, bar, or rect mark.":"stroke legend requires one eligible mark.":`Unknown ${n} legend target "${t}".`);return i}function Be(e,t,n){const i=e.resolvedScales[t];if(n==="sequential"?!D(i?.type):i?.type!==n)throw new Error(`Legend requires resolved ${n} scale "${t}".`);return i}function je(e,t="Continuous legend layout requires Canvas bounds."){const n=Z(e),i=ne(e);if(n===void 0||![n.x,n.y,n.width,n.height].every(Number.isFinite)||i?.type!=="canvas"||!Number.isFinite(i.properties.width)||!Number.isFinite(i.properties.height))throw new Error(t);return{plot:n,canvas:i.properties}}function Ae(e,t){return Q(e[0],e[1],t,"Continuous legend domain")}function We(e,t,n,i="auto"){const o=$(i,"Legend label format");return o==="auto"?n==="temporal"?e.map(r=>X(r,t)):J(e):e.map(r=>H(r,{format:o,valueType:n==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function B(e,t,n,{align:i="left"}={}){const o=u(e,t,{fill:n.color,fontSize:n.fontSize,fontFamily:n.fontFamily,fontWeight:n.fontWeight,textAlign:i,textBaseline:"middle"});return t.endsWith("Title")?M(o,t,o.graphicSpec.objects[t].properties.text,n):o}function ke(e,t,n,i,{hardBreaks:o=!1}={}){return U({x:e.x,y:e.y,text:String(t),...n,...o?{lines:R(t,n,i).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},i)}function De(e,t,n,i){if(!(i?.groupIntrinsic||I(i)||["left","right"].includes(i?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw v(`${n} requires more Canvas margin space.`,e,t)}function Ue(e,t,n,i,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,a=-1/0,s=-1/0;for(const c of e)d=Math.min(d,c.left),l=Math.min(l,c.top),a=Math.max(a,c.right),s=Math.max(s,c.bottom);if(d-=t.padding,l-=t.padding,a+=t.padding,s+=t.padding,!o?.groupIntrinsic&&!I(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||a+r>n.width||s+r>n.height))throw v(`${i} background requires more Canvas margin space.`,[{left:d-r,right:a+r,top:l-r,bottom:s+r}],n);return{x:d,y:l,width:a-d,height:s-l}}function fe(e,t,n,i){return n===void 0?e:u(e,t,{...n,fill:i.background,stroke:i.color,strokeWidth:i.lineWidth})}function ce(e,{multiColumnSide:t=!1}={}){const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=["left","right"].includes(n)||p({position:n}),o=e.layout===void 0?"edge":e.layout;if(o!=="edge")throw new Error('This legend requires layout "edge".');const r=e.align??"center";if(!["left","center","right"].includes(r))throw new Error(`Unsupported legend alignment "${r}".`);const d=e.direction??(i?"vertical":"horizontal");if(!["horizontal","vertical"].includes(d))throw new Error(`Unsupported legend direction "${d}".`);const l=e.columns;if(l!==void 0&&(!Number.isInteger(l)||l<1))throw new RangeError("Legend columns must be a positive integer.");const a=e.titlePosition??"top";if(x(e.titleStyle,a),!["top","left"].includes(a))throw new Error(`Unsupported legend titlePosition "${a}".`);if(i&&(r!=="center"||a!=="top"||!t&&(d!=="vertical"||l!==void 0&&l!==1)))throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");const s=e.offset??(p({position:n})?8:30),c=e.itemGap??(d==="vertical"?L:28);return g(s,"Legend offset"),y(c,"Legend itemGap"),{position:n,layout:o,align:r,direction:d,columns:l,titlePosition:a,offset:s,itemGap:c}}function Re(e,t,n,i,{symbols:o,optional:r={},text:d=i.labels,labels:l=i.config.labels,titleStyle:a=i.config.titleStyle}){const s=`${t}Legend`,c=Object.values(o).find(Array.isArray)?.length??d.length;let f=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);f=u(f,`${s}Symbols`,{length:c,...o}),f=u(f,`${s}Labels`,{length:d.length,x:i.labelX,y:i.itemY,text:d}),f=fe(f,`${s}Background`,i.background,n.border),f=u(f,`${s}Symbols`,Object.fromEntries(Object.entries(r).filter(([,j])=>j!==void 0))),f=B(f,`${s}Labels`,l);const E=i.config;return E.titleVisible===!1?f:(f=u(f,`${s}Title`,{x:i.title.x,y:i.title.y,text:E.title}),B(f,`${s}Title`,a,{align:i.title.align}))}function Me(e,t,n,i,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);return n.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:i,...r}).createGraphics({id:`${d}Labels`,type:"text",length:i,...r}),n.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{de as ITEM_LEGEND_LABELS,ze as ITEM_LEGEND_OPTIONS,se as ITEM_LEGEND_TITLE_STYLE,De as assertLegendBoundsInsideCanvas,Me as createItemLegendGraphics,u as editGraphicProperties,fe as editLegendBackground,We as formatContinuousValues,Re as materializeItemLegend,Ce as normalizeContinuousLegend,N as normalizeInitialLegendTitle,Ne as normalizeItemLegendConfig,ce as normalizeItemLegendLayout,F as normalizeLegendBorder,w as normalizeLegendTextOptions,C as normalizeLegendTitleOptions,Be as requireResolvedLegendScale,je as resolveContinuousBounds,Pe as resolveContinuousColorLayer,Fe as resolveContinuousLegendLayer,Ue as resolveLegendBackgroundFromBounds,ke as resolveLegendTextBounds,Ae as sampleContinuousValues,P as selectLegendLayer,B as styleContinuousText,g as validateNonNegative,y as validatePositive};
@@ -1,6 +1,10 @@
1
+ import { normalizeColorSymbols, resolveColorSymbolsLayout, materializeColorSymbols } from "./colorSymbols.js";
2
+ import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
3
+ import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
4
+ import { mapContinuousScaleValues, transformedTicks } from "../../../../grammar/scales/index.js";
1
5
  import { action, closedAction } from "../../../../core/action.js";
2
6
  import { isPlainObject } from "../../../../core/immutable.js";
3
- import { validateKeys } from "../../../../core/validation.js";
7
+ import { normalizeLegendGradient, DEFAULT_LEGEND_GRADIENT } from "../../../../grammar/legendGradient.js";
4
8
  import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
5
9
  import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
6
10
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
@@ -16,31 +20,32 @@ import {
16
20
  resolveLegendBackgroundFromBounds,
17
21
  resolveLegendTextBounds,
18
22
  sampleContinuousValues,
19
- styleContinuousText,
20
- validatePositive
23
+ styleContinuousText
21
24
  } from "./common.js";
22
25
  import { resolveLegendGraphicPlacement } from
23
26
  "../../../../materialization/graphicHierarchy.js";
24
27
  import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
25
28
 
26
- export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12 });
29
+ export const DEFAULT_GRADIENT_SIZE = DEFAULT_LEGEND_GRADIENT;
27
30
 
28
- const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
29
-
30
- export function resolveGradientLayout(program, config, scale, label = "Gradient legend") {
31
+ export function resolveGradientLayout(program, config, scale, label = "Gradient legend", stripStrokeWidth = 0) {
32
+ if (config.representation === "symbols") return resolveColorSymbolsLayout(program, config, scale);
33
+ config = intrinsicLegendConfig(program, config);
31
34
  const { plot, canvas } = resolveContinuousBounds(program);
32
35
  const vertical = ["right", "left"].includes(config.position);
33
36
  const length = config.gradient.length;
34
37
  const thickness = config.gradient.thickness;
38
+ const titleText = config.titleVisible === false ? { height: 0, extraHeight: 0 }
39
+ : resolveLegendTitleText(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
35
40
  const titleGap = config.blockGap ?? 12;
36
41
  let x;
37
42
  let y;
38
43
  if (config.position === "right") {
39
44
  x = plot.x + plot.width + config.offset;
40
- y = plot.y + 46 + titleGap - 12;
45
+ y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
41
46
  } else if (config.position === "left") {
42
47
  x = plot.x - config.offset - thickness;
43
- y = plot.y + 46 + titleGap - 12;
48
+ y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
44
49
  } else {
45
50
  x = config.align === "left" ? plot.x
46
51
  : config.align === "right" ? plot.x + plot.width - length
@@ -49,16 +54,21 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
49
54
  ? plot.y - config.offset - thickness - config.labels.offset -
50
55
  config.labels.fontSize
51
56
  : plot.y + plot.height + config.offset +
52
- (config.titleVisible === false ? 0 : config.titleStyle.fontSize + titleGap);
57
+ (config.titleVisible === false ? 0 : titleText.height + titleGap);
53
58
  }
54
- const title = vertical
59
+ let title = vertical
55
60
  ? { x, y: plot.y + 20, align: "left" }
56
61
  : {
57
62
  x: x + length / 2,
58
- y: y - titleGap - config.titleStyle.fontSize / 2,
63
+ y: y - titleGap - titleText.height / 2,
59
64
  align: "center"
60
65
  };
61
- const values = [...sampleContinuousValues(scale.domain, config.count)];
66
+ const transformed = ["log", "sqrt", "symlog"].includes(scale.type);
67
+ const values = transformed
68
+ ? transformedTicks(scale.type, scale.domain, config.count,
69
+ scale.type === "log" ? {base: scale.base}
70
+ : scale.type === "symlog" ? {constant: scale.constant} : {})
71
+ : [...sampleContinuousValues(scale.domain, config.count)];
62
72
  if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
63
73
  values.push(scale.midpoint);
64
74
  values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
@@ -69,7 +79,8 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
69
79
  config.fieldType,
70
80
  config.labels.format
71
81
  );
72
- const fractions = values.map((value, index) => scale.midpoint === undefined
82
+ const fractions = transformed ? mapContinuousScaleValues(values, {...scale, range: [0, 1]})
83
+ : values.map((value, index) => scale.midpoint === undefined
73
84
  ? index / (values.length - 1)
74
85
  : inverseLerp(value, ...scale.domain));
75
86
  const labelOffset = config.labels.offset;
@@ -114,11 +125,15 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
114
125
  const labelBounds = labels.map((label, index) =>
115
126
  resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
116
127
  );
128
+ if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
129
+ [{ left: stripBounds.left - stripStrokeWidth / 2, right: stripBounds.right + stripStrokeWidth / 2,
130
+ top: stripBounds.top - stripStrokeWidth / 2, bottom: stripBounds.bottom + stripStrokeWidth / 2 },
131
+ ...tickBounds, ...labelBounds], program.materializationConfigs.textMetrics);
117
132
  const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
118
133
  title,
119
134
  config.title,
120
135
  config.titleStyle
121
- , program.materializationConfigs.textMetrics);
136
+ , program.materializationConfigs.textMetrics, { hardBreaks: true });
122
137
  const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
123
138
  ...(config.titleVisible === false ? [] : [titleBounds])];
124
139
  assertLegendBoundsInsideCanvas(
@@ -173,14 +188,15 @@ export function materializeGradientLegend(program, resolved, appearance) {
173
188
  const prefix = `${channel}Gradient`;
174
189
  const { scale, encoding, config: currentConfig } = resolved;
175
190
  const config = resolveEffectiveLegendBlockConfig(program, kind, currentConfig);
191
+ if (!stroke && config.representation === "symbols") return materializeColorSymbols(program, resolved, config);
176
192
  const layout = resolveGradientLayout(program, config, scale,
177
- stroke ? "Stroke gradient legend" : "Gradient legend");
193
+ stroke ? "Stroke gradient legend" : "Gradient legend", appearance?.strokeWidth ?? 0);
178
194
  const stripCount = 60;
179
195
  const stripSize = layout.length / stripCount;
180
196
  const strips = Array.from({ length: stripCount }, (_, index) => {
181
197
  const fraction = (index + 0.5) / stripCount;
182
198
  const position = layout.vertical ? 1 - fraction : fraction;
183
- const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
199
+ const samplingScale = scale.midpoint === undefined ? { ...scale, type: "sequential", domain: [0, 1] } : scale;
184
200
  const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
185
201
  const [color] = mapScaleConsumerValues([value], samplingScale, channel);
186
202
  return {
@@ -204,6 +220,9 @@ export function materializeGradientLegend(program, resolved, appearance) {
204
220
  for (const property of ["x", "y", "width", "height", "fill", "stroke", "strokeWidth"]) {
205
221
  next = next.editGraphics({ target: `${prefix}Strips`, property, value: strips.map(strip => strip[property]) });
206
222
  }
223
+ if (config.gradient.opacity !== undefined) {
224
+ next = next.editGraphics({ target: `${prefix}Strips`, property: "opacity", value: config.gradient.opacity });
225
+ }
207
226
  next = next.editGraphics({ target: `${prefix}Ticks`, property: "length", value: layout.ticks.length });
208
227
  for (const property of ["x1", "y1", "x2", "y2"]) {
209
228
  next = next.editGraphics({ target: `${prefix}Ticks`, property, value: layout.ticks.map(tick => tick[property]) });
@@ -235,7 +254,10 @@ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
235
254
 
236
255
 
237
256
  export function resolveGradientLegendCreation(program, args = {}, channel = "color") {
238
- const config = normalizeContinuousLegend(args, "gradient");
257
+ const { representation, ...options } = args;
258
+ if (representation !== undefined && !["gradient", "symbols"].includes(representation)) throw new Error("Legend representation must be gradient or symbols.");
259
+ if (channel !== "color" && representation !== undefined) throw new Error("Legend representation requires continuous color.");
260
+ const config = representation === "symbols" ? normalizeColorSymbols(options) : normalizeContinuousLegend(options, "gradient");
239
261
  if (args.channels !== undefined && (
240
262
  !Array.isArray(args.channels) ||
241
263
  args.channels.length !== 1 ||
@@ -246,18 +268,7 @@ export function resolveGradientLegendCreation(program, args = {}, channel = "col
246
268
  if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
247
269
  throw new TypeError("createLegend.gradient must be a plain object.");
248
270
  }
249
- validateKeys(
250
- args.gradient ?? {},
251
- GRADIENT_OPTIONS,
252
- "createLegend.gradient"
253
- );
254
- config.gradient = {
255
- length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
256
- thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
257
- };
258
- config.titleVisible = true;
259
- validatePositive(config.gradient.length, "Gradient length");
260
- validatePositive(config.gradient.thickness, "Gradient thickness");
271
+ if (config.representation !== "symbols") config.gradient = normalizeLegendGradient(args.gradient);
261
272
  const resolved = resolveGradientConfig(program, config, channel);
262
273
  return resolved;
263
274
  }
@@ -284,7 +295,9 @@ export function createGradientLegendFromConfig(program, config, channel = "color
284
295
  ...resolveLegendGraphicPlacement(next)
285
296
  });
286
297
  }
287
- next = next
298
+ if (resolved.config.representation === "symbols") {
299
+ next = next.createGraphics({ id: `${prefix}Symbols`, type: "circle", length: 0, ...resolveLegendGraphicPlacement(next) });
300
+ } else next = next
288
301
  .createGraphics({
289
302
  id: `${prefix}Strips`,
290
303
  type: "rect",
@@ -298,8 +311,8 @@ export function createGradientLegendFromConfig(program, config, channel = "color
298
311
  type: "line",
299
312
  length: 0,
300
313
  ...resolveLegendGraphicPlacement(next)
301
- })
302
- .createGraphics({
314
+ });
315
+ next = next.createGraphics({
303
316
  id: `${prefix}Labels`,
304
317
  type: "text",
305
318
  length: 0,
@@ -323,7 +336,7 @@ export const createGradientLegend = /* @__PURE__ */ action(
323
336
  function (args = {}) {
324
337
  const resolved = resolveGradientLegendCreation(this, args);
325
338
  resolveGradientLayout(this, resolved.config, resolved.scale);
326
- if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
339
+ if (this.guideConfigs.legend?.gradient !== undefined) {
327
340
  throw new Error(
328
341
  "createGradientLegend requires a missing gradient legend."
329
342
  );