ggaction 0.0.20 → 0.0.22

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 (254) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/interval.js +4 -3
  165. package/src/grammar/jitter.js +2 -2
  166. package/src/grammar/legendSpacing.js +45 -0
  167. package/src/grammar/lineMissing.js +90 -0
  168. package/src/grammar/lineSeries.js +1 -376
  169. package/src/grammar/lookup.js +97 -0
  170. package/src/grammar/markFilter.js +4 -2
  171. package/src/grammar/markLabels.js +8 -3
  172. package/src/grammar/numeric.js +1 -1
  173. package/src/grammar/pathCommands.js +1 -1
  174. package/src/grammar/pathSeries.js +1 -109
  175. package/src/grammar/pointPolygon.js +81 -0
  176. package/src/grammar/pointShapes.js +15 -15
  177. package/src/grammar/rects.js +1 -1
  178. package/src/grammar/regression/derive.js +1 -195
  179. package/src/grammar/scales/appearance.js +1 -1
  180. package/src/grammar/scales/color.js +1 -1
  181. package/src/grammar/scales/colorConsumers.js +1 -63
  182. package/src/grammar/scales/continuous.js +58 -1
  183. package/src/grammar/scales/definition.js +1 -1
  184. package/src/grammar/scales/ordinal.js +5 -2
  185. package/src/grammar/scales/policies.js +3 -3
  186. package/src/grammar/schemas/graphicBounds.js +40 -12
  187. package/src/grammar/schemas/graphicClip.js +23 -0
  188. package/src/grammar/schemas/graphicTree.js +16 -0
  189. package/src/grammar/statisticalReference.js +22 -0
  190. package/src/grammar/text.js +65 -1
  191. package/src/grammar/ticks.js +92 -1
  192. package/src/grammar/transformTopology.js +3 -1
  193. package/src/grammar/transforms.js +24 -0
  194. package/src/inspection.js +1 -495
  195. package/src/inspectionDescriptors.js +1 -1
  196. package/src/layout/canvas.js +1 -1
  197. package/src/layout/facetContentFlow.js +101 -0
  198. package/src/layout/facetHeaderTitles.js +1 -0
  199. package/src/layout/facetTopLane.js +29 -0
  200. package/src/layout/facets.js +1 -1
  201. package/src/layout/legendGroup.js +1 -1
  202. package/src/layout/legendItems.js +26 -10
  203. package/src/layout/legendSpacing.js +66 -0
  204. package/src/layout/plotDrawOrder.js +17 -0
  205. package/src/materialization/bars/histogram.js +8 -2
  206. package/src/materialization/bars/resolve.js +1 -1
  207. package/src/materialization/composition.js +1 -0
  208. package/src/materialization/compositionSnapshot.js +22 -2
  209. package/src/materialization/dataProvenance.js +1 -1
  210. package/src/materialization/facetGuides/layout.js +1 -1
  211. package/src/materialization/facetGuides/placement.js +23 -11
  212. package/src/materialization/facetGuides/preparation.js +43 -24
  213. package/src/materialization/facetHeaders.js +1 -1
  214. package/src/materialization/facetTopLane.js +25 -0
  215. package/src/materialization/facets.js +1 -1
  216. package/src/materialization/graphicHierarchy.js +5 -0
  217. package/src/materialization/graphicTranslation.js +2 -0
  218. package/src/materialization/guides/layout.js +1 -1
  219. package/src/materialization/guides/resources.js +1 -1
  220. package/src/materialization/legends.js +1 -1
  221. package/src/materialization/markClip.js +26 -0
  222. package/src/materialization/marks/capabilities.js +3 -2
  223. package/src/materialization/marks/grouping.js +7 -5
  224. package/src/materialization/marks/index.js +4 -2
  225. package/src/materialization/marks/policies.js +4 -0
  226. package/src/materialization/plotDrawOrder.js +44 -0
  227. package/src/materialization/rect.js +11 -3
  228. package/src/materialization/scaleGuideDependencies.js +13 -5
  229. package/src/materialization/selection/items/common.js +1 -1
  230. package/src/materialization/selection/items/path.js +1 -0
  231. package/src/materialization/selection/policies/index.js +2 -1
  232. package/src/materialization/text.js +10 -5
  233. package/src/mcp/cli.js +0 -0
  234. package/src/persistence/validation.js +108 -18
  235. package/src/persistence.js +4 -2
  236. package/src/renderers/canvas/index.js +16 -0
  237. package/src/renderers/canvas/isolated.js +11 -1
  238. package/src/renderers/canvas/native.js +7 -0
  239. package/src/renderers/canvas/scene.js +66 -0
  240. package/src/renderers/canvas/text.js +3 -2
  241. package/src/renderers/svg.js +1 -617
  242. package/src/renderers/svgSerialization.js +643 -0
  243. package/src/renderers/transitionScene.js +67 -0
  244. package/src/scene.js +89 -0
  245. package/src/selectors/axes.js +1 -1
  246. package/src/selectors/datasets.js +1 -0
  247. package/src/selectors/grids.js +9 -0
  248. package/src/selectors/markClip.js +28 -0
  249. package/src/selectors/raincloud.js +7 -0
  250. package/src/version.js +1 -1
  251. package/types/basic.d.ts +7 -0
  252. package/types/index.d.ts +8 -0
  253. package/types/program.d.ts +270 -60
  254. package/types/scene.d.ts +35 -0
