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,383 +1 @@
1
- import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
- import { MAX_GENERATED_ITEMS } from "../../core/validation.js";
3
- import { interpolateNumber, inverseLerp, numericExtent } from "../numeric.js";
4
- import { niceTicks } from "../ticks.js";
5
- import { niceLinearDomain } from "./continuous.js";
6
- import {
7
- SCALE_ROLES,
8
- validateScaleTypeForRole
9
- } from "./types.js";
10
- import { validatePair } from "./validation.js";
11
- const TRANSFORMED_TYPES = Object.freeze(["log", "pow", "sqrt", "symlog"]);
12
-
13
- function positiveFinite(value, label) {
14
- if (!Number.isFinite(value) || value <= 0) {
15
- throw new RangeError(`${label} must be a positive finite number.`);
16
- }
17
- return value;
18
- }
19
-
20
- export function normalizeTransformParameters(type, options = {}) {
21
- if (!TRANSFORMED_TYPES.includes(type) && type !== "linear") {
22
- throw new Error(`Scale type "${type}" is not a quantitative transform.`);
23
- }
24
- if (!isPlainObject(options)) {
25
- throw new TypeError("Scale transform options must be a plain object.");
26
- }
27
- const allowed = type === "log"
28
- ? ["base"]
29
- : type === "pow"
30
- ? ["exponent"]
31
- : type === "symlog" ? ["constant"] : [];
32
- for (const key of Object.keys(options)) {
33
- if (!allowed.includes(key)) {
34
- throw new Error(`Scale type "${type}" does not support ${key}.`);
35
- }
36
- }
37
- if (type === "log") {
38
- const base = positiveFinite(options.base ?? 10, "Log scale base");
39
- if (base === 1) throw new RangeError("Log scale base must not equal 1.");
40
- return cloneAndFreeze({ base });
41
- }
42
- if (type === "pow") {
43
- return cloneAndFreeze({
44
- exponent: positiveFinite(options.exponent ?? 1, "Power scale exponent")
45
- });
46
- }
47
- if (type === "sqrt") return cloneAndFreeze({ exponent: 0.5 });
48
- if (type === "symlog") {
49
- return cloneAndFreeze({
50
- constant: positiveFinite(options.constant ?? 1, "Symlog scale constant")
51
- });
52
- }
53
- return cloneAndFreeze({});
54
- }
55
-
56
- function signedTransformProportion(value, start, end, transform) {
57
- const transformed = [start, end, value].map(item =>
58
- Math.sign(item) * transform(Math.abs(item))
59
- );
60
- return transformed[0] !== transformed[1] &&
61
- transformed.every(Number.isFinite)
62
- ? inverseLerp(transformed[2], transformed[0], transformed[1])
63
- : undefined;
64
- }
65
-
66
- function powerProportion(value, domain, exponent) {
67
- const [start, end] = domain;
68
- const direct = signedTransformProportion(
69
- value, start, end, magnitude => magnitude ** exponent
70
- );
71
- if (direct !== undefined) return direct;
72
- const scale = Math.max(Math.abs(start), Math.abs(end));
73
- const normalized = signedTransformProportion(
74
- value,
75
- start,
76
- end,
77
- magnitude => magnitude === 0 ? 0 : Math.exp(exponent * (
78
- Math.log(magnitude) - Math.log(scale)
79
- ))
80
- );
81
- if (normalized !== undefined) return normalized;
82
-
83
- if (
84
- start !== 0 &&
85
- end !== 0 &&
86
- value !== 0 &&
87
- Math.sign(start) === Math.sign(end)
88
- ) {
89
- const endLogRatio = Math.log(Math.abs(end)) - Math.log(Math.abs(start));
90
- const valueLogRatio = Math.log(Math.abs(value)) - Math.log(Math.abs(start));
91
- if (Math.abs(exponent * endLogRatio) < 1e-8) {
92
- return valueLogRatio / endLogRatio;
93
- }
94
- const proportion = Math.expm1(exponent * valueLogRatio) /
95
- Math.expm1(exponent * endLogRatio);
96
- if (Number.isFinite(proportion)) return proportion;
97
- }
98
-
99
- return inverseLerp(value, start, end);
100
- }
101
-
102
- function logOnePlusRatio(value, constant) {
103
- if (value === 0) return 0;
104
- const ratio = Math.abs(value) / constant;
105
- if (Number.isFinite(ratio)) return Math.sign(value) * Math.log1p(ratio);
106
- const logRatio = Math.log(Math.abs(value)) - Math.log(constant);
107
- return Math.sign(value) * (
108
- Math.max(0, logRatio) + Math.log1p(Math.exp(-Math.abs(logRatio)))
109
- );
110
- }
111
-
112
- function transformedProportion(value, domain, type, parameters) {
113
- const [start, end] = domain;
114
- if (type === "linear") return inverseLerp(value, start, end);
115
- if (type === "log") {
116
- const logarithm = Math.log10(parameters.base);
117
- const direct = signedTransformProportion(
118
- value, start, end, magnitude => Math.log10(magnitude) / logarithm
119
- );
120
- return direct ?? signedTransformProportion(
121
- value, start, end, Math.log
122
- ) ?? inverseLerp(value, start, end);
123
- }
124
- if (type === "pow" || type === "sqrt") {
125
- return powerProportion(value, domain, parameters.exponent);
126
- }
127
-
128
- const transformedStart = logOnePlusRatio(start, parameters.constant);
129
- const transformedEnd = logOnePlusRatio(end, parameters.constant);
130
- if (transformedStart === transformedEnd) {
131
- return inverseLerp(value, start, end);
132
- }
133
- const proportion = inverseLerp(
134
- logOnePlusRatio(value, parameters.constant),
135
- transformedStart,
136
- transformedEnd
137
- );
138
- return Number.isFinite(proportion) ? proportion : inverseLerp(value, start, end);
139
- }
140
-
141
- export function validateTransformedDomain(type, domain, options = {}) {
142
- validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
143
- const validated = validatePair(domain, "Transformed scale domain");
144
- if (validated[0] === validated[1]) {
145
- throw new RangeError("Transformed scale domain values must be distinct.");
146
- }
147
- if (
148
- type === "log" &&
149
- (validated.includes(0) || Math.sign(validated[0]) !== Math.sign(validated[1]))
150
- ) {
151
- throw new RangeError(
152
- "Log scale domain must be strictly positive or strictly negative."
153
- );
154
- }
155
- normalizeTransformParameters(type, options);
156
- return validated;
157
- }
158
-
159
- function radixPower(radix, exponent, logarithm) {
160
- const direct = Number.isFinite(radix) ? radix ** exponent : Number.NaN;
161
- if (Number.isFinite(direct) && direct > 0) return direct;
162
- const transformed = exponent * logarithm;
163
- if (transformed > Math.log(Number.MAX_VALUE)) return Number.POSITIVE_INFINITY;
164
- if (transformed < Math.log(Number.MIN_VALUE)) return 0;
165
- return Math.exp(transformed);
166
- }
167
-
168
- function logDomainInfo(domain, base) {
169
- const low = Math.min(...domain);
170
- const high = Math.max(...domain);
171
- const negative = high < 0;
172
- return [
173
- base < 1 ? -Math.log(base) : Math.log(base),
174
- base < 1 ? 1 / base : base,
175
- domain[0] > domain[1],
176
- negative,
177
- negative ? Math.abs(high) : low,
178
- negative ? Math.abs(low) : high
179
- ];
180
- }
181
-
182
- function niceLogDomain(domain, base) {
183
- const [logarithm, radix, reversed, negative, minimum, maximum] =
184
- logDomainInfo(domain, base);
185
- let lower = radixPower(
186
- radix,
187
- Math.floor(Math.log(minimum) / logarithm),
188
- logarithm
189
- );
190
- let upper = radixPower(
191
- radix,
192
- Math.ceil(Math.log(maximum) / logarithm),
193
- logarithm
194
- );
195
- if (!Number.isFinite(lower) || lower <= 0 || lower > minimum) lower = minimum;
196
- if (!Number.isFinite(upper) || upper < maximum) upper = maximum;
197
- const result = negative ? [-upper, -lower] : [lower, upper];
198
- return cloneAndFreeze(reversed ? result.reverse() : result);
199
- }
200
-
201
- function padAutomaticTransformedDomain(type, value, options) {
202
- const parameters = normalizeTransformParameters(type, options);
203
- if (value === 0) {
204
- if (type === "log") {
205
- throw new RangeError(
206
- "Log scale domain must be strictly positive or strictly negative."
207
- );
208
- }
209
- return [-1, 1];
210
- }
211
- const magnitude = Math.abs(value);
212
- let lower = magnitude / 2;
213
- let upper = magnitude * 2;
214
- if (type === "pow" || type === "sqrt") {
215
- upper = magnitude * (2 - 2 ** -parameters.exponent) **
216
- (1 / parameters.exponent);
217
- } else if (type === "symlog") {
218
- const center = Math.abs(logOnePlusRatio(value, parameters.constant));
219
- if (center === 0) return [-1, 1];
220
- upper = parameters.constant * Math.expm1(
221
- 2 * center - logOnePlusRatio(lower, parameters.constant)
222
- );
223
- }
224
- if (!(lower < magnitude && upper > magnitude && Number.isFinite(upper))) {
225
- lower = magnitude / 2;
226
- upper = Number.isFinite(magnitude * 2) ? magnitude * 2 : magnitude;
227
- }
228
- if (type === "log" && lower === 0) lower = magnitude;
229
- return value > 0
230
- ? [lower, upper]
231
- : [-upper, lower === 0 ? 0 : -lower];
232
- }
233
-
234
- export function resolveTransformedDomain({
235
- type,
236
- domain = "auto",
237
- values,
238
- nice = false,
239
- zero = false,
240
- ...options
241
- }) {
242
- validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
243
- if (!Array.isArray(values) || !values.every(Number.isFinite)) {
244
- throw new TypeError("Transformed scale values must be finite numbers.");
245
- }
246
- if (typeof nice !== "boolean" || typeof zero !== "boolean") {
247
- throw new TypeError("Scale nice and zero must be booleans.");
248
- }
249
- if (type === "log" && zero) {
250
- throw new Error('Scale type "log" does not support zero.');
251
- }
252
- const explicit = domain !== "auto";
253
- if (explicit) return validateTransformedDomain(type, domain, options);
254
- if (values.length === 0) {
255
- throw new Error("Cannot infer an automatic scale domain from no values.");
256
- }
257
- let resolved = numericExtent(values);
258
- if (zero) resolved = [Math.min(0, resolved[0]), Math.max(0, resolved[1])];
259
- if (resolved[0] === resolved[1]) {
260
- resolved = padAutomaticTransformedDomain(type, resolved[0], options);
261
- }
262
- validateTransformedDomain(type, resolved, options);
263
- if (nice) {
264
- resolved = type === "log"
265
- ? niceLogDomain(resolved, normalizeTransformParameters(type, options).base)
266
- : niceLinearDomain(resolved);
267
- }
268
- return validateTransformedDomain(type, resolved, options);
269
- }
270
-
271
- export function resolveMappingRange(range, { reverse = false } = {}) {
272
- const validated = validatePair(range, "Scale range");
273
- if (typeof reverse !== "boolean") {
274
- throw new TypeError("Scale reverse must be a boolean.");
275
- }
276
- return reverse
277
- ? cloneAndFreeze([...validated].reverse())
278
- : validated;
279
- }
280
-
281
- export function mapTransformedValues(
282
- values,
283
- domain,
284
- range,
285
- { type = "linear", clamp = false, reverse = false, ...options } = {}
286
- ) {
287
- if (!Array.isArray(values)) {
288
- throw new TypeError("Scale values must be an array.");
289
- }
290
- if (typeof clamp !== "boolean") {
291
- throw new TypeError("Scale clamp must be a boolean.");
292
- }
293
- const hasUnknown = Object.hasOwn(options, "unknown");
294
- const { unknown, ...transformOptions } = options;
295
- const parameters = normalizeTransformParameters(type, transformOptions);
296
- const validatedDomain = validateTransformedDomain(type, domain, transformOptions);
297
- const resolvedRange = resolveMappingRange(range, { reverse });
298
- const span = resolvedRange[1] - resolvedRange[0];
299
- return cloneAndFreeze(values.map(value => {
300
- const valid = Number.isFinite(value) && !(type === "log" && (
301
- value === 0 || Math.sign(value) !== Math.sign(validatedDomain[0])
302
- ));
303
- if (!valid) {
304
- if (hasUnknown) return unknown;
305
- throw new TypeError(`Scale type "${type}" received an invalid value.`);
306
- }
307
- let proportion = transformedProportion(
308
- value,
309
- validatedDomain,
310
- type,
311
- parameters
312
- );
313
- if (clamp) proportion = Math.max(0, Math.min(1, proportion));
314
- const direct = resolvedRange[0] + proportion * span;
315
- return Number.isFinite(span) && Number.isFinite(direct) && (
316
- proportion !== 1 ||
317
- Math.abs(direct - resolvedRange[1]) <=
318
- Number.EPSILON * Math.abs(resolvedRange[1])
319
- )
320
- ? direct
321
- : interpolateNumber(resolvedRange[0], resolvedRange[1], proportion);
322
- }));
323
- }
324
-
325
- export function transformedTicks(type, domain, count, options = {}) {
326
- const validated = validateTransformedDomain(type, domain, options);
327
- if (!Number.isInteger(count) || count <= 0) {
328
- throw new RangeError("Tick count must be a positive integer.");
329
- }
330
- if (type !== "log") return niceTicks(validated, count);
331
- const { base } = normalizeTransformParameters(type, options);
332
- const [
333
- logarithm,
334
- radix,
335
- reversed,
336
- negative,
337
- minimumMagnitude,
338
- maximumMagnitude
339
- ] = logDomainInfo(validated, base);
340
- const start = Math.ceil(
341
- Math.log(minimumMagnitude) / logarithm - 1e-12
342
- );
343
- const end = Math.floor(
344
- Math.log(maximumMagnitude) / logarithm + 1e-12
345
- );
346
- const available = end - start + 1;
347
- const tickCount = available <= MAX_GENERATED_ITEMS
348
- ? available
349
- : Math.min(count, MAX_GENERATED_ITEMS);
350
- const values = [];
351
- for (let index = 0; index < tickCount; index += 1) {
352
- const exponent = available <= MAX_GENERATED_ITEMS
353
- ? start + index
354
- : Math.round(start + (end - start) * index / Math.max(1, tickCount - 1));
355
- const magnitude = radixPower(radix, exponent, logarithm);
356
- const value = (negative ? -1 : 1) * magnitude;
357
- if (
358
- Number.isFinite(value) &&
359
- magnitude >= minimumMagnitude &&
360
- magnitude <= maximumMagnitude &&
361
- (values.length === 0 || value !== values.at(-1))
362
- ) {
363
- values.push(value);
364
- }
365
- }
366
- if (negative) values.reverse();
367
- if (reversed) values.reverse();
368
- return cloneAndFreeze(values.length === 0 ? [...validated] : values);
369
- }
370
-
371
- export function formatTransformedTick(type, value) {
372
- if (type !== "log") return String(value);
373
- const magnitude = Math.abs(value);
374
- const units = [
375
- [1_000_000_000, "B"],
376
- [1_000_000, "M"],
377
- [1_000, "K"]
378
- ];
379
- const unit = units.find(([threshold]) => magnitude >= threshold);
380
- if (unit === undefined) return String(value);
381
- const scaled = value / unit[0];
382
- return `${Number(scaled.toPrecision(3))}${unit[1]}`;
383
- }
1
+ import{cloneAndFreeze as h,isPlainObject as I}from"../../core/immutable.js";import{MAX_GENERATED_ITEMS as R}from"../../core/validation.js";import{interpolateNumber as D,inverseLerp as b,numericExtent as q}from"../numeric.js";import{niceTicks as $}from"../ticks.js";import{niceLinearDomain as j}from"./continuous.js";import{SCALE_ROLES as L,validateScaleTypeForRole as A}from"./types.js";import{validatePair as _}from"./validation.js";const z=Object.freeze(["log","pow","sqrt","symlog"]);function v(t,e){if(!Number.isFinite(t)||t<=0)throw new RangeError(`${e} must be a positive finite number.`);return t}function p(t,e={}){if(!z.includes(t)&&t!=="linear")throw new Error(`Scale type "${t}" is not a quantitative transform.`);if(!I(e))throw new TypeError("Scale transform options must be a plain object.");const o=t==="log"?["base"]:t==="pow"?["exponent"]:t==="symlog"?["constant"]:[];for(const n of Object.keys(e))if(!o.includes(n))throw new Error(`Scale type "${t}" does not support ${n}.`);if(t==="log"){const n=v(e.base??10,"Log scale base");if(n===1)throw new RangeError("Log scale base must not equal 1.");return h({base:n})}return t==="pow"?h({exponent:v(e.exponent??1,"Power scale exponent")}):t==="sqrt"?h({exponent:.5}):t==="symlog"?h({constant:v(e.constant??1,"Symlog scale constant")}):h({})}function E(t,e,o,n){const r=[e,o,t].map(i=>Math.sign(i)*n(Math.abs(i)));return r[0]!==r[1]&&r.every(Number.isFinite)?b(r[2],r[0],r[1]):void 0}function V(t,e,o){const[n,r]=e,i=E(t,n,r,c=>c**o);if(i!==void 0)return i;const s=Math.max(Math.abs(n),Math.abs(r)),a=E(t,n,r,c=>c===0?0:Math.exp(o*(Math.log(c)-Math.log(s))));if(a!==void 0)return a;if(n!==0&&r!==0&&t!==0&&Math.sign(n)===Math.sign(r)){const c=Math.log(Math.abs(r))-Math.log(Math.abs(n)),f=Math.log(Math.abs(t))-Math.log(Math.abs(n));if(Math.abs(o*c)<1e-8)return f/c;const u=Math.expm1(o*f)/Math.expm1(o*c);if(Number.isFinite(u))return u}return b(t,n,r)}function N(t,e){if(t===0)return 0;const o=Math.abs(t)/e;if(Number.isFinite(o))return Math.sign(t)*Math.log1p(o);const n=Math.log(Math.abs(t))-Math.log(e);return Math.sign(t)*(Math.max(0,n)+Math.log1p(Math.exp(-Math.abs(n))))}function C(t,e,o,n){const[r,i]=e;if(o==="linear")return b(t,r,i);if(o==="log"){const f=Math.log10(n.base);return E(t,r,i,d=>Math.log10(d)/f)??E(t,r,i,Math.log)??b(t,r,i)}if(o==="pow"||o==="sqrt")return V(t,e,n.exponent);const s=N(r,n.constant),a=N(i,n.constant);if(s===a)return b(t,r,i);const c=b(N(t,n.constant),s,a);return Number.isFinite(c)?c:b(t,r,i)}function x(t,e,o={}){A(t,L.quantitativePosition);const n=_(e,"Transformed scale domain");if(n[0]===n[1])throw new RangeError("Transformed scale domain values must be distinct.");if(t==="log"&&(n.includes(0)||Math.sign(n[0])!==Math.sign(n[1])))throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return p(t,o),n}function P(t,e,o){const n=Number.isFinite(t)?t**e:Number.NaN;if(Number.isFinite(n)&&n>0)return n;const r=e*o;return r>Math.log(Number.MAX_VALUE)?Number.POSITIVE_INFINITY:r<Math.log(Number.MIN_VALUE)?0:Math.exp(r)}function O(t,e){const o=Math.min(...t),n=Math.max(...t),r=n<0;return[e<1?-Math.log(e):Math.log(e),e<1?1/e:e,t[0]>t[1],r,r?Math.abs(n):o,r?Math.abs(o):n]}function U(t,e){const[o,n,r,i,s,a]=O(t,e);let c=P(n,Math.floor(Math.log(s)/o),o),f=P(n,Math.ceil(Math.log(a)/o),o);(!Number.isFinite(c)||c<=0||c>s)&&(c=s),(!Number.isFinite(f)||f<a)&&(f=a);const u=i?[-f,-c]:[c,f];return h(r?u.reverse():u)}function X(t,e,o){const n=p(t,o);if(e===0){if(t==="log")throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return[-1,1]}const r=Math.abs(e);let i=r/2,s=r*2;if(t==="pow"||t==="sqrt")s=r*(2-2**-n.exponent)**(1/n.exponent);else if(t==="symlog"){const a=Math.abs(N(e,n.constant));if(a===0)return[-1,1];s=n.constant*Math.expm1(2*a-N(i,n.constant))}return i<r&&s>r&&Number.isFinite(s)||(i=r/2,s=Number.isFinite(r*2)?r*2:r),t==="log"&&i===0&&(i=r),e>0?[i,s]:[-s,i===0?0:-i]}function W({type:t,domain:e="auto",values:o,nice:n=!1,zero:r=!1,...i}){if(A(t,L.quantitativePosition),!Array.isArray(o)||!o.every(Number.isFinite))throw new TypeError("Transformed scale values must be finite numbers.");if(typeof n!="boolean"||typeof r!="boolean")throw new TypeError("Scale nice and zero must be booleans.");if(t==="log"&&r)throw new Error('Scale type "log" does not support zero.');if(e!=="auto")return x(t,e,i);if(o.length===0)throw new Error("Cannot infer an automatic scale domain from no values.");let a=q(o);return r&&(a=[Math.min(0,a[0]),Math.max(0,a[1])]),a[0]===a[1]&&(a=X(t,a[0],i)),x(t,a,i),n&&(a=t==="log"?U(a,p(t,i).base):j(a)),x(t,a,i)}function Y(t,{reverse:e=!1}={}){const o=_(t,"Scale range");if(typeof e!="boolean")throw new TypeError("Scale reverse must be a boolean.");return e?h([...o].reverse()):o}function Z(t,e,o,{type:n="linear",clamp:r=!1,reverse:i=!1,...s}={}){if(!Array.isArray(t))throw new TypeError("Scale values must be an array.");if(typeof r!="boolean")throw new TypeError("Scale clamp must be a boolean.");const a=Object.hasOwn(s,"unknown"),{unknown:c,...f}=s,u=p(n,f),d=x(n,e,f),l=Y(o,{reverse:i}),w=l[1]-l[0];return h(t.map(g=>{if(!(Number.isFinite(g)&&!(n==="log"&&(g===0||Math.sign(g)!==Math.sign(d[0]))))){if(a)return c;throw new TypeError(`Scale type "${n}" received an invalid value.`)}let m=C(g,d,n,u);r&&(m=Math.max(0,Math.min(1,m)));const M=l[0]+m*w;return Number.isFinite(w)&&Number.isFinite(M)&&(m!==1||Math.abs(M-l[1])<=Number.EPSILON*Math.abs(l[1]))?M:D(l[0],l[1],m)}))}function tt(t,e,o,n={}){const r=x(t,e,n);if(!Number.isInteger(o)||o<=0)throw new RangeError("Tick count must be a positive integer.");if(t!=="log")return $(r,o);const{base:i}=p(t,n),[s,a,c,f,u,d]=O(r,i),l=Math.ceil(Math.log(u)/s-1e-12),w=Math.floor(Math.log(d)/s+1e-12),g=w-l+1,T=g<=R?g:Math.min(o,R),m=[];for(let M=0;M<T;M+=1){const k=g<=R?l+M:Math.round(l+(w-l)*M/Math.max(1,T-1)),S=P(a,k,s),F=(f?-1:1)*S;Number.isFinite(F)&&S>=u&&S<=d&&(m.length===0||F!==m.at(-1))&&m.push(F)}return f&&m.reverse(),c&&m.reverse(),h(m.length===0?[...r]:m)}function nt(t,e){if(t!=="log")return String(e);const o=Math.abs(e),r=[[1e9,"B"],[1e6,"M"],[1e3,"K"]].find(([s])=>o>=s);if(r===void 0)return String(e);const i=e/r[0];return`${Number(i.toPrecision(3))}${r[1]}`}export{nt as formatTransformedTick,Z as mapTransformedValues,p as normalizeTransformParameters,Y as resolveMappingRange,W as resolveTransformedDomain,tt as transformedTicks,x as validateTransformedDomain};
@@ -16,7 +16,7 @@ export const SCALE_TYPES_BY_ROLE = Object.freeze({
16
16
  [SCALE_ROLES.bandPosition]: Object.freeze(["band"]),
17
17
  [SCALE_ROLES.pointPosition]: Object.freeze(["point"]),
18
18
  [SCALE_ROLES.discreteAppearance]: Object.freeze(["ordinal"]),
19
- [SCALE_ROLES.continuousColor]: Object.freeze(["sequential"]),
19
+ [SCALE_ROLES.continuousColor]: Object.freeze(["sequential", "log", "sqrt", "symlog"]),
20
20
  [SCALE_ROLES.discretizedColor]: Object.freeze([
21
21
  "quantize", "quantile", "threshold"
22
22
  ])
@@ -1,66 +1 @@
1
- import { cloneAndFreeze } from "../../core/immutable.js";
2
- import { POSITION_CHANNELS } from "../../core/vocabulary.js";
3
- import { validateCompleteScaleType } from "./types.js";
4
-
5
- export function validatePair(value, label) {
6
- if (
7
- !Array.isArray(value) ||
8
- value.length !== 2 ||
9
- !value.every(Number.isFinite)
10
- ) {
11
- throw new TypeError(`${label} must be "auto" or two finite numbers.`);
12
- }
13
- return cloneAndFreeze(value);
14
- }
15
-
16
- export function validatePositionChannel(channel) {
17
- if (!POSITION_CHANNELS.includes(channel)) {
18
- throw new Error(`Unknown position channel "${channel}".`);
19
- }
20
- return channel;
21
- }
22
-
23
- export function validateScaleType(type) {
24
- return validateCompleteScaleType(type);
25
- }
26
-
27
- export function validateSemanticScaleType(type) {
28
- return validateCompleteScaleType(type);
29
- }
30
-
31
- export function validateLinearScaleType(type) {
32
- if (type !== "linear") {
33
- throw new Error(`Unsupported position scale type "${type}".`);
34
- }
35
- return type;
36
- }
37
-
38
- export function validateTimeScaleType(type) {
39
- if (type !== "time") {
40
- throw new Error(`Unsupported temporal scale type "${type}".`);
41
- }
42
- return type;
43
- }
44
-
45
- export function validateOrdinalScaleType(type) {
46
- if (type !== "ordinal") {
47
- throw new Error(`Unsupported color scale type "${type}".`);
48
- }
49
- return type;
50
- }
51
-
52
- export function validateScaleDomain(domain) {
53
- return domain === "auto" ? domain : validatePair(domain, "Scale domain");
54
- }
55
-
56
- export function validateScaleRange(range) {
57
- return range === "auto" ? range : validatePair(range, "Scale range");
58
- }
59
-
60
- export function validateSemanticScaleDomain(domain) {
61
- if (domain === "auto") return domain;
62
- if (!Array.isArray(domain) || domain.length === 0) {
63
- throw new TypeError("Scale domain must be \"auto\" or a non-empty array.");
64
- }
65
- return cloneAndFreeze(domain);
66
- }
1
+ import{cloneAndFreeze as o}from"../../core/immutable.js";import{POSITION_CHANNELS as c}from"../../core/vocabulary.js";import{validateCompleteScaleType as i}from"./types.js";function u(r,e,t=1){if(!Array.isArray(r)||r.length<t||!r.every(Number.isFinite))throw new TypeError(`${e} must contain finite numbers.`);return r}function s(r,e){if(u(r,e),r.some((t,n)=>n>0&&t<=r[n-1]))throw new RangeError(`${e} must be strictly increasing.`);return r}function a(r,e){if(!Array.isArray(r)||r.length!==2||!r.every(Number.isFinite))throw new TypeError(`${e} must be "auto" or two finite numbers.`);return o(r)}function m(r){if(!c.includes(r))throw new Error(`Unknown position channel "${r}".`);return r}function y(r){return i(r)}function w(r){return i(r)}function S(r){if(r!=="linear")throw new Error(`Unsupported position scale type "${r}".`);return r}function d(r){if(r!=="time")throw new Error(`Unsupported temporal scale type "${r}".`);return r}function h(r){if(r!=="ordinal")throw new Error(`Unsupported color scale type "${r}".`);return r}function x(r){return r==="auto"?r:a(r,"Scale domain")}function T(r){return r==="auto"?r:a(r,"Scale range")}function A(r){if(r==="auto")return r;if(!Array.isArray(r)||r.length===0)throw new TypeError('Scale domain must be "auto" or a non-empty array.');return o(r)}export{u as validateFiniteScaleArray,s as validateIncreasingScaleArray,S as validateLinearScaleType,h as validateOrdinalScaleType,a as validatePair,m as validatePositionChannel,x as validateScaleDomain,T as validateScaleRange,y as validateScaleType,A as validateSemanticScaleDomain,w as validateSemanticScaleType,d as validateTimeScaleType};
@@ -1,3 +1,5 @@
1
+ import { validateGeneratedItemLimit } from "../../core/validation.js";
2
+ import { validateFontStyle } from "../../core/font.js";
1
3
  import { isPlainObject } from "../../core/immutable.js";
2
4
  import { validatePathCommands } from "../pathCommands.js";
3
5
  import { validateFillPaint } from "../paint.js";
@@ -25,6 +27,14 @@ const TEXT_BASELINES = new Set([
25
27
  ]);
26
28
 
27
29
  export function validateConcreteGraphicValue(type, property, value) {
30
+ if (property === "lines") {
31
+ if (!Array.isArray(value) || value.length === 0 || value.some(line =>
32
+ !isPlainObject(line) || Object.keys(line).some(key => !["text", "x", "y"].includes(key)) ||
33
+ typeof line.text !== "string" || !Number.isFinite(line.x) || !Number.isFinite(line.y))) {
34
+ throw new TypeError("Text lines require non-empty resolved text/x/y records.");
35
+ }
36
+ validateGeneratedItemLimit(value.length, "Text line count");
37
+ }
28
38
  if (property === "fill") {
29
39
  if (typeof value === "string") {
30
40
  validateFillPaint(value, `${type}.fill`);
@@ -60,6 +70,7 @@ export function validateConcreteGraphicValue(type, property, value) {
60
70
  )) {
61
71
  throw new TypeError("text.fontWeight must be a non-empty string or finite number.");
62
72
  }
73
+ if (property === "fontStyle") validateFontStyle(value);
63
74
  if (property === "textAlign" && !TEXT_ALIGNS.has(value)) {
64
75
  throw new Error(`Unsupported text.textAlign "${value}".`);
65
76
  }
@@ -33,6 +33,7 @@ const GRAPHIC_PROPERTIES = Object.freeze({
33
33
  "fontSize",
34
34
  "fontFamily",
35
35
  "fontWeight",
36
+ "fontStyle",
36
37
  "text",
37
38
  "textAlign",
38
39
  "textBaseline",
@@ -85,10 +86,12 @@ const GRAPHIC_PROPERTIES = Object.freeze({
85
86
  "x",
86
87
  "y",
87
88
  "text",
89
+ "lines",
88
90
  "fill",
89
91
  "fontSize",
90
92
  "fontFamily",
91
93
  "fontWeight",
94
+ "fontStyle",
92
95
  "textAlign",
93
96
  "textBaseline",
94
97
  "rotation",
@@ -23,7 +23,7 @@ const ENCODING_PATHS = Object.freeze([
23
23
  "encoding.color.aggregate",
24
24
  "encoding.group.fields",
25
25
  "encoding.group.inferredFrom",
26
- ...["x", "y", "x2", "y2", "theta", "color"].map(channel =>
26
+ ...["x", "y", "x2", "y2", "theta", "color", "stroke"].map(channel =>
27
27
  `encoding.${channel}.temporalUnit`
28
28
  ),
29
29
  "encoding.pathOrder.field",
@@ -64,6 +64,7 @@ const ENTITY_PATHS = Object.freeze({
64
64
  "transform",
65
65
  "mark.type",
66
66
  "mark.missing",
67
+ "mark.orientation",
67
68
  "derivedBindings.regression",
68
69
  "layout.mode",
69
70
  ...ENCODING_PATHS
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { layoutSeriesPartition } from "./seriesLayout.js";
3
5
 
@@ -8,17 +10,7 @@ const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
8
10
  const MODES = new Set(["stack", "fill", "center", "diverging"]);
9
11
  const MAX_OUTPUT_ROWS = 10_000;
10
12
 
11
- function rejectUnknownKeys(value, supported, label) {
12
- const unknown = Object.keys(value).find(key => !supported.includes(key));
13
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
14
- }
15
13
 
16
- function requireField(value, label) {
17
- if (typeof value !== "string" || value.length === 0) {
18
- throw new TypeError(`${label} must be a non-empty string.`);
19
- }
20
- return value;
21
- }
22
14
 
23
15
  function normalizeAs(value, field) {
24
16
  const requested = value ?? {};
@@ -1,3 +1,4 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
1
2
  import { aggregateRows } from "./aggregate.js";
2
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
3
4
 
@@ -6,12 +7,6 @@ const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
6
7
  const POPULATIONS = new Set(["boundData", "visibleItems"]);
7
8
  const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
8
9
 
9
- function rejectUnknownKeys(value, supported, label) {
10
- const unknown = Object.keys(value).find(key => !supported.includes(key));
11
- if (unknown !== undefined) {
12
- throw new Error(`Unknown ${label} property "${unknown}".`);
13
- }
14
- }
15
10
 
16
11
  export function normalizeReferenceStatistic(value, label = "Reference statistic") {
17
12
  if (!isPlainObject(value)) {
@@ -1,3 +1,6 @@
1
+ import { scalarKey } from "./transformKeys.js";
2
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
+ import { requireStringValue as requireField } from "../core/validation.js";
1
4
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
5
  import {
3
6
  aggregateRows,
@@ -22,20 +25,6 @@ const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
22
25
  const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
23
26
  const MAX_OUTPUT_ROWS = 10_000;
24
27
 
25
- function rejectUnknownKeys(value, supported, label) {
26
- const unknown = Object.keys(value).find(key => !supported.includes(key));
27
- if (unknown !== undefined) {
28
- throw new Error(`Unknown ${label} property "${unknown}".`);
29
- }
30
- }
31
-
32
- function requireField(value, label) {
33
- if (typeof value !== "string" || value.length === 0) {
34
- throw new TypeError(`${label} must be a non-empty string.`);
35
- }
36
- return value;
37
- }
38
-
39
28
  function normalizeGroupBy(value) {
40
29
  if (value === undefined) return [];
41
30
  return Array.isArray(value) ? [...value] : [value];
@@ -131,18 +120,6 @@ export function validateSummaryTransform(transform) {
131
120
  return transform;
132
121
  }
133
122
 
134
- function scalarKey(value, label) {
135
- if (value === null) return "null";
136
- if (typeof value === "string") return `string:${value.length}:${value}`;
137
- if (typeof value === "boolean") return `boolean:${value}`;
138
- if (typeof value === "number" && Number.isFinite(value)) {
139
- return `number:${Object.is(value, -0) ? 0 : value}`;
140
- }
141
- throw new TypeError(
142
- `${label} must contain null, strings, booleans, or finite numbers.`
143
- );
144
- }
145
-
146
123
  function requireSourceFields(rows, transform) {
147
124
  if (rows.length === 0) return;
148
125
  const fields = new Set(rows.flatMap(row => Object.keys(row)));
@@ -11,6 +11,11 @@ export function isSourceOwnedText(layer) {
11
11
  return layer?.mark?.type === "text" && layer.source !== undefined;
12
12
  }
13
13
 
14
+ export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
15
+ "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
16
+ "align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
17
+ ]);
18
+
14
19
  export const DEFAULT_TEXT_MARK = cloneAndFreeze({
15
20
  fill: "#334155",
16
21
  opacity: 1,
@@ -49,6 +54,18 @@ export function formatTextValue(value, format = "auto") {
49
54
 
50
55
  export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
51
56
  const config = { ...base };
57
+ if (Object.hasOwn(options, "lineHeight")) {
58
+ if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
59
+ config.lineHeight = options.lineHeight;
60
+ }
61
+ if (Object.hasOwn(options, "blockAlign")) {
62
+ if (!["first", "middle"].includes(options.blockAlign)) throw new Error("Text blockAlign must be first or middle.");
63
+ config.blockAlign = options.blockAlign;
64
+ }
65
+ if (Object.hasOwn(options, "inheritColor")) {
66
+ if (![false, "fill", "stroke"].includes(options.inheritColor)) throw new Error("Text inheritColor must be fill, stroke, or false.");
67
+ config.inheritColor = options.inheritColor;
68
+ }
52
69
  const mapping = {
53
70
  fill: "fill",
54
71
  opacity: "opacity",
@@ -75,3 +92,5 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
75
92
  }
76
93
  return cloneAndFreeze(config);
77
94
  }
95
+
96
+ export { resolveTextLines } from "../core/textMetrics.js";