ggaction 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +10 -4
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -0,0 +1,382 @@
1
+ import { requireStringValue as requireName } from "../core/validation.js";
2
+ import { annotateError } from "../core/diagnostics.js";
3
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
+
5
+ const STORAGE_TYPES = new Set([
6
+ "number", "string", "boolean", "array", "object", "unknown", "mixed"
7
+ ]);
8
+ const DECLARED_STORAGE_TYPES = new Set([
9
+ "number", "string", "boolean", "array", "object"
10
+ ]);
11
+ const FIELD_KEYS = Object.freeze([
12
+ "name", "storageType", "nullable", "optional", "lineage"
13
+ ]);
14
+ const SOURCE_FIELD_KEYS = Object.freeze([
15
+ "name", "storageType", "nullable", "optional"
16
+ ]);
17
+
18
+ function rejectUnknownKeys(value, keys, label) {
19
+ const unknown = Object.keys(value).find(key => !keys.includes(key));
20
+ if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
21
+ }
22
+
23
+
24
+ function valueStorageType(value) {
25
+ if (typeof value === "number") return "number";
26
+ if (typeof value === "string") return "string";
27
+ if (typeof value === "boolean") return "boolean";
28
+ if (Array.isArray(value)) return "array";
29
+ if (isPlainObject(value)) return "object";
30
+ return "unknown";
31
+ }
32
+
33
+ function normalizeLineage(value, label) {
34
+ if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object.`);
35
+ rejectUnknownKeys(value, ["inputs", "owner", "role"], label);
36
+ if (!Array.isArray(value.inputs)) throw new TypeError(`${label} inputs must be an array.`);
37
+ const inputs = value.inputs.map((input, index) => {
38
+ if (!isPlainObject(input)) throw new TypeError(`${label} input ${index} must be a plain object.`);
39
+ rejectUnknownKeys(input, ["data", "field"], `${label} input ${index}`);
40
+ return {
41
+ data: requireName(input.data, `${label} input ${index} data`),
42
+ field: requireName(input.field, `${label} input ${index} field`)
43
+ };
44
+ });
45
+ return {
46
+ inputs,
47
+ owner: requireName(value.owner, `${label} owner`),
48
+ role: requireName(value.role, `${label} role`)
49
+ };
50
+ }
51
+
52
+ function normalizeField(value, index, { declared = false } = {}) {
53
+ if (!isPlainObject(value)) throw new TypeError(`Dataset schema field ${index} must be a plain object.`);
54
+ rejectUnknownKeys(
55
+ value,
56
+ declared ? SOURCE_FIELD_KEYS : FIELD_KEYS,
57
+ `dataset schema field ${index}`
58
+ );
59
+ // Stored row keys are lossless; action field references still require non-empty names.
60
+ const name = value.name === "" ? "" : requireName(value.name, `Dataset schema field ${index} name`);
61
+ const allowed = declared ? DECLARED_STORAGE_TYPES : STORAGE_TYPES;
62
+ if (!allowed.has(value.storageType)) {
63
+ throw new Error(`Unsupported dataset schema storageType "${value.storageType}".`);
64
+ }
65
+ for (const key of ["nullable", "optional"]) {
66
+ if (value[key] !== undefined && typeof value[key] !== "boolean") {
67
+ throw new TypeError(`Dataset schema field ${index} ${key} must be a boolean.`);
68
+ }
69
+ }
70
+ return {
71
+ name,
72
+ storageType: value.storageType,
73
+ nullable: value.nullable ?? false,
74
+ optional: value.optional ?? false,
75
+ ...(value.lineage === undefined
76
+ ? {}
77
+ : { lineage: normalizeLineage(value.lineage, `Dataset schema field ${index} lineage`) })
78
+ };
79
+ }
80
+
81
+ export function normalizeSourceSchema(value) {
82
+ if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
83
+ rejectUnknownKeys(value, ["fields"], "dataset schema");
84
+ if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
85
+ const fields = value.fields.map((field, index) => normalizeField(field, index, { declared: true }));
86
+ if (new Set(fields.map(field => field.name)).size !== fields.length) {
87
+ throw new Error("Dataset schema field names must be unique.");
88
+ }
89
+ return cloneAndFreeze({
90
+ version: 1,
91
+ completeness: "known",
92
+ origin: "declared",
93
+ fields
94
+ });
95
+ }
96
+
97
+ export function validateDatasetSchema(value) {
98
+ if (!isPlainObject(value)) throw new TypeError("Dataset schema must be a plain object.");
99
+ rejectUnknownKeys(value, ["version", "completeness", "origin", "fields"], "dataset schema");
100
+ if (value.version !== 1) throw new Error(`Unsupported dataset schema version "${value.version}".`);
101
+ if (!["known", "unknown"].includes(value.completeness)) {
102
+ throw new Error(`Unsupported dataset schema completeness "${value.completeness}".`);
103
+ }
104
+ if (!["declared", "inferred", "derived"].includes(value.origin)) {
105
+ throw new Error(`Unsupported dataset schema origin "${value.origin}".`);
106
+ }
107
+ if (!Array.isArray(value.fields)) throw new TypeError("Dataset schema fields must be an array.");
108
+ const fields = value.fields.map((field, index) => normalizeField(field, index));
109
+ if (new Set(fields.map(field => field.name)).size !== fields.length) {
110
+ throw new Error("Dataset schema field names must be unique.");
111
+ }
112
+ if (value.completeness === "unknown" && fields.length > 0) {
113
+ throw new Error("Unknown dataset schema cannot contain known fields.");
114
+ }
115
+ return cloneAndFreeze({
116
+ version: 1,
117
+ completeness: value.completeness,
118
+ origin: value.origin,
119
+ fields
120
+ });
121
+ }
122
+
123
+ export function inferDatasetSchema(values, { origin = "inferred" } = {}) {
124
+ if (!Array.isArray(values)) throw new TypeError("Dataset schema inference requires row values.");
125
+ if (values.length === 0) {
126
+ return cloneAndFreeze({ version: 1, completeness: "unknown", origin, fields: [] });
127
+ }
128
+ const records = new Map();
129
+ for (const row of values) {
130
+ for (const name of Object.keys(row)) {
131
+ if (!records.has(name)) records.set(name, { types: new Set(), nullable: false, present: 0 });
132
+ const record = records.get(name);
133
+ const value = row[name];
134
+ if (value !== undefined) record.present += 1;
135
+ if (value === null) record.nullable = true;
136
+ else if (value !== undefined) record.types.add(valueStorageType(value));
137
+ }
138
+ }
139
+ const fields = [...records].map(([name, record]) => ({
140
+ name,
141
+ storageType: record.types.size === 0
142
+ ? "unknown"
143
+ : record.types.size === 1 ? [...record.types][0] : "mixed",
144
+ nullable: record.nullable,
145
+ optional: record.present < values.length
146
+ }));
147
+ return cloneAndFreeze({ version: 1, completeness: "known", origin, fields });
148
+ }
149
+
150
+ export function validateRowsAgainstSchema(values, schema, label = "Dataset") {
151
+ const normalized = validateDatasetSchema(schema);
152
+ if (normalized.completeness !== "known") return normalized;
153
+ const fields = new Map(normalized.fields.map(field => [field.name, field]));
154
+ for (let rowIndex = 0; rowIndex < values.length; rowIndex += 1) {
155
+ const row = values[rowIndex];
156
+ if (normalized.origin === "declared") {
157
+ const unknown = Object.keys(row).find(name => !fields.has(name));
158
+ if (unknown !== undefined) throw new Error(`${label} row ${rowIndex} contains undeclared field "${unknown}".`);
159
+ }
160
+ for (const field of normalized.fields) {
161
+ const present = Object.hasOwn(row, field.name) && row[field.name] !== undefined;
162
+ if (!present) {
163
+ if (!field.optional) throw new Error(`${label} row ${rowIndex} requires field "${field.name}".`);
164
+ continue;
165
+ }
166
+ const value = row[field.name];
167
+ if (value === null) {
168
+ if (!field.nullable) throw new TypeError(`${label} field "${field.name}" cannot be null at row ${rowIndex}.`);
169
+ continue;
170
+ }
171
+ if (!["unknown", "mixed"].includes(field.storageType) && valueStorageType(value) !== field.storageType) {
172
+ throw new TypeError(`${label} field "${field.name}" must contain ${field.storageType} values; invalid row ${rowIndex}.`);
173
+ }
174
+ }
175
+ }
176
+ return normalized;
177
+ }
178
+
179
+ export function schemaField(schema, name) {
180
+ return validateDatasetSchema(schema).fields.find(field => field.name === name);
181
+ }
182
+
183
+ export function assertFieldsAvailable(schema, fields, { data, operation } = {}) {
184
+ const normalized = validateDatasetSchema(schema);
185
+ const unique = [...new Set(fields.filter(field => typeof field === "string" && field.length > 0))];
186
+ for (const field of unique) {
187
+ if (normalized.completeness !== "known" || schemaField(normalized, field) === undefined) {
188
+ throw annotateError(new Error(
189
+ `Dataset "${data ?? "unknown"}" does not contain field "${field}".`
190
+ ), {
191
+ code: "missing-resource",
192
+ operation,
193
+ optionPath: "field",
194
+ resourceId: data
195
+ });
196
+ }
197
+ }
198
+ return normalized;
199
+ }
200
+
201
+ function expressionFields(expression, output = []) {
202
+ if (!isPlainObject(expression)) return output;
203
+ if (typeof expression.field === "string") output.push(expression.field);
204
+ for (const value of Object.values(expression)) {
205
+ if (Array.isArray(value)) value.forEach(item => expressionFields(item, output));
206
+ else if (isPlainObject(value)) expressionFields(value, output);
207
+ }
208
+ return output;
209
+ }
210
+
211
+ export function transformInputFields(transform) {
212
+ const result = [];
213
+ const add = value => {
214
+ if (typeof value === "string" && value.length > 0) result.push(value);
215
+ else if (Array.isArray(value)) value.forEach(add);
216
+ };
217
+ add(transform.field);
218
+ add(transform.x);
219
+ add(transform.x?.field);
220
+ add(transform.y);
221
+ add(transform.y?.field);
222
+ add(transform.category);
223
+ add(transform.group);
224
+ add(transform.value);
225
+ add(transform.key);
226
+ add(transform.groupBy);
227
+ add(transform.partitionBy);
228
+ add(transform.fields);
229
+ add(transform.keys);
230
+ add(transform.weight?.field ?? transform.weight);
231
+ add(transform.placement?.categoryField);
232
+ add(transform.placement?.split?.field);
233
+ add(transform.sortBy?.map(item => item.field));
234
+ if (transform.type === "window") {
235
+ const generated = new Set();
236
+ for (const item of transform.operations ?? []) {
237
+ if (typeof item.field === "string" && !generated.has(item.field)) add(item.field);
238
+ add(item.orderBy);
239
+ if (typeof item.as === "string") generated.add(item.as);
240
+ }
241
+ } else {
242
+ add(transform.operations?.flatMap(item => [item.field, item.orderBy]));
243
+ }
244
+ add(transform.aggregates?.flatMap(item => [item.field, item.op?.orderBy]));
245
+ add(expressionFields(transform.expression));
246
+ return [...new Set(result)];
247
+ }
248
+
249
+ function inheritedField(schema, name) {
250
+ const field = schema.fields.find(item => item.name === name);
251
+ return field === undefined ? undefined : { ...field };
252
+ }
253
+
254
+ function generatedField(name, storageType, transform, role, inputs = []) {
255
+ return {
256
+ name,
257
+ storageType,
258
+ nullable: !["count", "valid", "missing", "rowNumber", "rank", "denseRank"].includes(role),
259
+ optional: false,
260
+ lineage: {
261
+ inputs: inputs
262
+ .filter(field => typeof field === "string" && field.length > 0)
263
+ .map(field => ({ data: transform.__sourceId ?? "source", field })),
264
+ owner: transform.__ownerId ?? transform.type,
265
+ role
266
+ }
267
+ };
268
+ }
269
+
270
+ export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
271
+ const input = validateDatasetSchema(inputSchema);
272
+ const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
273
+ const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
274
+ let fields = retain.includes(transform.type) ? input.fields.map(field => ({ ...field })) : [];
275
+ const push = field => {
276
+ if (field === undefined) return;
277
+ fields = fields.filter(item => item.name !== field.name);
278
+ fields.push(field);
279
+ };
280
+ if (transform.type === "summary") {
281
+ fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
282
+ for (const item of transform.aggregates) {
283
+ const op = typeof item.op === "string" ? item.op : item.op.op;
284
+ push(generatedField(item.as, "number", definition, op, item.field ? [item.field] : []));
285
+ }
286
+ if (transform.members !== undefined) push(generatedField(transform.members, "array", definition, "members"));
287
+ } else if (transform.type === "bin") {
288
+ fields = [
289
+ generatedField(transform.as.lower, "number", definition, "lower", [transform.field]),
290
+ generatedField(transform.as.upper, "number", definition, "upper", [transform.field]),
291
+ generatedField(transform.as.count, "number", definition, "count", [transform.field])
292
+ ];
293
+ if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
294
+ } else if (transform.type === "bin2d") {
295
+ fields = [
296
+ generatedField(transform.as.x0, "number", definition, "x0", [transform.x]),
297
+ generatedField(transform.as.x1, "number", definition, "x1", [transform.x]),
298
+ generatedField(transform.as.y0, "number", definition, "y0", [transform.y]),
299
+ generatedField(transform.as.y1, "number", definition, "y1", [transform.y]),
300
+ generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
301
+ ];
302
+ if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
303
+ } else if (transform.type === "regression") {
304
+ fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
305
+ if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
306
+ if (transform.interval !== false && transform.method !== "loess") {
307
+ push(generatedField("__regression_ci_lower", "number", definition, "lower", [transform.x, transform.y]));
308
+ push(generatedField("__regression_ci_upper", "number", definition, "upper", [transform.x, transform.y]));
309
+ }
310
+ } else if (transform.type === "density") {
311
+ fields = [
312
+ ...(transform.groupBy === undefined ? [] : [inheritedField(input, transform.groupBy)]),
313
+ ...(transform.placement?.split === undefined ? [] : [inheritedField(input, transform.placement.split.field)]),
314
+ generatedField(transform.as[0], "number", definition, "value", [transform.field]),
315
+ generatedField(transform.as[1], "number", definition, "density", [transform.field])
316
+ ].filter(Boolean);
317
+ if (transform.groupBy === undefined && transform.placement !== undefined) {
318
+ push(generatedField(transform.placement.categoryField, "string", definition, "category"));
319
+ }
320
+ } else if (transform.type === "interval") {
321
+ fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
322
+ push(generatedField(transform.as.center, "number", definition, "center", [transform.field]));
323
+ push(generatedField(transform.as.lower, "number", definition, "lower", [transform.field]));
324
+ push(generatedField(transform.as.upper, "number", definition, "upper", [transform.field]));
325
+ } else if (transform.type === "ecdf") {
326
+ fields = transform.groupBy.map(name => inheritedField(input, name)).filter(Boolean);
327
+ push(generatedField(transform.as.value, "number", definition, "value", [transform.field]));
328
+ push(generatedField(transform.as.cumulative, "number", definition, "cumulative", [transform.field]));
329
+ push(generatedField(transform.as.probability, "number", definition, "probability", [transform.field]));
330
+ } else if (transform.type === "computed") {
331
+ push(generatedField(transform.as, "unknown", definition, "value", expressionFields(transform.expression)));
332
+ } else if (transform.type === "timeUnit") {
333
+ push(generatedField(transform.as, "number", definition, "value", [transform.field]));
334
+ } else if (transform.type === "window") {
335
+ for (const item of transform.operations) {
336
+ push(generatedField(
337
+ item.as,
338
+ ["lag", "lead"].includes(item.op) ? "unknown" : "number",
339
+ definition,
340
+ item.op,
341
+ item.field ? [item.field] : []
342
+ ));
343
+ }
344
+ } else if (transform.type === "normalize") {
345
+ push(generatedField(transform.as, "number", definition, "value", [transform.field]));
346
+ } else if (transform.type === "stack") {
347
+ for (const [role, name] of Object.entries(transform.as ?? {})) {
348
+ push(generatedField(name, "number", definition, role, [transform.field]));
349
+ }
350
+ } else if (transform.type === "fold") {
351
+ fields = input.fields.filter(field => !transform.fields.includes(field.name)).map(field => ({ ...field }));
352
+ push(generatedField(transform.as.key, "string", definition, "key", transform.fields));
353
+ push(generatedField(transform.as.value, "unknown", definition, "value", transform.fields));
354
+ }
355
+ const inferred = Array.isArray(values) && values.length > 0
356
+ ? inferDatasetSchema(values, { origin: "derived" })
357
+ : undefined;
358
+ if (inferred !== undefined) {
359
+ const actual = new Map(inferred.fields.map(field => [field.name, field]));
360
+ fields = fields.map(planned => {
361
+ const field = actual.get(planned.name);
362
+ if (field === undefined) {
363
+ return ["complete", "impute"].includes(transform.type)
364
+ ? { ...planned, optional: true }
365
+ : planned;
366
+ }
367
+ if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
368
+ return planned;
369
+ }
370
+ return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
371
+ });
372
+ for (const field of inferred.fields) {
373
+ if (!fields.some(planned => planned.name === field.name)) fields.push(field);
374
+ }
375
+ }
376
+ return cloneAndFreeze({
377
+ version: 1,
378
+ completeness: input.completeness === "known" || fields.length > 0 ? "known" : "unknown",
379
+ origin: "derived",
380
+ fields: input.completeness === "known" || fields.length > 0 ? fields : []
381
+ });
382
+ }
@@ -59,6 +59,7 @@ export function normalizeDensityTransform(args = {}) {
59
59
  as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
60
60
  resolve: args.resolve ?? "shared",
61
61
  ...(args.weight === undefined ? {} : { weight: args.weight }),
62
+ ...(args.missing === undefined ? {} : { missing: args.missing }),
62
63
  ...(args.placement === undefined ? {} : { placement: args.placement })
63
64
  };
64
65
  validateDensityTransform(transform);
@@ -370,7 +371,7 @@ function hasValidResolvedBandwidthState(resolved, transform) {
370
371
  export function validateDensityTransform(transform) {
371
372
  const supported = [
372
373
  "type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
373
- "resolve", "kernel", "normalization", "placement", "weight", "resolved"
374
+ "resolve", "kernel", "normalization", "placement", "weight", "missing", "resolved"
374
375
  ];
375
376
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
376
377
  if (unknown !== undefined) {
@@ -383,6 +384,9 @@ export function validateDensityTransform(transform) {
383
384
  if (transform.weight !== undefined) {
384
385
  normalizeStatisticalWeight(transform.weight, "Density weight");
385
386
  }
387
+ if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
388
+ throw new Error('Density missing must be "error" or "drop".');
389
+ }
386
390
  if (transform.groupBy !== undefined) {
387
391
  requireField(transform.groupBy, "Density groupBy");
388
392
  }
@@ -540,7 +544,8 @@ export function deriveKernelDensity(values, {
540
544
  normalization = "unit",
541
545
  as,
542
546
  placement,
543
- weight
547
+ weight,
548
+ missing
544
549
  } = {}) {
545
550
  if (!Array.isArray(values)) {
546
551
  throw new TypeError("Density values must be an array.");
@@ -561,9 +566,31 @@ export function deriveKernelDensity(values, {
561
566
  )];
562
567
  const resolvedKernel = validateDensityKernel(kernel);
563
568
  const resolvedNormalization = validateDensityNormalization(normalization);
569
+ let missingValueRows = 0;
570
+ let missingWeightRows = 0;
571
+ const inputValues = missing === undefined ? values : values.filter((row, index) => {
572
+ const source = row?.[sourceField];
573
+ if (source === null || source === undefined) {
574
+ if (missing === "error") throw new TypeError(`Density field "${sourceField}" is missing at row ${index}.`);
575
+ missingValueRows += 1;
576
+ return false;
577
+ }
578
+ if (!Number.isFinite(source)) {
579
+ throw new TypeError(`Density field "${sourceField}" must contain a finite number at row ${index}.`);
580
+ }
581
+ if (weight !== undefined) {
582
+ const weightValue = row?.[weight.field];
583
+ if (weightValue === null || weightValue === undefined) {
584
+ if (missing === "error") throw new TypeError(`Density weight "${weight.field}" is missing at row ${index}.`);
585
+ missingWeightRows += 1;
586
+ return false;
587
+ }
588
+ }
589
+ return true;
590
+ });
564
591
  const statisticalWeights = weight === undefined
565
592
  ? undefined
566
- : readStatisticalWeights(values, weight, "Density");
593
+ : readStatisticalWeights(inputValues, weight, "Density");
567
594
  if (statisticalWeights !== undefined) {
568
595
  validateWeightedNumericFields(
569
596
  statisticalWeights.entries,
@@ -579,7 +606,7 @@ export function deriveKernelDensity(values, {
579
606
  isNominalValue(entry.row[placement.split.field]))
580
607
  );
581
608
  const validRows = statisticalWeights === undefined
582
- ? values.filter(row =>
609
+ ? inputValues.filter(row =>
583
610
  row !== null &&
584
611
  typeof row === "object" &&
585
612
  Number.isFinite(row[sourceField]) &&
@@ -759,6 +786,27 @@ export function deriveKernelDensity(values, {
759
786
  extent: resolvedExtent,
760
787
  steps,
761
788
  samples,
762
- values: rows
789
+ values: rows,
790
+ ...(missing === undefined ? {} : {
791
+ report: {
792
+ version: 1,
793
+ owner: { kind: "data", id: "pending" },
794
+ inputs: [],
795
+ units: [{
796
+ role: outputFields[1],
797
+ group: {},
798
+ inputRows: values.length,
799
+ usedRows: validRows.length,
800
+ excludedRows: values.length - validRows.length,
801
+ excludedByReason: {
802
+ ...(missingValueRows === 0 ? {} : { "missing-value": missingValueRows }),
803
+ ...(missingWeightRows === 0 ? {} : { "missing-weight": missingWeightRows })
804
+ },
805
+ ...(statisticalWeights === undefined ? {} : {
806
+ zeroWeightRows: statisticalWeights.entries.filter(entry => entry.weight === 0).length
807
+ })
808
+ }]
809
+ }
810
+ })
763
811
  });
764
812
  }
@@ -1,4 +1,5 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
+ import { validateNonEmptyString } from "../core/validation.js";
2
3
 
3
4
  const ENTRY_KEYS = Object.freeze(["value", "label"]);
4
5
 
@@ -55,3 +56,30 @@ export function resolveDisplayLabel(value, map, fallback) {
55
56
  }
56
57
  return resolved;
57
58
  }
59
+
60
+
61
+ export function normalizeDisplayColorMap(value, label = "colorMap") {
62
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
63
+ const normalized = [];
64
+ for (const [index, entry] of value.entries()) {
65
+ if (!isPlainObject(entry)) throw new TypeError(`${label}[${index}] must be a plain object.`);
66
+ if (Reflect.ownKeys(entry).length !== 2 ||
67
+ !Object.hasOwn(entry, "value") || !Object.hasOwn(entry, "color")) {
68
+ throw new Error(`${label}[${index}] must contain exactly value and color.`);
69
+ }
70
+ if (!isDisplayLabelValue(entry.value)) {
71
+ throw new TypeError(`${label}[${index}].value must be a string, boolean, null, or finite number.`);
72
+ }
73
+ validateNonEmptyString(entry.color, `${label}[${index}].color`);
74
+ if (normalized.some(previous => sameDisplayLabelValue(previous.value, entry.value))) {
75
+ throw new Error(`${label} contains a duplicate typed value.`);
76
+ }
77
+ normalized.push(Object.freeze({ value: entry.value, color: entry.color }));
78
+ }
79
+ return Object.freeze(normalized);
80
+ }
81
+
82
+ export function resolveDisplayLabelColor(value, map, fallback) {
83
+ const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
84
+ return entry?.color ?? fallback;
85
+ }
@@ -1,3 +1,5 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
2
+ import { isGroupValue as groupValue } from "./transformKeys.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { stableDecimal } from "./numeric.js";
3
5
 
@@ -6,11 +8,6 @@ const TRANSFORM_KEYS = Object.freeze([
6
8
  ]);
7
9
  const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
8
10
 
9
- function nonEmptyString(value, label) {
10
- if (typeof value !== "string" || value.length === 0) {
11
- throw new TypeError(`${label} must be a non-empty string.`);
12
- }
13
- }
14
11
 
15
12
  function normalizeGroupBy(groupBy) {
16
13
  const fields = groupBy === undefined
@@ -101,10 +98,6 @@ export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop
101
98
  return cloneAndFreeze(transform);
102
99
  }
103
100
 
104
- function groupValue(value) {
105
- return typeof value === "string" || typeof value === "boolean" ||
106
- (typeof value === "number" && Number.isFinite(value));
107
- }
108
101
 
109
102
  function rejectOrDrop(transform, index, reason) {
110
103
  if (transform.missing === "error") {
@@ -6,12 +6,13 @@ import { resolveRectMode } from "../rects.js";
6
6
  import { resolveRuleMode } from "../rules.js";
7
7
 
8
8
  const CARTESIAN_MARKS = new Set([
9
- "point", "line", "area", "bar", "rule", "tick", "rect"
9
+ "point", "line", "area", "bar", "rule", "tick", "rect", "text"
10
10
  ]);
11
11
  const FACET_BAR_GRAINS = new Set([
12
12
  BAR_GRAINS.histogram,
13
13
  BAR_GRAINS.aggregate,
14
- BAR_GRAINS.ranged
14
+ BAR_GRAINS.ranged,
15
+ BAR_GRAINS.centered
15
16
  ]);
16
17
  const ORDINARY_SCALE_CHANNELS = Object.freeze([
17
18
  ["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
@@ -66,6 +67,9 @@ function classifyPrimaryLayer(semanticSpec, layer) {
66
67
  `facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
67
68
  );
