ggaction 0.0.2 → 0.0.4

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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
@@ -1,5 +1,6 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { aggregateScalarValues } from "./aggregate.js";
3
+ import { studentTCriticalValue } from "./statistics/studentT.js";
3
4
 
4
5
  const CENTER_VALUES = Object.freeze(["mean", "median"]);
5
6
  const EXTENT_VALUES = Object.freeze(["stderr", "stdev", "ci", "iqr"]);
@@ -133,92 +134,6 @@ export function normalizeIntervalParameters({
133
134
  });
134
135
  }
135
136
 
136
- const LANCZOS = Object.freeze([
137
- 676.5203681218851,
138
- -1259.1392167224028,
139
- 771.3234287776531,
140
- -176.6150291621406,
141
- 12.507343278686905,
142
- -0.13857109526572012,
143
- 9.984369578019572e-6,
144
- 1.5056327351493116e-7
145
- ]);
146
-
147
- function logGamma(value) {
148
- if (value < 0.5) {
149
- return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
150
- logGamma(1 - value);
151
- }
152
- const shifted = value - 1;
153
- let sum = 0.9999999999998099;
154
- for (const [index, coefficient] of LANCZOS.entries()) {
155
- sum += coefficient / (shifted + index + 1);
156
- }
157
- const t = shifted + LANCZOS.length - 0.5;
158
- return 0.5 * Math.log(2 * Math.PI) +
159
- (shifted + 0.5) * Math.log(t) - t + Math.log(sum);
160
- }
161
-
162
- function betaFraction(a, b, x) {
163
- const maximumIterations = 200;
164
- const epsilon = 3e-14;
165
- const floor = 1e-300;
166
- const sum = a + b;
167
- let c = 1;
168
- let d = 1 - sum * x / (a + 1);
169
- if (Math.abs(d) < floor) d = floor;
170
- d = 1 / d;
171
- let result = d;
172
-
173
- for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
174
- const even = 2 * iteration;
175
- let coefficient = iteration * (b - iteration) * x /
176
- ((a + even - 1) * (a + even));
177
- d = 1 + coefficient * d;
178
- if (Math.abs(d) < floor) d = floor;
179
- c = 1 + coefficient / c;
180
- if (Math.abs(c) < floor) c = floor;
181
- d = 1 / d;
182
- result *= d * c;
183
-
184
- coefficient = -(a + iteration) * (sum + iteration) * x /
185
- ((a + even) * (a + even + 1));
186
- d = 1 + coefficient * d;
187
- if (Math.abs(d) < floor) d = floor;
188
- c = 1 + coefficient / c;
189
- if (Math.abs(c) < floor) c = floor;
190
- d = 1 / d;
191
- const delta = d * c;
192
- result *= delta;
193
- if (Math.abs(delta - 1) <= epsilon) return result;
194
- }
195
- throw new Error("Student-t calculation did not converge.");
196
- }
197
-
198
- function regularizedBeta(x, a, b) {
199
- if (x <= 0) return 0;
200
- if (x >= 1) return 1;
201
- const front = Math.exp(
202
- logGamma(a + b) - logGamma(a) - logGamma(b) +
203
- a * Math.log(x) + b * Math.log1p(-x)
204
- );
205
- return x < (a + 1) / (a + b + 2)
206
- ? front * betaFraction(a, b, x) / a
207
- : 1 - front * betaFraction(b, a, 1 - x) / b;
208
- }
209
-
210
- function studentTCdf(value, degreesOfFreedom) {
211
- if (value === 0) return 0.5;
212
- const x = degreesOfFreedom /
213
- (degreesOfFreedom + value * value);
214
- const tail = 0.5 * regularizedBeta(
215
- x,
216
- degreesOfFreedom / 2,
217
- 0.5
218
- );
219
- return value > 0 ? 1 - tail : tail;
220
- }
221
-
222
137
  export function studentTCritical(degreesOfFreedom, level) {
223
138
  if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
224
139
  throw new RangeError("Student-t degreesOfFreedom must be a positive integer.");
@@ -226,16 +141,7 @@ export function studentTCritical(degreesOfFreedom, level) {
226
141
  if (!Number.isFinite(level) || level <= 0 || level >= 1) {
227
142
  throw new RangeError("Student-t level must be between 0 and 1.");
228
143
  }
229
- const target = 0.5 + level / 2;
230
- let low = 0;
231
- let high = 1;
232
- while (studentTCdf(high, degreesOfFreedom) < target) high *= 2;
233
- for (let iteration = 0; iteration < 100; iteration += 1) {
234
- const midpoint = (low + high) / 2;
235
- if (studentTCdf(midpoint, degreesOfFreedom) < target) low = midpoint;
236
- else high = midpoint;
237
- }
238
- return (low + high) / 2;
144
+ return studentTCriticalValue(level, degreesOfFreedom);
239
145
  }
240
146
 
241
147
  function isMissing(value) {
@@ -3,7 +3,7 @@ import {
3
3
  readNominalField,
4
4
  readQuantitativeField,
5
5
  readTemporalField
6
- } from "./scales.js";
6
+ } from "./scales/index.js";
7
7
  import {
8
8
  aggregateRows,
9
9
  isAggregate,
@@ -64,8 +64,13 @@ function readSeriesFields(rows, layer) {
64
64
  const encoding = layer.encoding?.[channel];
65
65
 
66
66
  if (encoding?.field === undefined) continue;
67
- if (encoding.fieldType !== "nominal") {
68
- throw new Error(`Line ${channel} encoding on mark "${layer.id}" must be nominal.`);
67
+ const categorical = channel === "color"
68
+ ? ["nominal", "ordinal"].includes(encoding.fieldType)
69
+ : encoding.fieldType === "nominal";
70
+ if (!categorical) {
71
+ throw new Error(
72
+ `Line ${channel} encoding on mark "${layer.id}" must be categorical.`
73
+ );
69
74
  }
70
75
 
71
76
  if (!values.has(encoding.field)) {
@@ -81,7 +86,7 @@ function groupKey(values) {
81
86
  return JSON.stringify(values);
82
87
  }
83
88
 
84
- export function deriveLineSeries(rows, layer) {
89
+ function deriveCartesianLineSeries(rows, layer) {
85
90
  const { x, y, isAggregate, directTemporal } = requireLineEncoding(layer);
86
91
  if (isAggregate) {
87
92
  validateAggregateFieldValues(rows, y.field, y.fieldType);
@@ -195,3 +200,106 @@ export function deriveLineSeries(rows, layer) {
195
200
  series
196
201
  });
197
202
  }
203
+
204
+ function requirePolarLineEncoding(layer) {
205
+ if (layer?.mark?.type !== "line") {
206
+ throw new Error("Polar line series derivation requires a semantic line mark.");
207
+ }
208
+ const theta = layer.encoding?.theta;
209
+ const radius = layer.encoding?.radius;
210
+ if (theta === undefined || radius === undefined) {
211
+ throw new Error(
212
+ `Polar line mark "${layer.id}" requires theta and radius encodings.`
213
+ );
214
+ }
215
+ if (!["quantitative", "temporal", "ordinal", "nominal"].includes(
216
+ theta.fieldType
217
+ )) {
218
+ throw new Error(
219
+ `Polar line mark "${layer.id}" has unsupported theta field type.`
220
+ );
221
+ }
222
+ if (radius.fieldType !== "quantitative") {
223
+ throw new Error(
224
+ `Polar line mark "${layer.id}" requires a quantitative radius encoding.`
225
+ );
226
+ }
227
+ return { theta, radius };
228
+ }
229
+
230
+ function readThetaValues(rows, encoding) {
231
+ if (["nominal", "ordinal"].includes(encoding.fieldType)) {
232
+ return readNominalField(rows, encoding.field);
233
+ }
234
+ return encoding.fieldType === "temporal"
235
+ ? readTemporalField(rows, encoding.field)
236
+ : readQuantitativeField(rows, encoding.field);
237
+ }
238
+
239
+ function thetaOrder(values, fieldType, domain) {
240
+ if (!["nominal", "ordinal"].includes(fieldType)) return values;
241
+ const order = domain ?? [...new Set(values)];
242
+ const indices = new Map(order.map((value, index) => [value, index]));
243
+ for (const value of values) {
244
+ if (!indices.has(value)) {
245
+ throw new Error(`Polar line theta domain does not contain "${value}".`);
246
+ }
247
+ }
248
+ return values.map(value => indices.get(value));
249
+ }
250
+
251
+ export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
252
+ const { theta, radius } = requirePolarLineEncoding(layer);
253
+ const thetaValues = readThetaValues(rows, theta);
254
+ const radiusValues = readQuantitativeField(rows, radius.field);
255
+ const sortValues = thetaOrder(thetaValues, theta.fieldType, thetaDomain);
256
+ const seriesFields = readSeriesFields(rows, layer);
257
+ const groups = new Map();
258
+
259
+ for (let index = 0; index < rows.length; index += 1) {
260
+ const dimensions = seriesFields.fields.map(
261
+ field => seriesFields.values.get(field)[index]
262
+ );
263
+ const key = groupKey(dimensions);
264
+ const series = groups.get(key) ?? {
265
+ key: Object.fromEntries(
266
+ seriesFields.fields.map((field, item) => [field, dimensions[item]])
267
+ ),
268
+ values: []
269
+ };
270
+ series.values.push({
271
+ theta: thetaValues[index],
272
+ radius: radiusValues[index],
273
+ order: sortValues[index],
274
+ sourceIndex: index
275
+ });
276
+ groups.set(key, series);
277
+ }
278
+
279
+ const series = [...groups.values()].flatMap(item => {
280
+ const values = item.values.sort(
281
+ (left, right) => left.order - right.order ||
282
+ left.sourceIndex - right.sourceIndex
283
+ );
284
+ return values.length < 2 ? [] : [{ key: item.key, values }];
285
+ });
286
+ if (series.length === 0) {
287
+ throw new Error(
288
+ `Polar line series on mark "${layer.id}" requires at least two points.`
289
+ );
290
+ }
291
+ return cloneAndFreeze({
292
+ thetaFieldType: theta.fieldType,
293
+ thetaValues: series.flatMap(item => item.values.map(value => value.theta)),
294
+ radiusValues: series.flatMap(item => item.values.map(value => value.radius)),
295
+ series
296
+ });
297
+ }
298
+
299
+ export function deriveLineSeries(rows, layer, options) {
300
+ const polar = layer?.encoding?.theta !== undefined ||
301
+ layer?.encoding?.radius !== undefined;
302
+ return polar
303
+ ? derivePolarLineSeries(rows, layer, options)
304
+ : deriveCartesianLineSeries(rows, layer);
305
+ }
@@ -190,16 +190,14 @@ export function resolvePalette(value, domainCount) {
190
190
  }
191
191
 
192
192
  export function resolveContinuousPalette(value, count = 31) {
193
- if (!Number.isInteger(count) || count < 2) {
194
- throw new RangeError("Continuous palette count must be an integer of at least 2.");
195
- }
196
193
  const palette = normalizePalette(value);
197
- if (palette.count !== undefined) {
198
- throw new Error("Continuous palette does not accept count.");
194
+ const resolvedCount = palette.count ?? count;
195
+ if (!Number.isInteger(resolvedCount) || resolvedCount < 2) {
196
+ throw new RangeError("Continuous palette count must be an integer of at least 2.");
199
197
  }
200
198
  const discrete = DISCRETE[palette.name];
201
- if (discrete !== undefined) return discrete;
202
- return resolvePalette({ ...palette, count }, count);
199
+ if (discrete !== undefined && palette.count === undefined) return discrete;
200
+ return resolvePalette({ ...palette, count: resolvedCount }, resolvedCount);
203
201
  }
204
202
 
205
203
  export function paletteFamily(name) {
@@ -0,0 +1,102 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validatePair } from "./scales/validation.js";
3
+
4
+ function validateBounds(bounds) {
5
+ if (
6
+ !isPlainObject(bounds) ||
7
+ ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
8
+ bounds.width < 0 ||
9
+ bounds.height < 0
10
+ ) {
11
+ throw new TypeError(
12
+ "Polar geometry requires finite, non-negative graphical bounds."
13
+ );
14
+ }
15
+ return bounds;
16
+ }
17
+
18
+ export function resolvePolarFrame(bounds) {
19
+ validateBounds(bounds);
20
+ return cloneAndFreeze({
21
+ centerX: bounds.x + bounds.width / 2,
22
+ centerY: bounds.y + bounds.height / 2,
23
+ availableRadius: Math.min(bounds.width, bounds.height) / 2
24
+ });
25
+ }
26
+
27
+ export function validateThetaRange(range) {
28
+ if (range === "auto") return range;
29
+ const validated = validatePair(range, "Theta scale range");
30
+ if (Math.abs(validated[1] - validated[0]) > 360) {
31
+ throw new RangeError("Theta scale range span must not exceed 360 degrees.");
32
+ }
33
+ return validated;
34
+ }
35
+
36
+ export function validateRadialRange(range, availableRadius) {
37
+ if (range === "auto") return range;
38
+ const validated = validatePair(range, "Radius scale range");
39
+ if (validated.some(value => value < 0)) {
40
+ throw new RangeError("Radius scale range values must be non-negative.");
41
+ }
42
+ if (
43
+ availableRadius !== undefined &&
44
+ validated.some(value => value > availableRadius)
45
+ ) {
46
+ throw new RangeError(
47
+ `Radius scale range must fit within the available radius ${availableRadius}.`
48
+ );
49
+ }
50
+ return validated;
51
+ }
52
+
53
+ export function resolvePolarScaleRange(range, channel, bounds) {
54
+ if (channel === "theta") {
55
+ return range === "auto"
56
+ ? cloneAndFreeze([0, 360])
57
+ : validateThetaRange(range);
58
+ }
59
+ if (channel !== "radius") {
60
+ throw new Error(`Unknown Polar position channel "${channel}".`);
61
+ }
62
+ const frame = resolvePolarFrame(bounds);
63
+ return range === "auto"
64
+ ? cloneAndFreeze([0, frame.availableRadius])
65
+ : validateRadialRange(range, frame.availableRadius);
66
+ }
67
+
68
+ export function polarDirection(theta) {
69
+ if (!Number.isFinite(theta)) {
70
+ throw new TypeError("Polar theta must be a finite number of degrees.");
71
+ }
72
+ const angle = theta * Math.PI / 180;
73
+ return cloneAndFreeze({
74
+ x: Math.sin(angle),
75
+ y: -Math.cos(angle)
76
+ });
77
+ }
78
+
79
+ export function polarToCartesian({ theta, radius, frame }) {
80
+ const direction = polarDirection(theta);
81
+ if (!Number.isFinite(radius) || radius < 0) {
82
+ throw new TypeError("Polar radius must be a non-negative finite number.");
83
+ }
84
+ const resolvedFrame = frame?.availableRadius === undefined
85
+ ? resolvePolarFrame(frame)
86
+ : frame;
87
+ if (
88
+ ![resolvedFrame.centerX, resolvedFrame.centerY,
89
+ resolvedFrame.availableRadius].every(Number.isFinite)
90
+ ) {
91
+ throw new TypeError("Polar frame must contain finite geometry.");
92
+ }
93
+ if (radius > resolvedFrame.availableRadius) {
94
+ throw new RangeError(
95
+ `Polar radius ${radius} exceeds the available radius ${resolvedFrame.availableRadius}.`
96
+ );
97
+ }
98
+ return cloneAndFreeze({
99
+ x: resolvedFrame.centerX + radius * direction.x,
100
+ y: resolvedFrame.centerY + radius * direction.y
101
+ });
102
+ }
@@ -0,0 +1,243 @@
1
+ import { freezeOwned } from "../core/immutable.js";
2
+ import { polarDirection } from "./polar.js";
3
+ import { buildPolarCircleCommands } from "./polarPaths.js";
4
+
5
+ export { buildPolarCircleCommands } from "./polarPaths.js";
6
+
7
+ function validateFrame(frame) {
8
+ if (
9
+ frame === null ||
10
+ typeof frame !== "object" ||
11
+ ![frame.centerX, frame.centerY, frame.availableRadius].every(Number.isFinite) ||
12
+ frame.availableRadius < 0
13
+ ) {
14
+ throw new TypeError(
15
+ "Polar guide frame requires finite centerX, centerY, and non-negative availableRadius."
16
+ );
17
+ }
18
+ return frame;
19
+ }
20
+
21
+ function validateFiniteValues(values, label) {
22
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
23
+ throw new TypeError(`${label} must be an array of finite numbers.`);
24
+ }
25
+ return values;
26
+ }
27
+
28
+ function validateNonNegative(value, label) {
29
+ if (!Number.isFinite(value) || value < 0) {
30
+ throw new RangeError(`${label} must be a non-negative finite number.`);
31
+ }
32
+ return value;
33
+ }
34
+
35
+ function own(value) {
36
+ return freezeOwned(value);
37
+ }
38
+
39
+ function pointAt(frame, theta, radius) {
40
+ validateNonNegative(radius, "Polar guide radius");
41
+ const direction = polarDirection(theta);
42
+ return {
43
+ x: frame.centerX + direction.x * radius,
44
+ y: frame.centerY + direction.y * radius
45
+ };
46
+ }
47
+
48
+ export function resolveThetaSpokes({ frame, angles }) {
49
+ validateFrame(frame);
50
+ validateFiniteValues(angles, "Polar spoke angles");
51
+ const end = angles.map(theta => pointAt(frame, theta, frame.availableRadius));
52
+ return own({
53
+ x1: angles.map(() => frame.centerX),
54
+ y1: angles.map(() => frame.centerY),
55
+ x2: end.map(point => point.x),
56
+ y2: end.map(point => point.y)
57
+ });
58
+ }
59
+
60
+ export function resolveRadialCircles({ frame, radii }) {
61
+ validateFrame(frame);
62
+ validateFiniteValues(radii, "Polar grid radii");
63
+ if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
64
+ throw new RangeError("Polar grid radii must be inside the Polar frame.");
65
+ }
66
+ const visibleRadii = radii.filter(radius => radius > 0);
67
+ return own({
68
+ radii: visibleRadii,
69
+ commands: visibleRadii.map(radius => buildPolarCircleCommands(frame, radius))
70
+ });
71
+ }
72
+
73
+ export function resolveThetaAxisTicks({ frame, angles, length }) {
74
+ validateFrame(frame);
75
+ validateFiniteValues(angles, "Theta-axis angles");
76
+ validateNonNegative(length, "Theta-axis tick length");
77
+ const start = angles.map(theta => pointAt(frame, theta, frame.availableRadius));
78
+ const end = angles.map(theta => pointAt(
79
+ frame,
80
+ theta,
81
+ frame.availableRadius + length
82
+ ));
83
+ return own({
84
+ x1: start.map(point => point.x),
85
+ y1: start.map(point => point.y),
86
+ x2: end.map(point => point.x),
87
+ y2: end.map(point => point.y)
88
+ });
89
+ }
90
+
91
+ function perimeterAlignment(theta) {
92
+ const radians = theta * Math.PI / 180;
93
+ const horizontal = Math.sin(radians);
94
+ const vertical = Math.cos(radians);
95
+ return {
96
+ textAlign: horizontal > 0.25
97
+ ? "left"
98
+ : horizontal < -0.25
99
+ ? "right"
100
+ : "center",
101
+ textBaseline: vertical > 0.25
102
+ ? "bottom"
103
+ : vertical < -0.25
104
+ ? "top"
105
+ : "middle"
106
+ };
107
+ }
108
+
109
+ export function resolveThetaAxisLabels({ frame, angles, offset }) {
110
+ validateFrame(frame);
111
+ validateFiniteValues(angles, "Theta-axis label angles");
112
+ validateNonNegative(offset, "Theta-axis label offset");
113
+ const points = angles.map(theta => pointAt(
114
+ frame,
115
+ theta,
116
+ frame.availableRadius + offset
117
+ ));
118
+ const alignment = angles.map(perimeterAlignment);
119
+ return own({
120
+ x: points.map(point => point.x),
121
+ y: points.map(point => point.y),
122
+ textAlign: alignment.map(value => value.textAlign),
123
+ textBaseline: alignment.map(value => value.textBaseline)
124
+ });
125
+ }
126
+
127
+ function radialVectors(angle) {
128
+ let direction;
129
+ try {
130
+ direction = polarDirection(angle);
131
+ } catch {
132
+ throw new TypeError("Radial-axis angle must be finite degrees.");
133
+ }
134
+ return {
135
+ direction,
136
+ normal: { x: -direction.y, y: direction.x }
137
+ };
138
+ }
139
+
140
+ export function resolveRadialAxisLine({ frame, angle, startRadius = 0 }) {
141
+ validateFrame(frame);
142
+ validateNonNegative(startRadius, "Radial-axis start radius");
143
+ if (startRadius > frame.availableRadius) {
144
+ throw new RangeError("Radial-axis start radius must be inside the Polar frame.");
145
+ }
146
+ const { direction } = radialVectors(angle);
147
+ return own({
148
+ x1: frame.centerX + direction.x * startRadius,
149
+ y1: frame.centerY + direction.y * startRadius,
150
+ x2: frame.centerX + direction.x * frame.availableRadius,
151
+ y2: frame.centerY + direction.y * frame.availableRadius
152
+ });
153
+ }
154
+
155
+ export function resolveRadialAxisTicks({ frame, angle, radii, length }) {
156
+ validateFrame(frame);
157
+ validateFiniteValues(radii, "Radial-axis tick radii");
158
+ validateNonNegative(length, "Radial-axis tick length");
159
+ if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
160
+ throw new RangeError("Radial-axis tick radii must be inside the Polar frame.");
161
+ }
162
+ const { direction, normal } = radialVectors(angle);
163
+ const half = length / 2;
164
+ const centers = radii.map(radius => ({
165
+ x: frame.centerX + direction.x * radius,
166
+ y: frame.centerY + direction.y * radius
167
+ }));
168
+ return own({
169
+ x1: centers.map(point => point.x - normal.x * half),
170
+ y1: centers.map(point => point.y - normal.y * half),
171
+ x2: centers.map(point => point.x + normal.x * half),
172
+ y2: centers.map(point => point.y + normal.y * half)
173
+ });
174
+ }
175
+
176
+ export function resolveRadialAxisLabels({ frame, angle, radii, offset }) {
177
+ validateFrame(frame);
178
+ validateFiniteValues(radii, "Radial-axis label radii");
179
+ validateNonNegative(offset, "Radial-axis label offset");
180
+ if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
181
+ throw new RangeError("Radial-axis label radii must be inside the Polar frame.");
182
+ }
183
+ const { direction, normal } = radialVectors(angle);
184
+ return own({
185
+ x: radii.map(radius =>
186
+ frame.centerX + direction.x * radius - normal.x * offset
187
+ ),
188
+ y: radii.map(radius =>
189
+ frame.centerY + direction.y * radius - normal.y * offset
190
+ ),
191
+ textAlign: radii.map(() => "center"),
192
+ textBaseline: radii.map(() => "bottom")
193
+ });
194
+ }
195
+
196
+ export function resolveRadialAxisTitle({
197
+ frame,
198
+ angle,
199
+ radius = frame.availableRadius / 2,
200
+ offset,
201
+ position = "inside"
202
+ }) {
203
+ validateFrame(frame);
204
+ validateNonNegative(offset, "Radial-axis title offset");
205
+ if (!["inside", "outside"].includes(position)) {
206
+ throw new Error(`Unknown radial-axis title position "${position}".`);
207
+ }
208
+ if (position === "outside") {
209
+ const { direction } = radialVectors(angle);
210
+ return own({
211
+ x: frame.centerX + direction.x * (frame.availableRadius + offset),
212
+ y: frame.centerY + direction.y * (frame.availableRadius + offset),
213
+ textAlign: direction.x > 0.25
214
+ ? "left"
215
+ : direction.x < -0.25 ? "right" : "center",
216
+ textBaseline: direction.y > 0.25
217
+ ? "top"
218
+ : direction.y < -0.25 ? "bottom" : "middle"
219
+ });
220
+ }
221
+ validateNonNegative(radius, "Radial-axis title radius");
222
+ if (radius > frame.availableRadius) {
223
+ throw new RangeError("Radial-axis title radius must be inside the Polar frame.");
224
+ }
225
+ const { direction, normal } = radialVectors(angle);
226
+ return own({
227
+ x: frame.centerX + direction.x * radius + normal.x * offset,
228
+ y: frame.centerY + direction.y * radius + normal.y * offset,
229
+ textAlign: "center",
230
+ textBaseline: "top"
231
+ });
232
+ }
233
+
234
+ export function resolveThetaAxisTitle({ frame, offset }) {
235
+ validateFrame(frame);
236
+ validateNonNegative(offset, "Theta-axis title offset");
237
+ return own({
238
+ x: frame.centerX,
239
+ y: frame.centerY + frame.availableRadius + offset,
240
+ textAlign: "center",
241
+ textBaseline: "middle"
242
+ });
243
+ }
@@ -0,0 +1,43 @@
1
+ import { buildLinearPathCommands } from "./pathCommands.js";
2
+ import { polarToCartesian } from "./polar.js";
3
+ import {
4
+ mapContinuousScaleValues,
5
+ mapOrdinalPositionValues
6
+ } from "./scales/index.js";
7
+
8
+ function mappedTheta(values, fieldType, scale) {
9
+ return ["nominal", "ordinal"].includes(fieldType)
10
+ ? mapOrdinalPositionValues(values, scale)
11
+ : mapContinuousScaleValues(values, scale);
12
+ }
13
+
14
+ export function buildPolarLinePathCommands({
15
+ series,
16
+ thetaFieldType,
17
+ thetaScale,
18
+ radiusScale,
19
+ frame,
20
+ closed = false
21
+ }) {
22
+ if (!Array.isArray(series) || series.length < 2) {
23
+ throw new TypeError("Polar line commands require at least two series values.");
24
+ }
25
+ if (typeof closed !== "boolean") {
26
+ throw new TypeError("Polar line closed must be a boolean.");
27
+ }
28
+ const theta = mappedTheta(
29
+ series.map(value => value.theta),
30
+ thetaFieldType,
31
+ thetaScale
32
+ );
33
+ const radius = mapContinuousScaleValues(
34
+ series.map(value => value.radius),
35
+ radiusScale
36
+ );
37
+ const points = series.map((_, index) => polarToCartesian({
38
+ theta: theta[index],
39
+ radius: radius[index],
40
+ frame
41
+ }));
42
+ return buildLinearPathCommands(points, { close: closed });
43
+ }