ggaction 0.0.3 → 0.0.5

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 (223) hide show
  1. package/CHANGELOG.md +58 -3
  2. package/README.md +117 -83
  3. package/package.json +27 -11
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/regression.js +1 -1
  26. package/src/actions/data/window.js +50 -0
  27. package/src/actions/encodings/appearance.js +1 -1
  28. package/src/actions/encodings/color/continuous.js +137 -0
  29. package/src/actions/encodings/color/index.js +167 -0
  30. package/src/actions/encodings/color/layout.js +99 -0
  31. package/src/actions/encodings/color/policy.js +88 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +193 -88
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -2
  37. package/src/actions/encodings/offset.js +1 -1
  38. package/src/actions/encodings/parallel.js +115 -0
  39. package/src/actions/encodings/pathOrder.js +131 -0
  40. package/src/actions/encodings/position/apply.js +73 -4
  41. package/src/actions/encodings/{position.js → position/index.js} +21 -8
  42. package/src/actions/encodings/position/policies/arc.js +27 -4
  43. package/src/actions/encodings/position/policies/area.js +33 -2
  44. package/src/actions/encodings/position/policies/index.js +7 -2
  45. package/src/actions/encodings/position/policies/line.js +18 -2
  46. package/src/actions/encodings/position/resolve.js +25 -3
  47. package/src/actions/encodings/ranged.js +5 -3
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/encodings/strokeDash.js +1 -1
  50. package/src/actions/encodings/text.js +1 -1
  51. package/src/actions/errorBands/options.js +1 -1
  52. package/src/actions/errorBars/options.js +1 -1
  53. package/src/actions/facets/actions.js +2 -3
  54. package/src/actions/facets/derive.js +35 -5
  55. package/src/actions/facets/guides.js +1 -1
  56. package/src/actions/facets/replay.js +7 -36
  57. package/src/actions/gradientPlots/components.js +177 -0
  58. package/src/actions/gradientPlots/create.js +96 -0
  59. package/src/actions/gradientPlots/edit.js +153 -0
  60. package/src/actions/gradientPlots/index.js +24 -0
  61. package/src/actions/gradientPlots/materialize.js +287 -0
  62. package/src/actions/gradientPlots/options.js +120 -0
  63. package/src/actions/gradientPlots/paint.js +107 -0
  64. package/src/actions/gradientPlots/rebind.js +51 -0
  65. package/src/actions/gradientPlots/resolve.js +81 -0
  66. package/src/actions/guides/applicability.js +44 -17
  67. package/src/actions/guides/axes/axes.js +37 -3
  68. package/src/actions/guides/axes/index.js +2 -0
  69. package/src/actions/guides/axes/labels.js +12 -7
  70. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  71. package/src/actions/guides/axes/parallel.js +228 -0
  72. package/src/actions/guides/axes/ticks.js +1 -1
  73. package/src/actions/guides/axes/titles.js +12 -9
  74. package/src/actions/guides/grids/resolve.js +1 -1
  75. package/src/actions/guides/guides.js +60 -5
  76. package/src/actions/guides/legends/categorical/actions.js +48 -11
  77. package/src/actions/guides/legends/categorical/layout.js +4 -2
  78. package/src/actions/guides/legends/continuous/interval.js +1 -1
  79. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  80. package/src/actions/guides/legends/edit.js +7 -20
  81. package/src/actions/guides/legends/index.js +2 -0
  82. package/src/actions/guides/legends/remove.js +6 -31
  83. package/src/actions/guides/legends/size.js +1 -1
  84. package/src/actions/guides/legends/strokeWidth.js +170 -0
  85. package/src/actions/guides/legends/target.js +25 -0
  86. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  87. package/src/actions/guides/polar/index.js +1 -1
  88. package/src/actions/guides/polar/resolve.js +1 -1
  89. package/src/actions/guides/tickValues.js +1 -1
  90. package/src/actions/index.js +6 -0
  91. package/src/actions/intervals/resolve.js +1 -1
  92. package/src/actions/marks/arc/actions.js +2 -2
  93. package/src/actions/marks/area/actions.js +61 -137
  94. package/src/actions/marks/area/materialize.js +163 -0
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/index.js +5 -1
  97. package/src/actions/marks/line/actions.js +100 -81
  98. package/src/actions/marks/line/materialize.js +146 -0
  99. package/src/actions/marks/point/create.js +76 -0
  100. package/src/actions/marks/point/edit.js +85 -0
  101. package/src/actions/marks/point/index.js +11 -1
  102. package/src/actions/marks/point/jitter.js +72 -0
  103. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  104. package/src/actions/marks/rect/actions.js +4 -1
  105. package/src/actions/marks/remove.js +13 -2
  106. package/src/actions/marks/rule/actions.js +26 -4
  107. package/src/actions/marks/shared.js +1 -1
  108. package/src/actions/marks/text/actions.js +20 -5
  109. package/src/actions/marks/text/index.js +7 -1
  110. package/src/actions/marks/text/layout.js +321 -0
  111. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  112. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  113. package/src/actions/primitives/semanticValidation/index.js +23 -0
  114. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  115. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  116. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  117. package/src/actions/primitives/semanticValue.js +1 -204
  118. package/src/actions/regression/components.js +1 -1
  119. package/src/actions/regression/create.js +1 -1
  120. package/src/actions/regression/edit.js +20 -22
  121. package/src/actions/scales/consumers/common.js +109 -0
  122. package/src/actions/scales/consumers/families.js +58 -0
  123. package/src/actions/scales/consumers/index.js +41 -0
  124. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  125. package/src/actions/scales/create.js +1 -1
  126. package/src/actions/scales/definitions.js +32 -2
  127. package/src/actions/scales/edit.js +8 -206
  128. package/src/actions/scales/editPolicy.js +214 -0
  129. package/src/actions/scales/materialize.js +3 -3
  130. package/src/actions/scales/patch.js +26 -0
  131. package/src/actions/selection/actions.js +28 -3
  132. package/src/actions/violinPlots/create.js +198 -0
  133. package/src/actions/violinPlots/index.js +5 -0
  134. package/src/core/ChartProgram.js +22 -236
  135. package/src/core/compositionState.js +169 -0
  136. package/src/core/materializationState.js +65 -0
  137. package/src/core/programState.js +28 -0
  138. package/src/core/textMetrics.js +47 -0
  139. package/src/core/vocabulary.js +7 -2
  140. package/src/grammar/arcs.js +50 -13
  141. package/src/grammar/areaSeries.js +42 -6
  142. package/src/grammar/bars/aggregate.js +1 -1
  143. package/src/grammar/bin2d.js +333 -0
  144. package/src/grammar/categoricalDensity.js +192 -0
  145. package/src/grammar/coordinates.js +1 -1
  146. package/src/grammar/curveCommands.js +3 -1
  147. package/src/grammar/density.js +253 -27
  148. package/src/grammar/facets/dependencies.js +3 -11
  149. package/src/grammar/{facets.js → facets/index.js} +8 -6
  150. package/src/grammar/gradientProfile.js +213 -0
  151. package/src/grammar/horizon.js +455 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/jitter.js +197 -0
  154. package/src/grammar/lineSeries.js +70 -1
  155. package/src/grammar/paint.js +88 -0
  156. package/src/grammar/palettes.js +5 -7
  157. package/src/grammar/parallelCoordinates.js +213 -0
  158. package/src/grammar/pathOrder.js +35 -0
  159. package/src/grammar/pointShapes.js +24 -0
  160. package/src/grammar/polarLineCommands.js +1 -1
  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 +2 -1
  166. package/src/grammar/scales/appearance.js +21 -0
  167. package/src/grammar/scales/color.js +4 -2
  168. package/src/grammar/schemas/concreteGraphic.js +11 -1
  169. package/src/grammar/schemas/graphicBounds.js +19 -37
  170. package/src/grammar/schemas/semanticPath.js +17 -2
  171. package/src/grammar/statistics/studentT.js +98 -0
  172. package/src/grammar/transforms.js +101 -12
  173. package/src/grammar/window.js +339 -0
  174. package/src/layout/labels.js +270 -0
  175. package/src/layout/text.js +4 -1
  176. package/src/layout/title.js +10 -20
  177. package/src/materialization/bars/aggregate.js +1 -1
  178. package/src/materialization/bars/grouped.js +1 -1
  179. package/src/materialization/bars/histogram.js +1 -1
  180. package/src/materialization/bars/ranged.js +1 -1
  181. package/src/materialization/dataProvenance.js +56 -2
  182. package/src/materialization/density.js +1 -1
  183. package/src/materialization/dependencies.js +15 -101
  184. package/src/materialization/encodings.js +2 -2
  185. package/src/materialization/facetGuides/index.js +2 -0
  186. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  187. package/src/materialization/facetGuides/placement.js +126 -0
  188. package/src/materialization/facetGuides/preparation.js +188 -0
  189. package/src/materialization/facets.js +1 -1
  190. package/src/materialization/guides/resources.js +74 -24
  191. package/src/materialization/horizon.js +29 -0
  192. package/src/materialization/layout.js +15 -0
  193. package/src/materialization/marks/capabilities.js +161 -0
  194. package/src/materialization/marks/index.js +108 -0
  195. package/src/materialization/marks/pathOrder.js +71 -0
  196. package/src/materialization/marks/policies.js +91 -0
  197. package/src/materialization/rect.js +1 -1
  198. package/src/materialization/rowEncoding.js +1 -1
  199. package/src/materialization/scaleGuideDependencies.js +102 -0
  200. package/src/materialization/scales/map.js +1 -1
  201. package/src/materialization/scales/policies/series.js +1 -1
  202. package/src/materialization/scales/resolve.js +3 -1
  203. package/src/materialization/selection/items/bar.js +1 -1
  204. package/src/materialization/selection/items/path.js +27 -1
  205. package/src/materialization/selection/items/point.js +1 -5
  206. package/src/materialization/selection/items/rect.js +21 -0
  207. package/src/materialization/selection/styles.js +21 -10
  208. package/src/materialization/text.js +12 -4
  209. package/src/renderers/canvas/fill.js +33 -0
  210. package/src/renderers/canvas/index.js +7 -0
  211. package/src/renderers/canvas/path.js +7 -4
  212. package/src/renderers/canvas/rect.js +9 -3
  213. package/src/renderers/canvas/text.js +14 -1
  214. package/src/selectors/datasets.js +4 -0
  215. package/src/theme/defaults.js +4 -0
  216. package/types/extension.d.ts +10 -1
  217. package/types/index.d.ts +76 -0
  218. package/types/program.d.ts +632 -10
  219. package/src/actions/encodings/color.js +0 -424
  220. package/src/grammar/regression.js +0 -578
  221. package/src/grammar/scales.js +0 -1
  222. package/src/materialization/facetGuides.js +0 -426
  223. package/src/materialization/marks.js +0 -316