68
69
  }
70
+ if (type === "text" && layer.encoding?.text === undefined) {
71
+ throw new Error(`Facet text layer "${layer.id}" requires a text encoding.`);
72
+ }
69
73
  const complete = (
70
74
  layer.encoding?.x?.scale !== undefined &&
71
75
  layer.encoding?.y?.scale !== undefined
@@ -165,6 +165,11 @@ function scaleIds(config) {
165
165
  export function resolveSharedFacetLegends(children) {
166
166
  const normalized = requireLegendChildren(children);
167
167
  const first = normalized[0];
168
+ for (const child of normalized.slice(1)) {
169
+ if (!sameValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
170
+ throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
171
+ }
172
+ }
168
173
  const kinds = Object.keys(first.legends);
169
174
  const entries = kinds.map(kind => {
170
175
  const family = LEGEND_FAMILIES[kind];
@@ -6,7 +6,6 @@ import {
6
6
  } from "../../core/validation.js";
7
7
  import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
8
8
  import { readNominalField } from "../scales/index.js";
9
- const MAX_FACET_CHILDREN = 100;
10
9
 
11
10
  function requireFacetField(field, label = "facet") {
12
11
  if (typeof field !== "string" || field.length === 0) {
@@ -100,8 +99,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
100
99
  const values = resolveValues(observed, options.values);
101
100
  validateGeneratedItemLimit(
102
101
  values.length,
103
- "Facet child count",
104
- MAX_FACET_CHILDREN
102
+ "Facet child count"
105
103
  );
106
104
  validateWorkLimit(
107
105
  dataset.values.length * values.length,
@@ -175,8 +173,7 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
175
173
  }
176
174
  validateGeneratedItemLimit(
177
175
  selected.length,
178
- "Facet grid child count",
179
- MAX_FACET_CHILDREN
176
+ "Facet grid child count"
180
177
  );
181
178
  validateWorkLimit(
182
179
  dataset.values.length * selected.length,