ggaction 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,221 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { isNominalValue } from "./fields.js";
3
+ import { validateColorRange, validateStrokeDashRange } from "./appearance.js";
4
+ import { validatePair, validateScaleRange } from "./validation.js";
5
+ import { resolveScaleRange } from "./continuous.js";
6
+
7
+ export function validateOrdinalDomain(domain) {
8
+ if (domain === "auto") return domain;
9
+ if (
10
+ !Array.isArray(domain) ||
11
+ domain.length === 0 ||
12
+ !domain.every(isNominalValue) ||
13
+ new Set(domain).size !== domain.length
14
+ ) {
15
+ throw new TypeError(
16
+ "Ordinal domain must be \"auto\" or unique nominal values."
17
+ );
18
+ }
19
+ return cloneAndFreeze(domain);
20
+ }
21
+
22
+ export function validateOrdinalRange(range) {
23
+ if (range === "auto") return range;
24
+ if (Array.isArray(range) && range.length === 2 && range.every(Number.isFinite)) {
25
+ return cloneAndFreeze(range);
26
+ }
27
+ if (Array.isArray(range) && range.every(item => typeof item === "string")) {
28
+ return validateColorRange(range);
29
+ }
30
+ if (Array.isArray(range) && range.every(Array.isArray)) {
31
+ return validateStrokeDashRange(range);
32
+ }
33
+ return validateColorRange(range);
34
+ }
35
+
36
+ export function resolveOrdinalDomain(domain, values) {
37
+ const validated = validateOrdinalDomain(domain);
38
+ if (validated !== "auto") return validated;
39
+ if (values.length === 0) {
40
+ throw new Error("Cannot infer an automatic ordinal domain from no values.");
41
+ }
42
+ return cloneAndFreeze([...new Set(values)]);
43
+ }
44
+
45
+ export function resolveOrdinalPositionScale({
46
+ domain,
47
+ values,
48
+ range,
49
+ channel,
50
+ bounds,
51
+ unknown
52
+ }) {
53
+ const resolvedDomain = resolveOrdinalDomain(domain, values);
54
+ const resolvedRange = range === "auto" && channel === "y"
55
+ ? cloneAndFreeze([bounds.y, bounds.y + bounds.height])
56
+ : resolveScaleRange(range, channel, bounds);
57
+ const domainValues = new Set(resolvedDomain);
58
+ for (const value of values) {
59
+ if (!domainValues.has(value) && unknown === undefined) {
60
+ throw new Error(`Value "${value}" is outside the ordinal domain.`);
61
+ }
62
+ }
63
+ const step = (resolvedRange[1] - resolvedRange[0]) / resolvedDomain.length;
64
+ return cloneAndFreeze({
65
+ type: "ordinal",
66
+ domain: resolvedDomain,
67
+ range: resolvedRange,
68
+ step,
69
+ bandwidth: Math.abs(step)
70
+ });
71
+ }
72
+
73
+ function validateAlign(value) {
74
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
75
+ throw new RangeError("Discrete scale align must be between 0 and 1.");
76
+ }
77
+ return value;
78
+ }
79
+
80
+ export function resolveDiscretePositionScale({
81
+ type,
82
+ domain,
83
+ values,
84
+ range,
85
+ channel,
86
+ bounds,
87
+ paddingInner = 0,
88
+ paddingOuter = 0,
89
+ padding = 0.5,
90
+ align = 0.5,
91
+ unknown
92
+ }) {
93
+ if (!["band", "point"].includes(type)) {
94
+ throw new Error(`Unsupported discrete position scale type "${type}".`);
95
+ }
96
+ const resolvedDomain = resolveOrdinalDomain(domain, values);
97
+ const resolvedRange = range === "auto" && channel === "y"
98
+ ? cloneAndFreeze([bounds.y, bounds.y + bounds.height])
99
+ : resolveScaleRange(range, channel, bounds);
100
+ const domainValues = new Set(resolvedDomain);
101
+ for (const value of values) {
102
+ if (!domainValues.has(value) && unknown === undefined) {
103
+ throw new Error(`Value "${value}" is outside the discrete domain.`);
104
+ }
105
+ }
106
+ validateAlign(align);
107
+ if (type === "band") {
108
+ if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
109
+ throw new RangeError(
110
+ "Band scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
111
+ );
112
+ }
113
+ if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
114
+ throw new RangeError(
115
+ "Band scale paddingOuter must be a non-negative finite number."
116
+ );
117
+ }
118
+ } else if (!Number.isFinite(padding) || padding < 0) {
119
+ throw new RangeError(
120
+ "Point scale padding must be a non-negative finite number."
121
+ );
122
+ }
123
+ const span = Math.abs(resolvedRange[1] - resolvedRange[0]);
124
+ const direction = Math.sign(resolvedRange[1] - resolvedRange[0]) || 1;
125
+ const count = resolvedDomain.length;
126
+ const denominator = type === "band"
127
+ ? Math.max(1, count - paddingInner + paddingOuter * 2)
128
+ : Math.max(1, count - 1 + padding * 2);
129
+ const step = direction * span / denominator;
130
+ const bandwidth = type === "band"
131
+ ? Math.abs(step) * (1 - paddingInner)
132
+ : 0;
133
+ const outerPadding = type === "band" ? paddingOuter : padding;
134
+ const start = resolvedRange[0] +
135
+ direction * Math.abs(step) * outerPadding * 2 * align;
136
+ return cloneAndFreeze({
137
+ type,
138
+ domain: resolvedDomain,
139
+ range: resolvedRange,
140
+ step,
141
+ start,
142
+ bandwidth,
143
+ align,
144
+ ...(type === "band" ? { paddingInner, paddingOuter } : { padding })
145
+ });
146
+ }
147
+
148
+ export function resolveOrdinalOffsetScale({
149
+ domain,
150
+ values,
151
+ range,
152
+ parentBandwidth,
153
+ paddingInner = 0,
154
+ paddingOuter = 0
155
+ }) {
156
+ const resolvedDomain = resolveOrdinalDomain(domain, values);
157
+ if (!Number.isFinite(parentBandwidth) || parentBandwidth <= 0) {
158
+ throw new Error("Automatic xOffset range requires a positive x bandwidth.");
159
+ }
160
+ const resolvedRange = range === "auto"
161
+ ? validatePair([0, parentBandwidth], "Offset scale range")
162
+ : validateScaleRange(range);
163
+ if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
164
+ throw new RangeError(
165
+ "Offset scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
166
+ );
167
+ }
168
+ if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
169
+ throw new RangeError(
170
+ "Offset scale paddingOuter must be a non-negative finite number."
171
+ );
172
+ }
173
+ const domainValues = new Set(resolvedDomain);
174
+ for (const value of values) {
175
+ if (!domainValues.has(value)) {
176
+ throw new Error(`Value "${value}" is outside the ordinal domain.`);
177
+ }
178
+ }
179
+ const step = (resolvedRange[1] - resolvedRange[0]) /
180
+ Math.max(1, resolvedDomain.length - paddingInner + paddingOuter * 2);
181
+ const bandwidth = Math.abs(step) * (1 - paddingInner);
182
+ if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
183
+ throw new Error("Offset scale padding must leave a positive bandwidth.");
184
+ }
185
+ return cloneAndFreeze({
186
+ type: "ordinal",
187
+ domain: resolvedDomain,
188
+ range: resolvedRange,
189
+ step,
190
+ start: resolvedRange[0] + step * paddingOuter,
191
+ bandwidth,
192
+ paddingInner,
193
+ paddingOuter
194
+ });
195
+ }
196
+
197
+ export function mapOrdinalPositionValues(values, scale) {
198
+ const hasUnknown = Object.hasOwn(scale, "unknown");
199
+ const indices = new Map(scale.domain.map((value, index) => [value, index]));
200
+ return cloneAndFreeze(values.map(value => {
201
+ const index = indices.get(value);
202
+ if (index === undefined) {
203
+ if (hasUnknown) return scale.unknown;
204
+ throw new Error(`Value "${value}" is outside the ordinal domain.`);
205
+ }
206
+ if (
207
+ (scale.type === "band" &&
208
+ scale.paddingInner === 0 &&
209
+ scale.paddingOuter === 0 &&
210
+ scale.align === 0.5) ||
211
+ (scale.type === "point" && scale.padding === 0.5 && scale.align === 0.5)
212
+ ) {
213
+ return scale.range[0] + (index + 0.5) * scale.step;
214
+ }
215
+ if (["band", "point"].includes(scale.type)) {
216
+ const direction = Math.sign(scale.step) || 1;
217
+ return scale.start + index * scale.step + direction * scale.bandwidth / 2;
218
+ }
219
+ return scale.range[0] + (index + 0.5) * scale.step;
220
+ }));
221
+ }
@@ -0,0 +1,50 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { POINT_SHAPES } from "../pointShapes.js";
3
+ import { normalizeStrokeDashPattern } from "./appearance.js";
4
+
5
+ export function validateScaleUnknown(channel, value) {
6
+ if (["x", "y", "xOffset"].includes(channel)) {
7
+ if (!Number.isFinite(value)) {
8
+ throw new TypeError(`Scale unknown for ${channel} must be a finite number.`);
9
+ }
10
+ return value;
11
+ }
12
+ if (channel === "color") {
13
+ if (typeof value !== "string" || value.length === 0) {
14
+ throw new TypeError("Scale unknown for color must be a non-empty color string.");
15
+ }
16
+ return value;
17
+ }
18
+ if (channel === "size") {
19
+ if (!Number.isFinite(value) || value < 0) {
20
+ throw new RangeError(
21
+ "Scale unknown for size must be a non-negative finite area."
22
+ );
23
+ }
24
+ return value;
25
+ }
26
+ if (channel === "opacity") {
27
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
28
+ throw new RangeError("Scale unknown for opacity must be between 0 and 1.");
29
+ }
30
+ return value;
31
+ }
32
+ if (channel === "shape") {
33
+ if (!POINT_SHAPES.includes(value)) {
34
+ throw new Error(`Unsupported scale unknown shape "${value}".`);
35
+ }
36
+ return value;
37
+ }
38
+ if (channel === "strokeDash") {
39
+ return cloneAndFreeze(normalizeStrokeDashPattern(value));
40
+ }
41
+ throw new Error(`Scale unknown is not supported for channel "${channel}".`);
42
+ }
43
+
44
+ export function withScaleUnknown(target, source, channel) {
45
+ if (!Object.hasOwn(source, "unknown")) return target;
46
+ return {
47
+ ...target,
48
+ unknown: validateScaleUnknown(channel, source.unknown)
49
+ };
50
+ }
@@ -0,0 +1,53 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+
3
+ const TIME_UNITS = [
4
+ { span: 2 * 365 * 24 * 60 * 60 * 1000, unit: "year" },
5
+ { span: 60 * 24 * 60 * 60 * 1000, unit: "month" },
6
+ { span: 2 * 24 * 60 * 60 * 1000, unit: "day" },
7
+ { span: 2 * 60 * 60 * 1000, unit: "hour" },
8
+ { span: 2 * 60 * 1000, unit: "minute" },
9
+ { span: 0, unit: "second" }
10
+ ];
11
+
12
+ function floorUtc(timestamp, unit) {
13
+ const date = new Date(timestamp);
14
+ if (unit === "year") return Date.UTC(date.getUTCFullYear(), 0, 1);
15
+ if (unit === "month") return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
16
+ if (unit === "day") {
17
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
18
+ }
19
+ if (unit === "hour") {
20
+ return Date.UTC(
21
+ date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours()
22
+ );
23
+ }
24
+ if (unit === "minute") {
25
+ return Date.UTC(
26
+ date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
27
+ date.getUTCHours(), date.getUTCMinutes()
28
+ );
29
+ }
30
+ return Math.floor(timestamp / 1000) * 1000;
31
+ }
32
+
33
+ function offsetUtc(timestamp, unit) {
34
+ const date = new Date(timestamp);
35
+ if (unit === "year") return Date.UTC(date.getUTCFullYear() + 1, 0, 1);
36
+ if (unit === "month") {
37
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
38
+ }
39
+ if (unit === "day") return timestamp + 24 * 60 * 60 * 1000;
40
+ if (unit === "hour") return timestamp + 60 * 60 * 1000;
41
+ if (unit === "minute") return timestamp + 60 * 1000;
42
+ return timestamp + 1000;
43
+ }
44
+
45
+ export function niceTimeDomain(domain) {
46
+ const [minimum, maximum] = domain;
47
+ if (minimum === maximum) return domain;
48
+ const unit = TIME_UNITS.find(item => maximum - minimum >= item.span).unit;
49
+ const start = floorUtc(minimum, unit);
50
+ const endFloor = floorUtc(maximum, unit);
51
+ const end = endFloor === maximum ? maximum : offsetUtc(endFloor, unit);
52
+ return cloneAndFreeze([start, end]);
53
+ }
@@ -0,0 +1,236 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
+ import { niceTicks } from "../ticks.js";
3
+ import { niceLinearDomain } from "./continuous.js";
4
+ import {
5
+ SCALE_ROLES,
6
+ validateScaleTypeForRole
7
+ } from "./types.js";
8
+ import { validatePair } from "./validation.js";
9
+ const TRANSFORMED_TYPES = Object.freeze(["log", "pow", "sqrt", "symlog"]);
10
+
11
+ function positiveFinite(value, label) {
12
+ if (!Number.isFinite(value) || value <= 0) {
13
+ throw new RangeError(`${label} must be a positive finite number.`);
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function normalizeTransformParameters(type, options = {}) {
19
+ if (!TRANSFORMED_TYPES.includes(type) && type !== "linear") {
20
+ throw new Error(`Scale type "${type}" is not a quantitative transform.`);
21
+ }
22
+ if (!isPlainObject(options)) {
23
+ throw new TypeError("Scale transform options must be a plain object.");
24
+ }
25
+ const allowed = type === "log"
26
+ ? ["base"]
27
+ : type === "pow"
28
+ ? ["exponent"]
29
+ : type === "symlog" ? ["constant"] : [];
30
+ for (const key of Object.keys(options)) {
31
+ if (!allowed.includes(key)) {
32
+ throw new Error(`Scale type "${type}" does not support ${key}.`);
33
+ }
34
+ }
35
+ if (type === "log") {
36
+ const base = positiveFinite(options.base ?? 10, "Log scale base");
37
+ if (base === 1) throw new RangeError("Log scale base must not equal 1.");
38
+ return cloneAndFreeze({ base });
39
+ }
40
+ if (type === "pow") {
41
+ return cloneAndFreeze({
42
+ exponent: positiveFinite(options.exponent ?? 1, "Power scale exponent")
43
+ });
44
+ }
45
+ if (type === "sqrt") return cloneAndFreeze({ exponent: 0.5 });
46
+ if (type === "symlog") {
47
+ return cloneAndFreeze({
48
+ constant: positiveFinite(options.constant ?? 1, "Symlog scale constant")
49
+ });
50
+ }
51
+ return cloneAndFreeze({});
52
+ }
53
+
54
+ function transformValue(value, type, parameters) {
55
+ if (type === "linear") return value;
56
+ if (type === "log") {
57
+ return Math.sign(value) * Math.log10(Math.abs(value)) /
58
+ Math.log10(parameters.base);
59
+ }
60
+ if (type === "sqrt") {
61
+ return Math.sign(value) * Math.sqrt(Math.abs(value));
62
+ }
63
+ const exponent = type === "sqrt" ? 0.5 : parameters.exponent;
64
+ if (type === "pow" || type === "sqrt") {
65
+ return Math.sign(value) * Math.abs(value) ** exponent;
66
+ }
67
+ return Math.sign(value) * Math.log1p(
68
+ Math.abs(value) / parameters.constant
69
+ );
70
+ }
71
+
72
+ export function validateTransformedDomain(type, domain, options = {}) {
73
+ validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
74
+ const validated = validatePair(domain, "Transformed scale domain");
75
+ if (validated[0] === validated[1]) {
76
+ throw new RangeError("Transformed scale domain values must be distinct.");
77
+ }
78
+ if (
79
+ type === "log" &&
80
+ (validated.includes(0) || Math.sign(validated[0]) !== Math.sign(validated[1]))
81
+ ) {
82
+ throw new RangeError(
83
+ "Log scale domain must be strictly positive or strictly negative."
84
+ );
85
+ }
86
+ normalizeTransformParameters(type, options);
87
+ return validated;
88
+ }
89
+
90
+ function niceLogDomain(domain, base) {
91
+ const reversed = domain[0] > domain[1];
92
+ const low = Math.min(...domain);
93
+ const high = Math.max(...domain);
94
+ let result;
95
+ if (low > 0) {
96
+ result = [
97
+ base ** Math.floor(Math.log(low) / Math.log(base)),
98
+ base ** Math.ceil(Math.log(high) / Math.log(base))
99
+ ];
100
+ } else {
101
+ result = [
102
+ -(base ** Math.ceil(Math.log(Math.abs(low)) / Math.log(base))),
103
+ -(base ** Math.floor(Math.log(Math.abs(high)) / Math.log(base)))
104
+ ];
105
+ }
106
+ return cloneAndFreeze(reversed ? result.reverse() : result);
107
+ }
108
+
109
+ export function resolveTransformedDomain({
110
+ type,
111
+ domain = "auto",
112
+ values,
113
+ nice = false,
114
+ zero = false,
115
+ ...options
116
+ }) {
117
+ validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
118
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
119
+ throw new TypeError("Transformed scale values must be finite numbers.");
120
+ }
121
+ if (typeof nice !== "boolean" || typeof zero !== "boolean") {
122
+ throw new TypeError("Scale nice and zero must be booleans.");
123
+ }
124
+ if (type === "log" && zero) {
125
+ throw new Error('Scale type "log" does not support zero.');
126
+ }
127
+ const explicit = domain !== "auto";
128
+ if (explicit) return validateTransformedDomain(type, domain, options);
129
+ if (values.length === 0) {
130
+ throw new Error("Cannot infer an automatic scale domain from no values.");
131
+ }
132
+ let resolved = [Math.min(...values), Math.max(...values)];
133
+ if (resolved[0] === resolved[1]) {
134
+ throw new RangeError("Automatic transformed domain requires distinct values.");
135
+ }
136
+ if (zero) resolved = [Math.min(0, resolved[0]), Math.max(0, resolved[1])];
137
+ validateTransformedDomain(type, resolved, options);
138
+ if (nice) {
139
+ resolved = type === "log"
140
+ ? niceLogDomain(resolved, normalizeTransformParameters(type, options).base)
141
+ : niceLinearDomain(resolved);
142
+ }
143
+ return validateTransformedDomain(type, resolved, options);
144
+ }
145
+
146
+ export function resolveMappingRange(range, { reverse = false } = {}) {
147
+ const validated = validatePair(range, "Scale range");
148
+ if (typeof reverse !== "boolean") {
149
+ throw new TypeError("Scale reverse must be a boolean.");
150
+ }
151
+ return reverse
152
+ ? cloneAndFreeze([...validated].reverse())
153
+ : validated;
154
+ }
155
+
156
+ export function mapTransformedValues(
157
+ values,
158
+ domain,
159
+ range,
160
+ { type = "linear", clamp = false, reverse = false, ...options } = {}
161
+ ) {
162
+ if (!Array.isArray(values)) {
163
+ throw new TypeError("Scale values must be an array.");
164
+ }
165
+ if (typeof clamp !== "boolean") {
166
+ throw new TypeError("Scale clamp must be a boolean.");
167
+ }
168
+ const hasUnknown = Object.hasOwn(options, "unknown");
169
+ const transformOptions = Object.fromEntries(
170
+ Object.entries(options).filter(([key]) => key !== "unknown")
171
+ );
172
+ const parameters = normalizeTransformParameters(type, transformOptions);
173
+ const domainOptions = type === "sqrt" ? {} : transformOptions;
174
+ const validatedDomain = validateTransformedDomain(type, domain, domainOptions);
175
+ const resolvedRange = resolveMappingRange(range, { reverse });
176
+ const transformedDomain = validatedDomain.map(value =>
177
+ transformValue(value, type, parameters)
178
+ );
179
+ const span = transformedDomain[1] - transformedDomain[0];
180
+
181
+ return cloneAndFreeze(values.map(value => {
182
+ const valid = Number.isFinite(value) && !(type === "log" && (
183
+ value === 0 || Math.sign(value) !== Math.sign(validatedDomain[0])
184
+ ));
185
+ if (!valid) {
186
+ if (hasUnknown) return options.unknown;
187
+ throw new TypeError(`Scale type "${type}" received an invalid value.`);
188
+ }
189
+ const transformed = transformValue(value, type, parameters);
190
+ let proportion = (transformed - transformedDomain[0]) / span;
191
+ if (clamp) proportion = Math.max(0, Math.min(1, proportion));
192
+ return resolvedRange[0] + proportion * (resolvedRange[1] - resolvedRange[0]);
193
+ }));
194
+ }
195
+
196
+ export function transformedTicks(type, domain, count, options = {}) {
197
+ const validated = validateTransformedDomain(type, domain, options);
198
+ if (!Number.isInteger(count) || count <= 0) {
199
+ throw new RangeError("Tick count must be a positive integer.");
200
+ }
201
+ if (type !== "log") return niceTicks(validated, count);
202
+ const { base } = normalizeTransformParameters(type, options);
203
+ const reversed = validated[0] > validated[1];
204
+ const low = Math.min(...validated);
205
+ const high = Math.max(...validated);
206
+ const negative = high < 0;
207
+ const minimumMagnitude = negative ? Math.abs(high) : low;
208
+ const maximumMagnitude = negative ? Math.abs(low) : high;
209
+ const start = Math.ceil(
210
+ Math.log(minimumMagnitude) / Math.log(base) - 1e-12
211
+ );
212
+ const end = Math.floor(
213
+ Math.log(maximumMagnitude) / Math.log(base) + 1e-12
214
+ );
215
+ const values = [];
216
+ for (let exponent = start; exponent <= end; exponent += 1) {
217
+ values.push((negative ? -1 : 1) * base ** exponent);
218
+ }
219
+ if (negative) values.reverse();
220
+ if (reversed) values.reverse();
221
+ return cloneAndFreeze(values.length === 0 ? [...validated] : values);
222
+ }
223
+
224
+ export function formatTransformedTick(type, value) {
225
+ if (type !== "log") return String(value);
226
+ const magnitude = Math.abs(value);
227
+ const units = [
228
+ [1_000_000_000, "B"],
229
+ [1_000_000, "M"],
230
+ [1_000, "K"]
231
+ ];
232
+ const unit = units.find(([threshold]) => magnitude >= threshold);
233
+ if (unit === undefined) return String(value);
234
+ const scaled = value / unit[0];
235
+ return `${Number(scaled.toPrecision(3))}${unit[1]}`;
236
+ }
@@ -0,0 +1,102 @@
1
+ export const SCALE_ROLES = Object.freeze({
2
+ quantitativePosition: "quantitative-position",
3
+ temporalPosition: "temporal-position",
4
+ bandPosition: "band-position",
5
+ pointPosition: "point-position",
6
+ discreteAppearance: "discrete-appearance",
7
+ continuousColor: "continuous-color",
8
+ discretizedColor: "discretized-color"
9
+ });
10
+
11
+ export const SCALE_TYPES_BY_ROLE = Object.freeze({
12
+ [SCALE_ROLES.quantitativePosition]: Object.freeze([
13
+ "linear", "log", "pow", "sqrt", "symlog"
14
+ ]),
15
+ [SCALE_ROLES.temporalPosition]: Object.freeze(["time"]),
16
+ [SCALE_ROLES.bandPosition]: Object.freeze(["band"]),
17
+ [SCALE_ROLES.pointPosition]: Object.freeze(["point"]),
18
+ [SCALE_ROLES.discreteAppearance]: Object.freeze(["ordinal"]),
19
+ [SCALE_ROLES.continuousColor]: Object.freeze(["sequential"]),
20
+ [SCALE_ROLES.discretizedColor]: Object.freeze([
21
+ "quantize", "quantile", "threshold"
22
+ ])
23
+ });
24
+
25
+ export const COMPLETE_SCALE_TYPES = Object.freeze([
26
+ ...new Set(Object.values(SCALE_TYPES_BY_ROLE).flat())
27
+ ]);
28
+
29
+ const SCALE_PROPERTY_TYPES = Object.freeze({
30
+ nice: Object.freeze(["linear", "log", "pow", "sqrt", "symlog", "time"]),
31
+ zero: Object.freeze(["linear", "pow", "sqrt", "symlog"]),
32
+ clamp: Object.freeze([
33
+ "linear", "log", "pow", "sqrt", "symlog", "time", "sequential",
34
+ "quantize"
35
+ ]),
36
+ base: Object.freeze(["log"]),
37
+ exponent: Object.freeze(["pow"]),
38
+ constant: Object.freeze(["symlog"]),
39
+ paddingInner: Object.freeze(["band"]),
40
+ paddingOuter: Object.freeze(["band"]),
41
+ padding: Object.freeze(["point"]),
42
+ align: Object.freeze(["band", "point"])
43
+ });
44
+
45
+ export function validateCompleteScaleType(type) {
46
+ if (!COMPLETE_SCALE_TYPES.includes(type)) {
47
+ throw new Error(`Unsupported scale type "${type}".`);
48
+ }
49
+ return type;
50
+ }
51
+
52
+ export function isDiscretePositionScaleType(type) {
53
+ return type === "band" || type === "point";
54
+ }
55
+
56
+ export function isContinuousPositionScaleType(type) {
57
+ return (
58
+ SCALE_TYPES_BY_ROLE[SCALE_ROLES.quantitativePosition].includes(type) ||
59
+ SCALE_TYPES_BY_ROLE[SCALE_ROLES.temporalPosition].includes(type)
60
+ );
61
+ }
62
+
63
+ export function isOrdinalScaleType(type) {
64
+ return type === "ordinal";
65
+ }
66
+
67
+ export function hasOrdinalDomain(type) {
68
+ return isOrdinalScaleType(type) || isDiscretePositionScaleType(type);
69
+ }
70
+
71
+ export function isContinuousColorScaleType(type) {
72
+ return SCALE_TYPES_BY_ROLE[SCALE_ROLES.continuousColor].includes(type);
73
+ }
74
+
75
+ export function isDiscretizedColorScaleType(type) {
76
+ return SCALE_TYPES_BY_ROLE[SCALE_ROLES.discretizedColor].includes(type);
77
+ }
78
+
79
+ export function isColorScaleType(type) {
80
+ return isContinuousColorScaleType(type) || isDiscretizedColorScaleType(type);
81
+ }
82
+
83
+ export function validateScaleTypeForRole(type, role) {
84
+ const accepted = SCALE_TYPES_BY_ROLE[role];
85
+ if (accepted === undefined) {
86
+ throw new Error(`Unknown scale role "${role}".`);
87
+ }
88
+ if (!accepted.includes(type)) {
89
+ throw new Error(`Scale type "${type}" is not valid for ${role}.`);
90
+ }
91
+ return type;
92
+ }
93
+
94
+ export function validateScalePropertyForType(type, property) {
95
+ validateCompleteScaleType(type);
96
+ const accepted = SCALE_PROPERTY_TYPES[property];
97
+ if (accepted === undefined) return property;
98
+ if (!accepted.includes(type)) {
99
+ throw new Error(`Scale type "${type}" does not support ${property}.`);
100
+ }
101
+ return property;
102
+ }