@@ -4,7 +4,7 @@ import {
4
4
  mapOrdinalPositionValues,
5
5
  readNominalField,
6
6
  readQuantitativeField
7
- } from "./scales.js";
7
+ } from "./scales/index.js";
8
8
 
9
9
  function requireArcLayer(layer) {
10
10
  if (layer?.mark?.type !== "arc") {
@@ -36,42 +36,79 @@ function colorValues(rows, encoding) {
36
36
  : readNominalField(rows, encoding.field);
37
37
  }
38
38
 
39
- function countSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
39
+ export function readArcThetaWeights(rows, field, layerId = "arc") {
40
+ if (!Array.isArray(rows)) throw new TypeError("Arc theta weights require rows.");
41
+ if (typeof field !== "string" || field.length === 0) {
42
+ throw new TypeError("Arc theta weight must be a non-empty string.");
43
+ }
44
+ const weights = rows.map((row, index) => {
45
+ const value = row?.[field];
46
+ if (!Number.isFinite(value) || value < 0) {
47
+ throw new TypeError(
48
+ `Arc theta weight field "${field}" must contain non-negative finite numbers at row ${index}.`
49
+ );
50
+ }
51
+ return value;
52
+ });
53
+ if (weights.every(value => value === 0)) {
54
+ throw new Error(`Arc mark "${layerId}" theta weights must have a positive total.`);
55
+ }
56
+ return cloneAndFreeze(weights);
57
+ }
58
+
59
+ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
40
60
  const theta = layer.encoding.theta;
41
- if (theta.aggregate !== "count") {
42
- throw new Error(`Arc mark "${layer.id}" requires count or radial layout.`);
61
+ if (!["count", "sum"].includes(theta.aggregate)) {
62
+ throw new Error(`Arc mark "${layer.id}" requires count, sum, or radial layout.`);
43
63
  }
44
64
  const values = readNominalField(rows, theta.field);
45
65
  const colors = colorValues(rows, layer.encoding?.color);
66
+ const weights = theta.aggregate === "count"
67
+ ? rows.map(() => 1)
68
+ : readArcThetaWeights(rows, theta.weight, layer.id);
46
69
  const groups = new Map(thetaScale.domain.map(value => [value, []]));
47
70
  for (let index = 0; index < rows.length; index += 1) {
48
71
  const group = groups.get(values[index]);
49
72
  if (group === undefined) {
50
73
  throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
51
74
  }
52
- group.push({ row: rows[index], color: colors[index], sourceIndex: index });
75
+ group.push({
76
+ row: rows[index],
77
+ color: colors[index],
78
+ sourceIndex: index,
79
+ weight: weights[index]
80
+ });
53
81
  }
54
- const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
55
- if (total === 0) throw new Error(`Arc mark "${layer.id}" has no rows to count.`);
82
+ const total = [...groups.values()].reduce(
83
+ (sum, group) => sum + group.reduce((groupSum, item) => groupSum + item.weight, 0),
84
+ 0
85
+ );
86
+ if (total === 0) throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
56
87
  const start = thetaScale.range[0];
57
88
  const span = thetaScale.range[1] - thetaScale.range[0];
58
89
  const innerRadius = frame.availableRadius * innerRadiusRatio;
59
90
  let cursor = start;
60
91
  const sectors = [];
61
- for (const value of thetaScale.domain) {
92
+ const positiveValues = thetaScale.domain.filter(value =>
93
+ groups.get(value).some(item => item.weight > 0)
94
+ );
95
+ for (const [positiveIndex, value] of positiveValues.entries()) {
62
96
  const group = groups.get(value);
63
- if (group.length === 0) continue;
64
- const endTheta = cursor + group.length / total * span;
97
+ const aggregateValue = group.reduce((sum, item) => sum + item.weight, 0);
98
+ const endTheta = positiveIndex === positiveValues.length - 1
99
+ ? thetaScale.range[1]
100
+ : cursor + aggregateValue / total * span;
65
101
  const distinctColors = [...new Set(group.map(item => item.color))];
66
102
  if (distinctColors.length > 1) {
67
103
  throw new Error(
68
- `Count arc theta group "${value}" must resolve to one color value.`
104
+ `Aggregated arc theta group "${value}" must resolve to one color value.`
69
105
  );
70
106
  }
71
107
  sectors.push({
72
108
  key: value,
73
109
  theta: value,
74
110
  count: group.length,
111
+ aggregateValue,
75
112
  color: distinctColors[0],
76
113
  startTheta: cursor,
77
114
  endTheta,
@@ -147,8 +184,8 @@ export function deriveArcSectors(rows, layer, {
147
184
  ) {
148
185
  throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
149
186
  }
150
- const sectors = theta.aggregate === "count"
151
- ? countSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
187
+ const sectors = ["count", "sum"].includes(theta.aggregate)
188
+ ? proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
152
189
  : radialSectors(rows, layer, thetaScale, radiusScale);
153
190
  return cloneAndFreeze({ sectors });
154
191
  }
@@ -3,17 +3,19 @@ import {
3
3
  readNominalField,
4
4
  readQuantitativeField,
5
5
  readTemporalField
6
- } from "./scales.js";
6
+ } from "./scales/index.js";
7
7
  import {
8
8
  layoutSeriesPartition,
9
9
  validateColorLayout
10
10
  } from "./seriesLayout.js";
11
+ import { stableOrderPathValues } from "./pathOrder.js";
12
+ import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
11
13
 
12
14
  export function deriveAreaSeries(rows, layer) {
13
15
  if (layer?.mark?.type !== "area") {
14
16
  throw new Error("Area series derivation requires a semantic area mark.");
15
17
  }
16
- const { x, y, x2, y2, group } = layer.encoding ?? {};
18
+ const { x, y, x2, y2, group, color } = layer.encoding ?? {};
17
19
  const vertical =
18
20
  ["quantitative", "temporal"].includes(x?.fieldType) &&
19
21
  y?.fieldType === "quantitative" &&
@@ -46,24 +48,44 @@ export function deriveAreaSeries(rows, layer) {
46
48
  const groupValues = group === undefined
47
49
  ? rows.map(() => undefined)
48
50
  : readNominalField(rows, group.field);
51
+ const colorValues = color === undefined
52
+ ? rows.map(() => undefined)
53
+ : readNominalField(rows, color.field);
54
+ const pathOrder = layer.encoding?.pathOrder;
55
+ const orderValues = pathOrder === undefined
56
+ ? undefined
57
+ : readQuantitativeField(rows, pathOrder.field);
49
58
  const groups = new Map();
50
59
 
51
60
  for (let index = 0; index < rows.length; index += 1) {
52
61
  const key = groupValues[index];
53
62
  const series = groups.get(key) ?? {
54
- key: group === undefined ? {} : { [group.field]: key },
63
+ key: {
64
+ ...(group === undefined ? {} : { [group.field]: key }),
65
+ ...(color === undefined ? {} : { [color.field]: colorValues[index] })
66
+ },
55
67
  values: []
56
68
  };
69
+ if (
70
+ color !== undefined &&
71
+ !Object.is(series.key[color.field], colorValues[index])
72
+ ) {
73
+ throw new Error(
74
+ `Area series "${String(key)}" must have one color value.`
75
+ );
76
+ }
57
77
  series.values.push(vertical
58
78
  ? {
59
79
  x: independentValues[index],
60
80
  y: lower[index],
61
- y2: upper[index]
81
+ y2: upper[index],
82
+ ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
62
83
  }
63
84
  : {
64
85
  x: lower[index],
65
86
  x2: upper[index],
66
- y: independentValues[index]
87
+ y: independentValues[index],
88
+ ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
67
89
  });
68
90
  groups.set(key, series);
69
91
  }
@@ -72,7 +94,13 @@ export function deriveAreaSeries(rows, layer) {
72
94
  }
73
95
  const series = [...groups.values()].map(item => {
74
96
  const key = vertical ? "x" : "y";
75
- const values = item.values.sort((left, right) => left[key] - right[key]);
97
+ const values = pathOrder === undefined
98
+ ? item.values.sort((left, right) => left[key] - right[key])
99
+ : stableOrderPathValues(
100
+ item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
101
+ item.values.map(value => value.pathOrder),
102
+ pathOrder.order
103
+ );
76
104
  if (values.length < 2) {
77
105
  throw new Error(
78
106
  `Area series on mark "${layer.id}" requires at least two points.`
@@ -96,9 +124,17 @@ export function deriveDensityAreaSeries(rows, layer, transform) {
96
124
  if (layer?.mark?.type !== "area") {
97
125
  throw new Error("Density area derivation requires a semantic area mark.");
98
126
  }
127
+ if (layer.encoding?.pathOrder !== undefined) {
128
+ throw new Error(
129
+ `Density area mark "${layer.id}" does not support path order.`
130
+ );
131
+ }
99
132
  if (transform?.type !== "density" || !Array.isArray(transform.as)) {
100
133
  throw new Error(`Area mark "${layer.id}" requires density provenance.`);
101
134
  }
135
+ if (transform.placement?.type === "category") {
136
+ return deriveCategoricalDensitySeries(rows, layer, transform);
137
+ }
102
138
  const { x, y, group } = layer.encoding ?? {};
103
139
  if (
104
140
  x?.fieldType !== "quantitative" ||
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
- import { readNominalField, readTemporalField } from "../scales.js";
2
+ import { readNominalField, readTemporalField } from "../scales/index.js";
3
3
  import {
4
4
  aggregateRows,
5
5
  validateAggregateFieldType,
@@ -0,0 +1,333 @@
1
+ import {
2
+ cloneAndFreeze,
3
+ isPlainObject
4
+ } from "../core/immutable.js";
5
+
6
+ const TRANSFORM_KEYS = Object.freeze([
7
+ "type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
8
+ "resolved"
9
+ ]);
10
+ const BIN_KEYS = Object.freeze(["x", "y"]);
11
+ const EXTENT_KEYS = Object.freeze(["x", "y"]);
12
+ const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
13
+ const RESOLVED_KEYS = Object.freeze([
14
+ "extent", "edges", "eligibleCount", "occupiedCount"
15
+ ]);
16
+
17
+ function requireField(value, label) {
18
+ if (typeof value !== "string" || value.length === 0) {
19
+ throw new TypeError(`${label} must be a non-empty string.`);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ function rejectUnknownKeys(value, supported, label) {
25
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
26
+ if (unknown !== undefined) {
27
+ throw new Error(`Unknown ${label} property "${unknown}".`);
28
+ }
29
+ }
30
+
31
+ function requirePositiveInteger(value, label) {
32
+ if (!Number.isInteger(value) || value <= 0) {
33
+ throw new RangeError(`${label} must be a positive integer.`);
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function normalizeBins(value) {
39
+ if (value === undefined) return { x: 10, y: 10 };
40
+ if (Number.isInteger(value)) return { x: value, y: value };
41
+ if (!isPlainObject(value)) {
42
+ throw new TypeError(
43
+ "2D bin bins must be a positive integer or an object with x and y."
44
+ );
45
+ }
46
+ rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
47
+ return { x: value.x, y: value.y };
48
+ }
49
+
50
+ function validateBins(value) {
51
+ if (!isPlainObject(value)) {
52
+ throw new TypeError("2D bin bins must be an object.");
53
+ }
54
+ rejectUnknownKeys(value, BIN_KEYS, "2D bin bins");
55
+ requirePositiveInteger(value.x, "2D bin x bins");
56
+ requirePositiveInteger(value.y, "2D bin y bins");
57
+ }
58
+
59
+ function normalizeRequestedExtent(value) {
60
+ if (value === undefined) return { x: "auto", y: "auto" };
61
+ if (!isPlainObject(value)) {
62
+ throw new TypeError("2D bin extent must be an object.");
63
+ }
64
+ rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
65
+ return {
66
+ x: value.x ?? "auto",
67
+ y: value.y ?? "auto"
68
+ };
69
+ }
70
+
71
+ function validateExtentValue(value, label, { allowAuto = true } = {}) {
72
+ if (allowAuto && value === "auto") return;
73
+ if (
74
+ !Array.isArray(value) ||
75
+ value.length !== 2 ||
76
+ !value.every(Number.isFinite) ||
77
+ value[0] >= value[1]
78
+ ) {
79
+ throw new RangeError(`${label} must be two increasing finite numbers.`);
80
+ }
81
+ }
82
+
83
+ function validateRequestedExtent(value) {
84
+ if (!isPlainObject(value)) {
85
+ throw new TypeError("2D bin extent must be an object.");
86
+ }
87
+ rejectUnknownKeys(value, EXTENT_KEYS, "2D bin extent");
88
+ validateExtentValue(value.x, "2D bin x extent");
89
+ validateExtentValue(value.y, "2D bin y extent");
90
+ }
91
+
92
+ function normalizeOutputFields(id, value, members) {
93
+ if (value !== undefined && !isPlainObject(value)) {
94
+ throw new TypeError("2D bin as must be an object.");
95
+ }
96
+ if (value !== undefined) rejectUnknownKeys(value, FIELD_KEYS, "2D bin as");
97
+ if (!members && value?.members !== undefined) {
98
+ throw new Error("2D bin as.members requires members: true.");
99
+ }
100
+ return {
101
+ x0: value?.x0 ?? `__${id}_x0`,
102
+ x1: value?.x1 ?? `__${id}_x1`,
103
+ y0: value?.y0 ?? `__${id}_y0`,
104
+ y1: value?.y1 ?? `__${id}_y1`,
105
+ count: value?.count ?? `__${id}_count`,
106
+ ...(members ? { members: value?.members ?? `__${id}_members` } : {})
107
+ };
108
+ }
109
+
110
+ function validateOutputFields(value, members) {
111
+ if (!isPlainObject(value)) {
112
+ throw new TypeError("2D bin as must be an object.");
113
+ }
114
+ rejectUnknownKeys(value, FIELD_KEYS, "2D bin as");
115
+ const required = ["x0", "x1", "y0", "y1", "count"];
116
+ if (members) required.push("members");
117
+ if (!members && value.members !== undefined) {
118
+ throw new Error("2D bin as.members requires members: true.");
119
+ }
120
+ const fields = required.map(role => requireField(
121
+ value[role],
122
+ `2D bin ${role} output field`
123
+ ));
124
+ if (new Set(fields).size !== fields.length) {
125
+ throw new Error("2D bin output fields must be unique.");
126
+ }
127
+ }
128
+
129
+ function validateEdges(value, extent, count, axis) {
130
+ if (
131
+ !Array.isArray(value) ||
132
+ value.length !== count + 1 ||
133
+ !value.every(Number.isFinite)
134
+ ) {
135
+ throw new TypeError(`Resolved 2D bin ${axis} edges are invalid.`);
136
+ }
137
+ if (value[0] !== extent[0] || value.at(-1) !== extent[1]) {
138
+ throw new Error(`Resolved 2D bin ${axis} edges must match its extent.`);
139
+ }
140
+ for (let index = 1; index < value.length; index += 1) {
141
+ if (value[index] <= value[index - 1]) {
142
+ throw new Error(`Resolved 2D bin ${axis} edges must be increasing.`);
143
+ }
144
+ }
145
+ }
146
+
147
+ function validateResolved(value, bins) {
148
+ if (!isPlainObject(value)) {
149
+ throw new TypeError("Resolved 2D bin state must be a plain object.");
150
+ }
151
+ rejectUnknownKeys(value, RESOLVED_KEYS, "resolved 2D bin");
152
+ if (!isPlainObject(value.extent) || !isPlainObject(value.edges)) {
153
+ throw new TypeError("Resolved 2D bin extent and edges must be objects.");
154
+ }
155
+ rejectUnknownKeys(value.extent, EXTENT_KEYS, "resolved 2D bin extent");
156
+ rejectUnknownKeys(value.edges, EXTENT_KEYS, "resolved 2D bin edges");
157
+ for (const axis of ["x", "y"]) {
158
+ validateExtentValue(
159
+ value.extent[axis],
160
+ `Resolved 2D bin ${axis} extent`,
161
+ { allowAuto: false }
162
+ );
163
+ validateEdges(value.edges[axis], value.extent[axis], bins[axis], axis);
164
+ }
165
+ for (const property of ["eligibleCount", "occupiedCount"]) {
166
+ if (!Number.isInteger(value[property]) || value[property] < 0) {
167
+ throw new RangeError(`Resolved 2D bin ${property} must be a non-negative integer.`);
168
+ }
169
+ }
170
+ if (value.occupiedCount > bins.x * bins.y) {
171
+ throw new RangeError("Resolved 2D bin occupiedCount exceeds the grid size.");
172
+ }
173
+ }
174
+
175
+ export function validateBin2DTransform(transform) {
176
+ if (!isPlainObject(transform)) {
177
+ throw new TypeError("2D bin transform must be a plain object.");
178
+ }
179
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "2D bin transform");
180
+ if (transform.type !== "bin2d") {
181
+ throw new Error(`Unsupported 2D bin transform "${transform.type}".`);
182
+ }
183
+ requireField(transform.x, "2D bin x field");
184
+ requireField(transform.y, "2D bin y field");
185
+ validateBins(transform.bins);
186
+ validateRequestedExtent(transform.extent);
187
+ if (typeof transform.includeEmpty !== "boolean") {
188
+ throw new TypeError("2D bin includeEmpty must be a boolean.");
189
+ }
190
+ if (typeof transform.members !== "boolean") {
191
+ throw new TypeError("2D bin members must be a boolean.");
192
+ }
193
+ validateOutputFields(transform.as, transform.members);
194
+ if (transform.resolved !== undefined) {
195
+ validateResolved(transform.resolved, transform.bins);
196
+ }
197
+ return transform;
198
+ }
199
+
200
+ export function normalizeBin2DTransform({
201
+ id,
202
+ x,
203
+ y,
204
+ bins,
205
+ extent,
206
+ includeEmpty = false,
207
+ members = false,
208
+ as
209
+ } = {}) {
210
+ const normalizedBins = normalizeBins(bins);
211
+ const transform = {
212
+ type: "bin2d",
213
+ x,
214
+ y,
215
+ bins: normalizedBins,
216
+ extent: normalizeRequestedExtent(extent),
217
+ includeEmpty,
218
+ members,
219
+ as: normalizeOutputFields(id, as, members)
220
+ };
221
+ validateBin2DTransform(transform);
222
+ return cloneAndFreeze(transform);
223
+ }
224
+
225
+ function resolveExtent(values, requested, axis) {
226
+ const minimum = Math.min(...values);
227
+ const maximum = Math.max(...values);
228
+ const extent = requested === "auto" ? [minimum, maximum] : requested;
229
+ if (extent[0] > minimum || extent[1] < maximum) {
230
+ throw new RangeError(
231
+ `2D bin ${axis} extent must contain every eligible value.`
232
+ );
233
+ }
234
+ if (extent[0] === extent[1]) {
235
+ throw new RangeError(`2D bin ${axis} extent must have positive span.`);
236
+ }
237
+ return [...extent];
238
+ }
239
+
240
+ function createEdges(extent, count) {
241
+ const [minimum, maximum] = extent;
242
+ const step = (maximum - minimum) / count;
243
+ return Array.from({ length: count + 1 }, (_, index) =>
244
+ index === count ? maximum : minimum + step * index
245
+ );
246
+ }
247
+
248
+ function findBinIndex(value, edges) {
249
+ if (value === edges.at(-1)) return edges.length - 2;
250
+ let low = 0;
251
+ let high = edges.length - 1;
252
+ while (low + 1 < high) {
253
+ const middle = Math.floor((low + high) / 2);
254
+ if (value < edges[middle]) high = middle;
255
+ else low = middle;
256
+ }
257
+ return low;
258
+ }
259
+
260
+ export function deriveBin2DRows(rows, transform) {
261
+ if (!Array.isArray(rows)) {
262
+ throw new TypeError("2D bin source rows must be an array.");
263
+ }
264
+ validateBin2DTransform(transform);
265
+ const eligible = rows.flatMap((row, index) =>
266
+ isPlainObject(row) &&
267
+ Number.isFinite(row[transform.x]) &&
268
+ Number.isFinite(row[transform.y])
269
+ ? [{ index, row }]
270
+ : []
271
+ );
272
+ if (eligible.length === 0) {
273
+ throw new Error(
274
+ "2D bin requires at least one row with finite x and y values."
275
+ );
276
+ }
277
+ const extent = {
278
+ x: resolveExtent(
279
+ eligible.map(item => item.row[transform.x]),
280
+ transform.extent.x,
281
+ "x"
282
+ ),
283
+ y: resolveExtent(
284
+ eligible.map(item => item.row[transform.y]),
285
+ transform.extent.y,
286
+ "y"
287
+ )
288
+ };
289
+ const edges = {
290
+ x: createEdges(extent.x, transform.bins.x),
291
+ y: createEdges(extent.y, transform.bins.y)
292
+ };
293
+ const buckets = Array.from(
294
+ { length: transform.bins.x * transform.bins.y },
295
+ () => []
296
+ );
297
+ for (const item of eligible) {
298
+ const xIndex = findBinIndex(item.row[transform.x], edges.x);
299
+ const yIndex = findBinIndex(item.row[transform.y], edges.y);
300
+ buckets[yIndex * transform.bins.x + xIndex].push(item.index);
301
+ }
302
+ const values = [];
303
+ for (let yIndex = 0; yIndex < transform.bins.y; yIndex += 1) {
304
+ for (let xIndex = 0; xIndex < transform.bins.x; xIndex += 1) {
305
+ const memberIndexes = buckets[yIndex * transform.bins.x + xIndex];
306
+ if (!transform.includeEmpty && memberIndexes.length === 0) continue;
307
+ values.push({
308
+ [transform.as.x0]: edges.x[xIndex],
309
+ [transform.as.x1]: edges.x[xIndex + 1],
310
+ [transform.as.y0]: edges.y[yIndex],
311
+ [transform.as.y1]: edges.y[yIndex + 1],
312
+ [transform.as.count]: memberIndexes.length,
313
+ ...(transform.members
314
+ ? { [transform.as.members]: [...memberIndexes] }
315
+ : {})
316
+ });
317
+ }
318
+ }
319
+ return cloneAndFreeze({
320
+ values,
321
+ resolved: {
322
+ extent,
323
+ edges,
324
+ eligibleCount: eligible.length,
325
+ occupiedCount: buckets.filter(bucket => bucket.length > 0).length
326
+ }
327
+ });
328
+ }
329
+
330
+ export function requestedBin2DTransform(transform) {
331
+ const { resolved: _resolved, ...requested } = transform;
332
+ return cloneAndFreeze(requested);
333
+ }