@@ -1,494 +1 @@
1
- import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import {
3
- validateGeneratedItemLimit,
4
- validateNonEmptyString as requireField
5
- } from "../core/validation.js";
6
- import { normalizePalette } from "./palettes.js";
7
- import { interpolateNumber, inverseLerp } from "./numeric.js";
8
- import { requireFiniteResult } from "./numeric.js";
9
- import {
10
- isNominalValue,
11
- normalizeTemporalValue,
12
- resolveTemporalUnit
13
- } from "./scales/index.js";
14
-
15
- export const HORIZON_RESOLUTIONS = Object.freeze(["shared", "independent"]);
16
- export const HORIZON_MISSING_POLICIES = Object.freeze(["break", "error"]);
17
- export const HORIZON_OVERFLOW_POLICIES = Object.freeze(["clip", "error"]);
18
-
19
- const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType", "temporalUnit"]);
20
- const OUTPUT_PROPERTIES = Object.freeze([
21
- "x", "lower", "upper", "group", "color", "sign", "band", "segment"
22
- ]);
23
-
24
- function validateEncoding(value, role) {
25
- if (!isPlainObject(value)) {
26
- throw new TypeError(`Horizon ${role} encoding must be a plain object.`);
27
- }
28
- const unknown = Object.keys(value).find(
29
- property => !ENCODING_PROPERTIES.includes(property)
30
- );
31
- if (unknown !== undefined) {
32
- throw new Error(`Unknown Horizon ${role} encoding property "${unknown}".`);
33
- }
34
- const field = requireField(value.field, `Horizon ${role} field`);
35
- const fieldType = value.fieldType ?? "quantitative";
36
- const supportsTemporal = role === "x";
37
- if (
38
- fieldType !== "quantitative" &&
39
- (!supportsTemporal || fieldType !== "temporal")
40
- ) {
41
- throw new Error(
42
- `Horizon ${role} fieldType must be quantitative${supportsTemporal ? " or temporal" : ""}.`
43
- );
44
- }
45
- const temporalUnit = resolveTemporalUnit(value, fieldType);
46
- return { field, fieldType, ...(temporalUnit === undefined ? {} : { temporalUnit }) };
47
- }
48
-
49
- function validateOutputFields(value) {
50
- if (!isPlainObject(value)) {
51
- throw new TypeError("Horizon as must be a plain object.");
52
- }
53
- const unknown = Object.keys(value).find(
54
- property => !OUTPUT_PROPERTIES.includes(property)
55
- );
56
- if (unknown !== undefined) {
57
- throw new Error(`Unknown Horizon output field "${unknown}".`);
58
- }
59
- const output = Object.fromEntries(OUTPUT_PROPERTIES.map(property => [
60
- property,
61
- requireField(value[property], `Horizon ${property} output field`)
62
- ]));
63
- if (new Set(Object.values(output)).size !== OUTPUT_PROPERTIES.length) {
64
- throw new Error("Horizon output fields must be distinct.");
65
- }
66
- return output;
67
- }
68
-
69
- function validatePalette(value = {}) {
70
- if (!isPlainObject(value)) {
71
- throw new TypeError("Horizon palette must be a plain object.");
72
- }
73
- const unknown = Object.keys(value).find(
74
- property => !["positive", "negative"].includes(property)
75
- );
76
- if (unknown !== undefined) {
77
- throw new Error(`Unknown Horizon palette property "${unknown}".`);
78
- }
79
- return {
80
- positive: normalizePalette(value.positive ?? "blues"),
81
- negative: normalizePalette(value.negative ?? "reds")
82
- };
83
- }
84
-
85
- function validateResolved(value) {
86
- if (!isPlainObject(value) || Object.keys(value).some(key => key !== "extents")) {
87
- throw new TypeError("Horizon resolved provenance must contain only extents.");
88
- }
89
- if (!Array.isArray(value.extents) || value.extents.length === 0) {
90
- throw new TypeError("Horizon resolved extents must be a non-empty array.");
91
- }
92
- for (const [index, entry] of value.extents.entries()) {
93
- if (
94
- !isPlainObject(entry) ||
95
- Object.keys(entry).some(key => !["group", "extent", "bandHeight"].includes(key)) ||
96
- !Number.isFinite(entry.extent) ||
97
- entry.extent < 0 ||
98
- !Number.isFinite(entry.bandHeight) ||
99
- entry.bandHeight < 0
100
- ) {
101
- throw new TypeError(`Horizon resolved extent ${index} is invalid.`);
102
- }
103
- if (Object.hasOwn(entry, "group") && !isNominalValue(entry.group)) {
104
- throw new TypeError(`Horizon resolved group ${index} must be nominal.`);
105
- }
106
- }
107
- return value;
108
- }
109
-
110
- export function requestedHorizonTransform(transform) {
111
- const { resolved: _resolved, ...requested } = transform;
112
- void _resolved;
113
- return requested;
114
- }
115
-
116
- export function validateHorizonTransform(transform) {
117
- if (!isPlainObject(transform)) {
118
- throw new TypeError("Horizon transform must be a plain object.");
119
- }
120
- const supported = [
121
- "type", "x", "y", "groupBy", "bands", "baseline", "extent",
122
- "resolve", "missing", "overflow", "palette", "as", "resolved"
123
- ];
124
- const unknown = Object.keys(transform).find(key => !supported.includes(key));
125
- if (unknown !== undefined) {
126
- throw new Error(`Unknown Horizon transform property "${unknown}".`);
127
- }
128
- if (transform.type !== "horizon") {
129
- throw new Error(`Unsupported Horizon transform "${transform.type}".`);
130
- }
131
- const x = validateEncoding(transform.x, "x");
132
- const y = validateEncoding(transform.y, "y");
133
- if (x.field === y.field) {
134
- throw new Error("Horizon x and y fields must differ.");
135
- }
136
- if (transform.groupBy !== undefined) {
137
- requireField(transform.groupBy, "Horizon groupBy");
138
- if ([x.field, y.field].includes(transform.groupBy)) {
139
- throw new Error("Horizon groupBy must differ from x and y fields.");
140
- }
141
- }
142
- if (!Number.isInteger(transform.bands) || transform.bands <= 0) {
143
- throw new RangeError("Horizon bands must be a positive integer.");
144
- }
145
- validateGeneratedItemLimit(transform.bands, "Horizon bands");
146
- if (!Number.isFinite(transform.baseline)) {
147
- throw new TypeError("Horizon baseline must be finite.");
148
- }
149
- if (
150
- transform.extent !== "auto" &&
151
- (!Number.isFinite(transform.extent) || transform.extent <= 0)
152
- ) {
153
- throw new RangeError('Horizon extent must be "auto" or a positive number.');
154
- }
155
- if (!HORIZON_RESOLUTIONS.includes(transform.resolve)) {
156
- throw new Error(`Unknown Horizon resolve "${transform.resolve}".`);
157
- }
158
- if (!HORIZON_MISSING_POLICIES.includes(transform.missing)) {
159
- throw new Error(`Unknown Horizon missing policy "${transform.missing}".`);
160
- }
161
- if (!HORIZON_OVERFLOW_POLICIES.includes(transform.overflow)) {
162
- throw new Error(`Unknown Horizon overflow policy "${transform.overflow}".`);
163
- }
164
- const palette = validatePalette(transform.palette);
165
- for (const [sign, value] of Object.entries(palette)) {
166
- if (value.count !== undefined && value.count !== transform.bands) {
167
- throw new Error(
168
- `Horizon ${sign} palette count must equal bands (${transform.bands}).`
169
- );
170
- }
171
- }
172
- const as = validateOutputFields(transform.as);
173
- if (transform.groupBy !== undefined && Object.values(as).includes(transform.groupBy)) {
174
- throw new Error("Horizon output fields must not collide with groupBy.");
175
- }
176
- if (transform.resolved !== undefined) validateResolved(transform.resolved);
177
- return cloneAndFreeze({
178
- ...transform,
179
- x,
180
- y,
181
- palette,
182
- as
183
- });
184
- }
185
-
186
- function orderedX(value, encoding, rowIndex) {
187
- if (encoding.fieldType === "temporal") {
188
- return normalizeTemporalValue(value, encoding.field, rowIndex, encoding.temporalUnit);
189
- }
190
- if (!Number.isFinite(value)) {
191
- throw new TypeError(
192
- `Horizon x field "${encoding.field}" must contain finite numbers at row ${rowIndex}.`
193
- );
194
- }
195
- return value;
196
- }
197
-
198
- function groupRows(rows, transform) {
199
- const groups = [];
200
- const indexed = new Map();
201
- for (const [sourceRowIndex, row] of rows.entries()) {
202
- if (!isPlainObject(row)) {
203
- throw new TypeError(`Horizon row ${sourceRowIndex} must be a plain object.`);
204
- }
205
- const group = transform.groupBy === undefined ? undefined : row[transform.groupBy];
206
- if (transform.groupBy !== undefined && !isNominalValue(group)) {
207
- throw new TypeError(
208
- `Horizon groupBy field "${transform.groupBy}" must contain nominal values at row ${sourceRowIndex}.`
209
- );
210
- }
211
- const key = transform.groupBy === undefined
212
- ? "__horizon_ungrouped__"
213
- : `${typeof group}:${String(group)}`;
214
- if (!indexed.has(key)) {
215
- const entry = { group, groupIndex: groups.length, rows: [] };
216
- indexed.set(key, entry);
217
- groups.push(entry);
218
- }
219
- const y = row[transform.y.field];
220
- const missing = !Number.isFinite(y);
221
- if (missing && transform.missing === "error") {
222
- throw new Error(
223
- `Horizon y field "${transform.y.field}" is missing at source row ${sourceRowIndex}.`
224
- );
225
- }
226
- indexed.get(key).rows.push({
227
- sourceRowIndex,
228
- x: orderedX(row[transform.x.field], transform.x, sourceRowIndex),
229
- y,
230
- missing
231
- });
232
- }
233
- for (const entry of groups) {
234
- entry.rows.sort((left, right) =>
235
- left.x - right.x || left.sourceRowIndex - right.sourceRowIndex
236
- );
237
- for (let index = 1; index < entry.rows.length; index += 1) {
238
- if (entry.rows[index - 1].x === entry.rows[index].x) {
239
- throw new Error(
240
- `Horizon group ${JSON.stringify(entry.group ?? null)} has duplicate x value ${entry.rows[index].x}.`
241
- );
242
- }
243
- }
244
- }
245
- return groups;
246
- }
247
-
248
- function splitSegments(rows, baseline) {
249
- const segments = [];
250
- let current = [];
251
- const flush = () => {
252
- if (current.length > 0) segments.push(current);
253
- current = [];
254
- };
255
- for (const row of rows) {
256
- if (row.missing) {
257
- flush();
258
- continue;
259
- }
260
- current.push({
261
- ...row,
262
- signed: requireFiniteResult(
263
- row.y - baseline,
264
- "Horizon signed deviation"
265
- ),
266
- interpolated: false
267
- });
268
- }
269
- flush();
270
- return segments;
271
- }
272
-
273
- function insertCrossings(points, baseline) {
274
- if (points.length < 2) return points.map(point => ({ ...point }));
275
- const output = [{ ...points[0] }];
276
- for (let index = 1; index < points.length; index += 1) {
277
- const left = points[index - 1];
278
- const right = points[index];
279
- if (left.signed * right.signed < 0) {
280
- const fraction = inverseLerp(baseline, left.y, right.y);
281
- output.push({
282
- sourceRowIndex: undefined,
283
- x: interpolateNumber(left.x, right.x, fraction),
284
- y: baseline,
285
- missing: false,
286
- signed: 0,
287
- interpolated: true,
288
- leftSourceRowIndex: left.sourceRowIndex,
289
- rightSourceRowIndex: right.sourceRowIndex,
290
- fraction
291
- });
292
- }
293
- output.push({ ...right });
294
- }
295
- return output;
296
- }
297
-
298
- function pointSign(value) {
299
- return value > 0 ? "positive" : value < 0 ? "negative" : "zero";
300
- }
301
-
302
- function runsForSign(points, desired) {
303
- const runs = [];
304
- let current = [];
305
- let pendingZeros = [];
306
- const flush = () => {
307
- if (current.some(point => pointSign(point.signed) === desired)) {
308
- runs.push(current);
309
- }
310
- current = [];
311
- };
312
- for (const point of points) {
313
- const sign = pointSign(point.signed);
314
- if (sign === desired) {
315
- if (current.length === 0 && pendingZeros.length > 0) {
316
- current.push(...pendingZeros);
317
- }
318
- current.push(point);
319
- pendingZeros = [];
320
- } else if (sign === "zero") {
321
- if (current.length > 0) {
322
- current.push(point);
323
- flush();
324
- }
325
- pendingZeros = [point];
326
- } else {
327
- flush();
328
- pendingZeros = [];
329
- }
330
- }
331
- flush();
332
- return runs;
333
- }
334
-
335
- function maximumMagnitude(group) {
336
- let maximum = 0;
337
- for (const segment of group.segments ?? [group]) {
338
- for (const point of segment) {
339
- maximum = Math.max(maximum, Math.abs(point.signed));
340
- }
341
- }
342
- return maximum;
343
- }
344
-
345
- function resolvedExtents(groups, transform) {
346
- if (transform.extent !== "auto") return groups.map(() => transform.extent);
347
- if (transform.resolve === "shared") {
348
- let shared = 0;
349
- for (const group of groups) shared = Math.max(shared, maximumMagnitude(group));
350
- return groups.map(() => shared);
351
- }
352
- return groups.map(maximumMagnitude);
353
- }
354
-
355
- function validateOutputBudget(groups, extents, transform) {
356
- let count = 0;
357
- for (const [groupIndex, group] of groups.entries()) {
358
- const extent = extents[groupIndex];
359
- if (extent === 0) continue;
360
- const bandHeight = extent / transform.bands;
361
- if (!(bandHeight > 0) || !Number.isFinite(bandHeight)) {
362
- throw new RangeError(
363
- "Horizon band height is outside the positive finite numeric range."
364
- );
365
- }
366
- for (const sign of ["negative", "positive"]) {
367
- for (const segment of group.segments) {
368
- for (const run of runsForSign(segment, sign)) {
369
- const magnitude = maximumMagnitude(run);
370
- count += Math.min(
371
- transform.bands,
372
- Math.ceil(Math.min(magnitude, extent) / bandHeight)
373
- ) * run.length;
374
- validateGeneratedItemLimit(count, "Horizon generated row count");
375
- }
376
- }
377
- }
378
- }
379
- }
380
-
381
- function seriesForGroup(group, extent, transform) {
382
- if (extent === 0) return [];
383
- const bandHeight = extent / transform.bands;
384
- const output = [];
385
- for (const sign of ["negative", "positive"]) {
386
- const runs = [];
387
- let segmentIndex = 0;
388
- for (const source of group.segments) {
389
- for (const points of runsForSign(source, sign)) {
390
- runs.push({ points, segmentIndex });
391
- segmentIndex += 1;
392
- }
393
- }
394
- for (let bandIndex = 0; bandIndex < transform.bands; bandIndex += 1) {
395
- for (const run of runs) {
396
- const points = run.points.map(point => {
397
- const rawMagnitude = Math.abs(point.signed);
398
- if (rawMagnitude > extent && transform.overflow === "error") {
399
- throw new RangeError(
400
- `Horizon magnitude ${rawMagnitude} exceeds extent ${extent}.`
401
- );
402
- }
403
- const magnitude = Math.min(rawMagnitude, extent);
404
- const amplitude = Math.min(
405
- bandHeight,
406
- Math.max(0, magnitude - bandIndex * bandHeight)
407
- );
408
- return {
409
- ...point,
410
- magnitude,
411
- rawMagnitude,
412
- amplitude,
413
- fraction: amplitude / bandHeight,
414
- overflowed: rawMagnitude > extent
415
- };
416
- });
417
- if (points.some(point => point.amplitude > 0)) {
418
- const groupIdentity = transform.groupBy === undefined
419
- ? null
420
- : group.group;
421
- output.push({
422
- group: group.group,
423
- groupIndex: group.groupIndex,
424
- sign,
425
- bandIndex,
426
- segmentIndex: run.segmentIndex,
427
- seriesKey: JSON.stringify([
428
- groupIdentity,
429
- sign,
430
- bandIndex,
431
- run.segmentIndex
432
- ]),
433
- colorKey: `${sign}:${bandIndex}`,
434
- bandHeight,
435
- points
436
- });
437
- }
438
- }
439
- }
440
- }
441
- return output;
442
- }
443
-
444
- function generatedRows(series, transform) {
445
- return series.flatMap(item => item.points.map(point => ({
446
- ...(transform.groupBy === undefined
447
- ? {}
448
- : { [transform.groupBy]: item.group }),
449
- [transform.as.x]: point.x,
450
- [transform.as.lower]: 0,
451
- [transform.as.upper]: point.fraction,
452
- [transform.as.group]: item.seriesKey,
453
- [transform.as.color]: item.colorKey,
454
- [transform.as.sign]: item.sign,
455
- [transform.as.band]: item.bandIndex,
456
- [transform.as.segment]: item.segmentIndex
457
- })));
458
- }
459
-
460
- export function deriveHorizon(rows, requested) {
461
- if (!Array.isArray(rows)) {
462
- throw new TypeError("Horizon values must be an array.");
463
- }
464
- if (rows.length === 0) {
465
- throw new Error("Horizon requires at least one source row.");
466
- }
467
- const transform = validateHorizonTransform(requested);
468
- const grouped = groupRows(rows, transform).map(group => ({
469
- ...group,
470
- segments: splitSegments(group.rows, transform.baseline).map(segment =>
471
- insertCrossings(segment, transform.baseline)
472
- )
473
- }));
474
- if (!grouped.some(group => group.rows.some(row => !row.missing))) {
475
- throw new Error("Horizon requires at least one finite y value.");
476
- }
477
- const extents = resolvedExtents(grouped, transform);
478
- validateOutputBudget(grouped, extents, transform);
479
- const groups = grouped.map((group, index) => ({
480
- ...(transform.groupBy === undefined ? {} : { group: group.group }),
481
- extent: extents[index],
482
- bandHeight: extents[index] / transform.bands
483
- }));
484
- const series = grouped.flatMap((group, index) =>
485
- seriesForGroup(group, extents[index], transform)
486
- );
487
- return cloneAndFreeze({
488
- transform: requestedHorizonTransform(transform),
489
- resolved: { extents: groups },
490
- groups,
491
- series,
492
- values: generatedRows(series, transform)
493
- });
494
- }
1
+ import{cloneAndFreeze as x,isPlainObject as a}from"../core/immutable.js";import{validateGeneratedItemLimit as m,validateNonEmptyString as g}from"../core/validation.js";import{normalizePalette as z}from"./palettes.js";import{interpolateNumber as O,inverseLerp as I}from"./numeric.js";import{requireFiniteResult as T}from"./numeric.js";import{isNominalValue as v,normalizeTemporalValue as R,resolveTemporalUnit as j}from"./scales/index.js";const B=Object.freeze(["shared","independent"]),N=Object.freeze(["break","error"]),S=Object.freeze(["clip","error"]),k=Object.freeze(["field","fieldType","temporalUnit"]),w=Object.freeze(["x","lower","upper","group","color","sign","band","segment"]);function H(e,o){if(!a(e))throw new TypeError(`Horizon ${o} encoding must be a plain object.`);const n=Object.keys(e).find(u=>!k.includes(u));if(n!==void 0)throw new Error(`Unknown Horizon ${o} encoding property "${n}".`);const t=g(e.field,`Horizon ${o} field`),r=e.fieldType??"quantitative",i=o==="x";if(r!=="quantitative"&&(!i||r!=="temporal"))throw new Error(`Horizon ${o} fieldType must be quantitative${i?" or temporal":""}.`);const s=j(e,r);return{field:t,fieldType:r,...s===void 0?{}:{temporalUnit:s}}}function M(e){if(!a(e))throw new TypeError("Horizon as must be a plain object.");const o=Object.keys(e).find(t=>!w.includes(t));if(o!==void 0)throw new Error(`Unknown Horizon output field "${o}".`);const n=Object.fromEntries(w.map(t=>[t,g(e[t],`Horizon ${t} output field`)]));if(new Set(Object.values(n)).size!==w.length)throw new Error("Horizon output fields must be distinct.");return n}function U(e={}){if(!a(e))throw new TypeError("Horizon palette must be a plain object.");const o=Object.keys(e).find(n=>!["positive","negative"].includes(n));if(o!==void 0)throw new Error(`Unknown Horizon palette property "${o}".`);return{positive:z(e.positive??"blues"),negative:z(e.negative??"reds")}}function F(e){if(!a(e)||Object.keys(e).some(o=>o!=="extents"))throw new TypeError("Horizon resolved provenance must contain only extents.");if(!Array.isArray(e.extents)||e.extents.length===0)throw new TypeError("Horizon resolved extents must be a non-empty array.");for(const[o,n]of e.extents.entries()){if(!a(n)||Object.keys(n).some(t=>!["group","extent","bandHeight"].includes(t))||!Number.isFinite(n.extent)||n.extent<0||!Number.isFinite(n.bandHeight)||n.bandHeight<0)throw new TypeError(`Horizon resolved extent ${o} is invalid.`);if(Object.hasOwn(n,"group")&&!v(n.group))throw new TypeError(`Horizon resolved group ${o} must be nominal.`)}return e}function _(e){const{resolved:o,...n}=e;return n}function P(e){if(!a(e))throw new TypeError("Horizon transform must be a plain object.");const o=["type","x","y","groupBy","bands","baseline","extent","resolve","missing","overflow","palette","as","resolved"],n=Object.keys(e).find(u=>!o.includes(u));if(n!==void 0)throw new Error(`Unknown Horizon transform property "${n}".`);if(e.type!=="horizon")throw new Error(`Unsupported Horizon transform "${e.type}".`);const t=H(e.x,"x"),r=H(e.y,"y");if(t.field===r.field)throw new Error("Horizon x and y fields must differ.");if(e.groupBy!==void 0&&(g(e.groupBy,"Horizon groupBy"),[t.field,r.field].includes(e.groupBy)))throw new Error("Horizon groupBy must differ from x and y fields.");if(!Number.isInteger(e.bands)||e.bands<=0)throw new RangeError("Horizon bands must be a positive integer.");if(m(e.bands,"Horizon bands"),!Number.isFinite(e.baseline))throw new TypeError("Horizon baseline must be finite.");if(e.extent!=="auto"&&(!Number.isFinite(e.extent)||e.extent<=0))throw new RangeError('Horizon extent must be "auto" or a positive number.');if(!B.includes(e.resolve))throw new Error(`Unknown Horizon resolve "${e.resolve}".`);if(!N.includes(e.missing))throw new Error(`Unknown Horizon missing policy "${e.missing}".`);if(!S.includes(e.overflow))throw new Error(`Unknown Horizon overflow policy "${e.overflow}".`);const i=U(e.palette);for(const[u,d]of Object.entries(i))if(d.count!==void 0&&d.count!==e.bands)throw new Error(`Horizon ${u} palette count must equal bands (${e.bands}).`);const s=M(e.as);if(e.groupBy!==void 0&&Object.values(s).includes(e.groupBy))throw new Error("Horizon output fields must not collide with groupBy.");return e.resolved!==void 0&&F(e.resolved),x({...e,x:t,y:r,palette:i,as:s})}function q(e,o,n){if(o.fieldType==="temporal")return R(e,o.field,n,o.temporalUnit);if(!Number.isFinite(e))throw new TypeError(`Horizon x field "${o.field}" must contain finite numbers at row ${n}.`);return e}function L(e,o){const n=[],t=new Map;for(const[r,i]of e.entries()){if(!a(i))throw new TypeError(`Horizon row ${r} must be a plain object.`);const s=o.groupBy===void 0?void 0:i[o.groupBy];if(o.groupBy!==void 0&&!v(s))throw new TypeError(`Horizon groupBy field "${o.groupBy}" must contain nominal values at row ${r}.`);const u=o.groupBy===void 0?"__horizon_ungrouped__":`${typeof s}:${String(s)}`;if(!t.has(u)){const l={group:s,groupIndex:n.length,rows:[]};t.set(u,l),n.push(l)}const d=i[o.y.field],p=!Number.isFinite(d);if(p&&o.missing==="error")throw new Error(`Horizon y field "${o.y.field}" is missing at source row ${r}.`);t.get(u).rows.push({sourceRowIndex:r,x:q(i[o.x.field],o.x,r),y:d,missing:p})}for(const r of n){r.rows.sort((i,s)=>i.x-s.x||i.sourceRowIndex-s.sourceRowIndex);for(let i=1;i<r.rows.length;i+=1)if(r.rows[i-1].x===r.rows[i].x)throw new Error(`Horizon group ${JSON.stringify(r.group??null)} has duplicate x value ${r.rows[i].x}.`)}return n}function A(e,o){const n=[];let t=[];const r=()=>{t.length>0&&n.push(t),t=[]};for(const i of e){if(i.missing){r();continue}t.push({...i,signed:T(i.y-o,"Horizon signed deviation"),interpolated:!1})}return r(),n}function C(e,o){if(e.length<2)return e.map(t=>({...t}));const n=[{...e[0]}];for(let t=1;t<e.length;t+=1){const r=e[t-1],i=e[t];if(r.signed*i.signed<0){const s=I(o,r.y,i.y);n.push({sourceRowIndex:void 0,x:O(r.x,i.x,s),y:o,missing:!1,signed:0,interpolated:!0,leftSourceRowIndex:r.sourceRowIndex,rightSourceRowIndex:i.sourceRowIndex,fraction:s})}n.push({...i})}return n}function E(e){return e>0?"positive":e<0?"negative":"zero"}function $(e,o){const n=[];let t=[],r=[];const i=()=>{t.some(s=>E(s.signed)===o)&&n.push(t),t=[]};for(const s of e){const u=E(s.signed);u===o?(t.length===0&&r.length>0&&t.push(...r),t.push(s),r=[]):u==="zero"?(t.length>0&&(t.push(s),i()),r=[s]):(i(),r=[])}return i(),n}function h(e){let o=0;for(const n of e.segments??[e])for(const t of n)o=Math.max(o,Math.abs(t.signed));return o}function G(e,o){if(o.extent!=="auto")return e.map(()=>o.extent);if(o.resolve==="shared"){let n=0;for(const t of e)n=Math.max(n,h(t));return e.map(()=>n)}return e.map(h)}function K(e,o,n){let t=0;for(const[r,i]of e.entries()){const s=o[r];if(s===0)continue;const u=s/n.bands;if(!(u>0)||!Number.isFinite(u))throw new RangeError("Horizon band height is outside the positive finite numeric range.");for(const d of["negative","positive"])for(const p of i.segments)for(const l of $(p,d)){const c=h(l);t+=Math.min(n.bands,Math.ceil(Math.min(c,s)/u))*l.length,m(t,"Horizon generated row count")}}}function Z(e,o,n){if(o===0)return[];const t=o/n.bands,r=[];for(const i of["negative","positive"]){const s=[];let u=0;for(const d of e.segments)for(const p of $(d,i))s.push({points:p,segmentIndex:u}),u+=1;for(let d=0;d<n.bands;d+=1)for(const p of s){const l=p.points.map(c=>{const f=Math.abs(c.signed);if(f>o&&n.overflow==="error")throw new RangeError(`Horizon magnitude ${f} exceeds extent ${o}.`);const b=Math.min(f,o),y=Math.min(t,Math.max(0,b-d*t));return{...c,magnitude:b,rawMagnitude:f,amplitude:y,fraction:y/t,overflowed:f>o}});if(l.some(c=>c.amplitude>0)){const c=n.groupBy===void 0?null:e.group;r.push({group:e.group,groupIndex:e.groupIndex,sign:i,bandIndex:d,segmentIndex:p.segmentIndex,seriesKey:JSON.stringify([c,i,d,p.segmentIndex]),colorKey:`${i}:${d}`,bandHeight:t,points:l})}}}return r}function V(e,o){return e.flatMap(n=>n.points.map(t=>({...o.groupBy===void 0?{}:{[o.groupBy]:n.group},[o.as.x]:t.x,[o.as.lower]:0,[o.as.upper]:t.fraction,[o.as.group]:n.seriesKey,[o.as.color]:n.colorKey,[o.as.sign]:n.sign,[o.as.band]:n.bandIndex,[o.as.segment]:n.segmentIndex})))}function ee(e,o){if(!Array.isArray(e))throw new TypeError("Horizon values must be an array.");if(e.length===0)throw new Error("Horizon requires at least one source row.");const n=P(o),t=L(e,n).map(u=>({...u,segments:A(u.rows,n.baseline).map(d=>C(d,n.baseline))}));if(!t.some(u=>u.rows.some(d=>!d.missing)))throw new Error("Horizon requires at least one finite y value.");const r=G(t,n);K(t,r,n);const i=t.map((u,d)=>({...n.groupBy===void 0?{}:{group:u.group},extent:r[d],bandHeight:r[d]/n.bands})),s=t.flatMap((u,d)=>Z(u,r[d],n));return x({transform:_(n),resolved:{extents:i},groups:i,series:s,values:V(s,n)})}function ne(e){if(e?.type!=="horizon")return;const o=[...new Set((e.resolved?.extents??[]).map(i=>i.bandHeight))],n=o.length===1?o[0]:void 0,t=i=>String(Number(i.toPrecision(3))),r=[];for(const i of["negative","positive"])for(let s=0;s<e.bands;s+=1){const u=i==="negative"?e.baseline-(s+1)*n:e.baseline+s*n,d=i==="negative"?e.baseline-s*n:e.baseline+(s+1)*n;r.push({value:`${i}:${s}`,label:n===void 0?`${i==="negative"?"Below":"Above"} baseline \xB7 band ${s+1}/${e.bands}`:`${t(u)} \u2013 ${t(d)}`})}return{title:e.y.field,labelMap:r}}export{N as HORIZON_MISSING_POLICIES,S as HORIZON_OVERFLOW_POLICIES,B as HORIZON_RESOLUTIONS,ee as deriveHorizon,ne as horizonLegendPresentation,_ as requestedHorizonTransform,P as validateHorizonTransform};
@@ -251,15 +251,16 @@ export function deriveInterval(rows, transform) {
251
251
  }
252
252
  const bootstrapSampler = bootstrapMethod === undefined ? undefined : createBootstrapMeanSampler(bootstrapMethod);
253
253
 
254
+ const outputValue = bootstrapSampler === undefined ? stableDecimal : requireFiniteResult;
254
255
  const output = [];
255
256
  for (const group of groups.values()) {
256
257
  const result = deriveGroup(group.values, transform, bootstrapSampler);
257
258
  if (result === undefined) continue;
258
259
  output.push({
259
260
  ...group.fields,
260
- [transform.as.center]: stableDecimal(result.center),
261
- [transform.as.lower]: stableDecimal(result.lower),
262
- [transform.as.upper]: stableDecimal(result.upper)
261
+ [transform.as.center]: outputValue(result.center),
262
+ [transform.as.lower]: outputValue(result.lower),
263
+ [transform.as.upper]: outputValue(result.upper)
263
264
  });
264
265
  }
265
266
  if (transform.missing === undefined) return cloneAndFreeze(output);
@@ -45,8 +45,8 @@ export function normalizePointJitterPolicy(options = {}) {
45
45
  if (!isPlainObject(options)) {
46
46
  throw new TypeError("Point jitter options must be a plain object.");
47
47
  }
48
- if (!["x", "y"].includes(options.channel)) {
49
- throw new Error('Point jitter channel must be "x" or "y".');
48
+ if (!["x", "y", "both"].includes(options.channel)) {
49
+ throw new Error('Point jitter channel must be "x", "y", or "both".');
50
50
  }
51
51
  const maxOffset = options.maxOffset;
52
52
  if (!isPlainObject(maxOffset)) {
@@ -0,0 +1,45 @@
1
+ import {validateNonNegativeFinite} from "../core/validation.js";
2
+
3
+ export const LEGEND_SPACING_KEYS = Object.freeze(["rowPadding", "titlePadding"]);
4
+
5
+ export function normalizeLegendSpacing(options) {
6
+ const result = {};
7
+ for (const key of LEGEND_SPACING_KEYS) {
8
+ if (options[key] === undefined) continue;
9
+ validateNonNegativeFinite(options[key], `Legend ${key}`);
10
+ result[key] = options[key];
11
+ }
12
+ return result;
13
+ }
14
+
15
+ // Reset removes an intent; an omitted/undefined edit preserves it. Normalizers
16
+ // consume only numeric stored values, never the authoring-only reset token.
17
+ export function patchLegendSpacing(config, options) {
18
+ const result = {...config};
19
+ for (const key of LEGEND_SPACING_KEYS) {
20
+ if (options[key] === undefined) continue;
21
+ if (options[key] === "auto") delete result[key];
22
+ else {
23
+ validateNonNegativeFinite(options[key], `Legend ${key}`);
24
+ result[key] = options[key];
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+
30
+ export function prepareLegendSpacingEdit(config, options) {
31
+ const args = {...options};
32
+ for (const key of LEGEND_SPACING_KEYS) delete args[key];
33
+ return {previous:patchLegendSpacing(config, options), args};
34
+ }
35
+
36
+ export function validateLegendSpacing(kind, config) {
37
+ normalizeLegendSpacing(config);
38
+ if (LEGEND_SPACING_KEYS.some(key => config[key] !== undefined) && config.layout === "legacy-bottom") {
39
+ throw new Error('Legend rowPadding and titlePadding require layout "edge".');
40
+ }
41
+ if (config.rowPadding !== undefined && ["gradient", "strokeGradient"].includes(kind) &&
42
+ config.representation !== "symbols") {
43
+ throw new Error("Legend rowPadding requires an item-based legend.");
44
+ }
45
+ }
@@ -0,0 +1,90 @@
1
+ import { validateNonEmptyString } from "../core/validation.js";
2
+ import { normalizeTemporalValue } from "./scales/fields.js";
3
+ import { pathGroupFields, pathSeriesKey } from "./pathSeries.js";
4
+ import { stableOrderPathValues } from "./pathOrder.js";
5
+
6
+ export function validateLineMissing(value = "error") {
7
+ if (!["error", "break"].includes(value)) {
8
+ throw new Error('Line missing must be "error" or "break".');
9
+ }
10
+ return value;
11
+ }
12
+
13
+ export function validateLineMissingScope(layer) {
14
+ if (validateLineMissing(layer.mark?.missing) !== "break") return;
15
+ const encoding = layer.encoding ?? {};
16
+ if (encoding.parallel !== undefined || encoding.theta !== undefined ||
17
+ encoding.radius !== undefined || [encoding.x, encoding.y].some(position =>
18
+ position !== undefined && (position.aggregate !== undefined ||
19
+ position.bin !== undefined || !["quantitative", "temporal"].includes(position.fieldType)))) {
20
+ throw new Error("Line missing:break requires direct Cartesian quantitative/temporal positions.");
21
+ }
22
+ }
23
+
24
+ // Keep positional nulls as topology boundaries, not scale values or coerced zeros.
25
+ export function readLinePosition(rows, encoding) {
26
+ const { field, fieldType, temporalUnit } = encoding;
27
+ validateNonEmptyString(field, "Line position field");
28
+ if (!["quantitative", "temporal"].includes(fieldType)) {
29
+ throw new Error("Line missing:break requires quantitative/temporal positions.");
30
+ }
31
+ if (!rows.some(row => Object.hasOwn(row, field))) {
32
+ throw new Error(`Line position field "${field}" does not exist.`);
33
+ }
34
+ const values = rows.map((row, index) => {
35
+ const value = row[field];
36
+ if (value == null) return null;
37
+ if (fieldType === "temporal") return normalizeTemporalValue(value, field, index, temporalUnit);
38
+ if (!Number.isFinite(value)) {
39
+ throw new TypeError(`Field "${field}" must contain a finite number at row ${index}.`);
40
+ }
41
+ return value;
42
+ });
43
+ if (!values.some(value => value !== null)) {
44
+ throw new Error(`Line position field "${field}" requires at least one nonmissing value.`);
45
+ }
46
+ return values;
47
+ }
48
+
49
+ // An unknown order coordinate has no sortable position: preserve it as a boundary.
50
+ export function orderLineGapValues(values, orderValues, direction = "ascending") {
51
+ const result = [], run = [];
52
+ const flush = () => {
53
+ const ordered = stableOrderPathValues(run.map(item => item.value),
54
+ run.map(item => item.order), direction);
55
+ for (const value of ordered) result.push(value);
56
+ run.length = 0;
57
+ };
58
+ values.forEach((value, index) => {
59
+ if (orderValues[index] === null) {
60
+ flush(); result.push(value);
61
+ } else run.push({ value, order: orderValues[index] });
62
+ });
63
+ flush();
64
+ return result;
65
+ }
66
+
67
+ export function splitLineGapValues(values) {
68
+ const segments = [], run = [];
69
+ const flush = () => { if (run.length) segments.push(run.splice(0)); };
70
+ for (const value of values) {
71
+ if (value.x === null || value.y === null) flush();
72
+ else run.push(value);
73
+ }
74
+ flush();
75
+ return segments;
76
+ }
77
+
78
+ // Filtering painted runs must not delete the invisible separators and bridge them.
79
+ export function retainLineGapBoundaries(rows, members, layer) {
80
+ if (layer?.mark?.type !== "line" || layer.mark.missing !== "break") return members;
81
+ const fields = pathGroupFields(layer);
82
+ const key = row => pathSeriesKey(fields.map(field => row[field]));
83
+ const selectedSeries = new Set([...members].map(key));
84
+ const result = new Set(members);
85
+ for (const row of rows) {
86
+ if ((row[layer.encoding.x.field] == null || row[layer.encoding.y.field] == null) &&
87
+ selectedSeries.has(key(row))) result.add(row);
88
+ }
89
+ return result;
90
+ }