ggaction 0.0.15 → 0.0.17

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 (270) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +23 -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/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +40 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/boxPlots/components.js +6 -8
  13. package/src/actions/boxPlots/create.js +4 -5
  14. package/src/actions/boxPlots/edit.js +3 -4
  15. package/src/actions/boxPlots/materialize.js +1 -1
  16. package/src/actions/canvas/actions.js +3 -3
  17. package/src/actions/canvas/fitting.js +1 -1
  18. package/src/actions/categoryOrder/index.js +6 -8
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +1 -1
  21. package/src/actions/charts/beeswarm.js +1 -1
  22. package/src/actions/charts/density.js +1 -1
  23. package/src/actions/charts/ecdf.js +2 -2
  24. package/src/actions/charts/endpoints.js +4 -4
  25. package/src/actions/charts/heatmap.js +1 -1
  26. package/src/actions/charts/histogram.js +1 -1
  27. package/src/actions/charts/horizon.js +1 -1
  28. package/src/actions/charts/interval-regression.js +2 -2
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/parallel.js +1 -1
  31. package/src/actions/charts/pie.js +1 -1
  32. package/src/actions/charts/polar.js +2 -2
  33. package/src/actions/charts/radar.js +1 -1
  34. package/src/actions/charts/radial.js +2 -2
  35. package/src/actions/charts/raincloud.js +2 -2
  36. package/src/actions/charts/rug-strip.js +2 -2
  37. package/src/actions/charts/scatter.js +1 -1
  38. package/src/actions/composition/actions.js +20 -24
  39. package/src/actions/coordinates/actions.js +1 -1
  40. package/src/actions/coordinates/edit.js +1 -1
  41. package/src/actions/data/basicBin2d.js +4 -5
  42. package/src/actions/data/bin.js +3 -3
  43. package/src/actions/data/bin2d.js +6 -8
  44. package/src/actions/data/bin2dMaterialize.js +4 -5
  45. package/src/actions/data/box.js +4 -4
  46. package/src/actions/data/complete.js +2 -2
  47. package/src/actions/data/computed.js +2 -2
  48. package/src/actions/data/create.js +23 -17
  49. package/src/actions/data/density.js +4 -4
  50. package/src/actions/data/derived.js +25 -15
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +54 -29
  53. package/src/actions/data/filter.js +29 -24
  54. package/src/actions/data/fold.js +2 -2
  55. package/src/actions/data/gradientProfile.js +2 -2
  56. package/src/actions/data/horizon.js +2 -2
  57. package/src/actions/data/impute.js +2 -2
  58. package/src/actions/data/index.js +58 -54
  59. package/src/actions/data/interval.js +4 -2
  60. package/src/actions/data/intervalEdit.js +26 -18
  61. package/src/actions/data/normalize.js +2 -2
  62. package/src/actions/data/regression.js +9 -7
  63. package/src/actions/data/revise.js +80 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/shared.js +30 -2
  66. package/src/actions/data/sort.js +21 -0
  67. package/src/actions/data/stack.js +2 -2
  68. package/src/actions/data/summary.js +3 -3
  69. package/src/actions/data/timeUnit.js +2 -2
  70. package/src/actions/data/window.js +2 -2
  71. package/src/actions/encodings/angle.js +5 -2
  72. package/src/actions/encodings/appearance.js +10 -6
  73. package/src/actions/encodings/barWidth.js +1 -1
  74. package/src/actions/encodings/channels.js +2 -2
  75. package/src/actions/encodings/color/continuous.js +4 -1
  76. package/src/actions/encodings/color/index.js +5 -2
  77. package/src/actions/encodings/density.js +2 -2
  78. package/src/actions/encodings/histogram.js +1 -1
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/offset.js +6 -3
  81. package/src/actions/encodings/parallel.js +4 -5
  82. package/src/actions/encodings/pathOrder.js +6 -11
  83. package/src/actions/encodings/position/index.js +4 -4
  84. package/src/actions/encodings/position/resolve.js +6 -1
  85. package/src/actions/encodings/ranged.js +11 -6
  86. package/src/actions/encodings/remove.js +4 -5
  87. package/src/actions/encodings/ruleAppearance.js +5 -2
  88. package/src/actions/encodings/seriesLayout.js +1 -1
  89. package/src/actions/encodings/stroke.js +5 -2
  90. package/src/actions/encodings/strokeDash.js +8 -3
  91. package/src/actions/encodings/text.js +7 -2
  92. package/src/actions/errorBands/create.js +11 -17
  93. package/src/actions/errorBands/edit.js +13 -131
  94. package/src/actions/errorBars/create.js +8 -13
  95. package/src/actions/errorBars/edit.js +10 -121
  96. package/src/actions/facets/actions.js +30 -16
  97. package/src/actions/facets/derive.js +8 -2
  98. package/src/actions/facets/guides.js +4 -5
  99. package/src/actions/facets/replay.js +4 -5
  100. package/src/actions/gradientPlots/components.js +5 -6
  101. package/src/actions/gradientPlots/create.js +4 -5
  102. package/src/actions/gradientPlots/edit.js +4 -5
  103. package/src/actions/gradientPlots/materialize.js +2 -2
  104. package/src/actions/gradientPlots/rebind.js +4 -5
  105. package/src/actions/guides/axes/axes.js +23 -4
  106. package/src/actions/guides/axes/axis.js +2 -2
  107. package/src/actions/guides/axes/edit.js +2 -2
  108. package/src/actions/guides/axes/labels.js +9 -9
  109. package/src/actions/guides/axes/lines.js +9 -7
  110. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  111. package/src/actions/guides/axes/parallel.js +5 -5
  112. package/src/actions/guides/axes/remove.js +4 -4
  113. package/src/actions/guides/axes/tickGroups.js +4 -4
  114. package/src/actions/guides/axes/ticks.js +13 -4
  115. package/src/actions/guides/axes/titles.js +10 -10
  116. package/src/actions/guides/grids/grid.js +18 -29
  117. package/src/actions/guides/guides.js +1 -1
  118. package/src/actions/guides/legends/blocks.js +1 -1
  119. package/src/actions/guides/legends/categorical/actions.js +4 -4
  120. package/src/actions/guides/legends/categorical/components.js +38 -33
  121. package/src/actions/guides/legends/categorical/layout.js +1 -1
  122. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  123. package/src/actions/guides/legends/continuous/common.js +46 -8
  124. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  125. package/src/actions/guides/legends/continuous/interval.js +13 -13
  126. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  127. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  128. package/src/actions/guides/legends/edit.js +1 -1
  129. package/src/actions/guides/legends/focused.js +5 -5
  130. package/src/actions/guides/legends/lane.js +6 -6
  131. package/src/actions/guides/legends/remove.js +4 -5
  132. package/src/actions/guides/legends/size.js +21 -82
  133. package/src/actions/guides/legends/strokeWidth.js +17 -77
  134. package/src/actions/guides/polar/axes/facade.js +4 -4
  135. package/src/actions/guides/polar/axes/labels.js +5 -5
  136. package/src/actions/guides/polar/axes/lines.js +9 -15
  137. package/src/actions/guides/polar/axes/ticks.js +9 -15
  138. package/src/actions/guides/polar/axes/titles.js +5 -5
  139. package/src/actions/guides/polar/grids.js +11 -13
  140. package/src/actions/index.js +2 -0
  141. package/src/actions/intervals/revision.js +126 -0
  142. package/src/actions/marks/arc/actions.js +3 -3
  143. package/src/actions/marks/area/actions.js +4 -4
  144. package/src/actions/marks/bar/create.js +9 -2
  145. package/src/actions/marks/bar/edit.js +13 -6
  146. package/src/actions/marks/bar/materialize.js +12 -38
  147. package/src/actions/marks/line/actions.js +3 -3
  148. package/src/actions/marks/point/create.js +9 -2
  149. package/src/actions/marks/point/edit.js +13 -6
  150. package/src/actions/marks/point/jitter.js +6 -8
  151. package/src/actions/marks/point/materialize.js +20 -26
  152. package/src/actions/marks/point/packing.js +6 -8
  153. package/src/actions/marks/rect/actions.js +26 -11
  154. package/src/actions/marks/references.js +8 -10
  155. package/src/actions/marks/remove.js +5 -49
  156. package/src/actions/marks/rule/actions.js +28 -12
  157. package/src/actions/marks/text/actions.js +30 -11
  158. package/src/actions/marks/text/layout.js +12 -20
  159. package/src/actions/marks/text/removal.js +1 -1
  160. package/src/actions/marks/text/selection.js +4 -5
  161. package/src/actions/marks/tick/actions.js +26 -11
  162. package/src/actions/primitives/createGraphics.js +5 -4
  163. package/src/actions/primitives/editGraphics.js +5 -4
  164. package/src/actions/primitives/graphicProperties.js +6 -0
  165. package/src/actions/primitives/semantic.js +10 -3
  166. package/src/actions/primitives/semanticAction.js +44 -7
  167. package/src/actions/primitives/semanticValidation/dataset.js +14 -4
  168. package/src/actions/primitives/semanticValidation/layer.js +22 -2
  169. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  170. package/src/actions/regression/components.js +10 -14
  171. package/src/actions/regression/create.js +28 -8
  172. package/src/actions/regression/edit.js +50 -16
  173. package/src/actions/regression/follow.js +22 -0
  174. package/src/actions/resources/remove.js +12 -11
  175. package/src/actions/scales/channels.js +11 -11
  176. package/src/actions/scales/consumers/common.js +2 -1
  177. package/src/actions/scales/create.js +3 -2
  178. package/src/actions/scales/definitions.js +1 -1
  179. package/src/actions/scales/edit.js +2 -2
  180. package/src/actions/scales/materialize.js +1 -1
  181. package/src/actions/scales/offset.js +2 -2
  182. package/src/actions/scales/parallel.js +2 -2
  183. package/src/actions/scales/patch.js +1 -1
  184. package/src/actions/scales/preview.js +28 -1
  185. package/src/actions/scales/quantitativeColor.js +4 -5
  186. package/src/actions/selection/actions.js +25 -36
  187. package/src/actions/textMetrics/index.js +56 -0
  188. package/src/actions/theme/actions.js +2 -2
  189. package/src/actions/theme/reconcile.js +67 -121
  190. package/src/actions/titles/actions.js +8 -8
  191. package/src/actions/titles/resolve.js +3 -3
  192. package/src/actions/violinPlots/create.js +1 -1
  193. package/src/actions/violinPlots/edit.js +1 -1
  194. package/src/core/action.js +109 -36
  195. package/src/core/diagnostics.js +16 -0
  196. package/src/core/extensionRegistry.js +27 -0
  197. package/src/core/font.js +18 -0
  198. package/src/core/identifiers.js +5 -4
  199. package/src/core/immutable.js +10 -3
  200. package/src/core/optionRequirements.js +8 -0
  201. package/src/core/programState.js +6 -2
  202. package/src/core/resourceReferences.js +15 -6
  203. package/src/core/textMetricProfile.js +28 -0
  204. package/src/core/textMetrics.js +24 -3
  205. package/src/core/validation.js +22 -10
  206. package/src/diagnostics.js +1 -0
  207. package/src/grammar/bin.js +39 -10
  208. package/src/grammar/datasetSchema.js +386 -0
  209. package/src/grammar/density.js +53 -5
  210. package/src/grammar/filter.js +133 -39
  211. package/src/grammar/impute.js +3 -2
  212. package/src/grammar/interval.js +32 -1
  213. package/src/grammar/itemMissing.js +59 -0
  214. package/src/grammar/regression/derive.js +57 -16
  215. package/src/grammar/regression/index.js +1 -0
  216. package/src/grammar/regression/models.js +51 -22
  217. package/src/grammar/regression/parameters.js +56 -4
  218. package/src/grammar/scales/definition.js +7 -0
  219. package/src/grammar/scales/types.js +8 -0
  220. package/src/grammar/schemas/graphicBounds.js +11 -11
  221. package/src/grammar/schemas/semanticPath.js +3 -2
  222. package/src/grammar/sort.js +88 -0
  223. package/src/grammar/summary.js +117 -19
  224. package/src/grammar/transformTopology.js +1 -0
  225. package/src/grammar/transforms.js +21 -11
  226. package/src/inspection.js +495 -0
  227. package/src/inspectionDescriptors.js +3 -0
  228. package/src/layout/labels.js +11 -10
  229. package/src/layout/legend.js +4 -4
  230. package/src/layout/legendItems.js +6 -6
  231. package/src/layout/text.js +10 -9
  232. package/src/layout/title.js +9 -9
  233. package/src/materialization/bars/resolve.js +4 -2
  234. package/src/materialization/facetGuides/placement.js +1 -1
  235. package/src/materialization/facetGuides/preparation.js +1 -1
  236. package/src/materialization/facetHeaders.js +11 -10
  237. package/src/materialization/guides/layout.js +3 -3
  238. package/src/materialization/revisionIdentity.js +19 -0
  239. package/src/materialization/scaleGuideDependencies.js +10 -0
  240. package/src/materialization/scales/resolve.js +67 -4
  241. package/src/materialization/selection/items/common.js +1 -1
  242. package/src/materialization/text.js +6 -4
  243. package/src/materialization/typography.js +31 -0
  244. package/src/mcp/cli.js +5 -3
  245. package/src/persistence/codec.js +65 -0
  246. package/src/persistence/validation.js +185 -0
  247. package/src/persistence.js +147 -0
  248. package/src/renderers/canvas/index.js +5 -4
  249. package/src/renderers/nativeBackend.js +10 -0
  250. package/src/renderers/options.js +13 -0
  251. package/src/renderers/pdf.js +25 -54
  252. package/src/renderers/png.js +22 -17
  253. package/src/renderers/svg.js +10 -14
  254. package/src/renderers/text.js +1 -11
  255. package/src/selectors/coordinates.js +3 -1
  256. package/src/selectors/datasets.js +8 -6
  257. package/src/selectors/layers.js +6 -4
  258. package/src/selectors/markOwners.js +46 -0
  259. package/src/selectors/scales.js +6 -4
  260. package/src/theme/defaults.js +2 -1
  261. package/src/version.js +2 -0
  262. package/types/accessibility.d.ts +37 -0
  263. package/types/basic.d.ts +4 -2
  264. package/types/diagnostics.d.ts +15 -0
  265. package/types/index.d.ts +5 -0
  266. package/types/inspection.d.ts +17 -0
  267. package/types/pdf.d.ts +9 -0
  268. package/types/persistence.d.ts +7 -0
  269. package/types/png.d.ts +9 -0
  270. package/types/program.d.ts +181 -28
@@ -1,12 +1,13 @@
1
+ import { annotateError } from "./diagnostics.js";
1
2
  export const USER_ID_SOURCE = "[A-Za-z0-9_-]+";
2
3
 
3
4
  const USER_ID_PATTERN = new RegExp(`^${USER_ID_SOURCE}$`);
4
5
 
5
6
  export function validateUserId(id, label = "ID") {
6
7
  if (typeof id !== "string" || !USER_ID_PATTERN.test(id)) {
7
- throw new TypeError(
8
+ throw annotateError(new TypeError(
8
9
  `${label} must contain only letters, numbers, _ or - and must not be empty.`
9
- );
10
+ ), { code: "invalid-value", optionPath: label });
10
11
  }
11
12
 
12
13
  return id;
@@ -20,9 +21,9 @@ export function resolveOptionalUserId(id, {
20
21
  }) {
21
22
  if (id !== undefined) return validateUserId(id, label);
22
23
  if (ambiguous) {
23
- throw new Error(
24
+ throw annotateError(new Error(
24
25
  `${operation} requires an explicit ${label.toLowerCase()} because its default is ambiguous.`
25
- );
26
+ ), { code: "ambiguous-resource", operation, optionPath: label });
26
27
  }
27
28
  return validateUserId(defaultId, label);
28
29
  }
@@ -22,11 +22,18 @@ export function isOwned(value) {
22
22
  return value !== null && typeof value === "object" && ownedValues.has(value);
23
23
  }
24
24
 
25
- export function cloneAndFreeze(value, ancestors = new WeakSet()) {
25
+ export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state") {
26
+ if (typeof value === "function") {
27
+ throw new TypeError(`Cannot store a function at ${path} in a ChartProgram.`);
28
+ }
26
29
  if (value === null || typeof value !== "object") {
27
30
  return value;
28
31
  }
29
32
 
33
+ // Only library-owned values are recursively immutable; Object.freeze alone
34
+ // does not prove ownership of a caller's descendants.
35
+ if (isOwned(value)) return value;
36
+
30
37
  if (ancestors.has(value)) {
31
38
  throw new TypeError("Cannot store cyclic values in a ChartProgram.");
32
39
  }
@@ -36,12 +43,12 @@ export function cloneAndFreeze(value, ancestors = new WeakSet()) {
36
43
  let clone;
37
44
 
38
45
  if (Array.isArray(value)) {
39
- clone = value.map(item => cloneAndFreeze(item, ancestors));
46
+ clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
40
47
  } else if (isPlainObject(value)) {
41
48
  clone = Object.fromEntries(
42
49
  Object.entries(value).map(([key, item]) => [
43
50
  key,
44
- cloneAndFreeze(item, ancestors)
51
+ cloneAndFreeze(item, ancestors, `${path}.${key}`)
45
52
  ])
46
53
  );
47
54
  } else {
@@ -0,0 +1,8 @@
1
+ // The method's value-producing contract is shared by runtime validation and
2
+ // authoring knowledge; editing can inherit these values from stored state.
3
+ export const IMPUTE_REQUIRED_OPTIONS = Object.freeze({
4
+ constant: Object.freeze(["value"]),
5
+ forward: Object.freeze(["sortBy"]),
6
+ backward: Object.freeze(["sortBy"]),
7
+ linear: Object.freeze(["sortBy"])
8
+ });
@@ -23,7 +23,7 @@ export function ownChildPrograms(children, ProgramClass) {
23
23
  if (!(program instanceof ProgramClass)) {
24
24
  throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
25
25
  }
26
- owned[id] = program;
26
+ Object.defineProperty(owned, id, { value: program, enumerable: true });
27
27
  }
28
28
  return freezeOwned(owned);
29
29
  }
@@ -34,12 +34,16 @@ export function ownChildPrograms(children, ProgramClass) {
34
34
  // domain's dependency validation and before the primitive deletion.
35
35
  export function releaseNamedResourceOwnership(program, { kind, id, dataOwner }) {
36
36
  if (kind !== "data") return program;
37
- const materializationConfigs = dataOwner === undefined
37
+ let materializationConfigs = dataOwner === undefined
38
38
  ? program.materializationConfigs
39
39
  : removeMaterializationConfig(
40
40
  program.materializationConfigs,
41
41
  ["data", dataOwner.family, dataOwner.owner]
42
42
  ).value;
43
+ materializationConfigs = removeMaterializationConfig(
44
+ materializationConfigs,
45
+ ["calculations", "datasets", dataOwner?.current ?? id]
46
+ ).value;
43
47
  const context = program.context.currentData === id
44
48
  ? freezeOwned({ ...program.context, currentData: undefined })
45
49
  : program.context;
@@ -148,14 +148,19 @@ export function collectMarkConfigReferences(program) {
148
148
  "coordinate", value, "markConfig", ownerId, path
149
149
  ));
150
150
  }
151
- if ((key === "scale" || key.endsWith("Scale")) && typeof value === "string") {
151
+ const reservedScale = root === "regression" && key === "colorScale" && config.regression.groupBy === undefined;
152
+ if (!reservedScale && (key === "scale" || key.endsWith("Scale")) && typeof value === "string") {
152
153
  append(references, reference("scale", value, "markConfig", ownerId, path));
153
154
  } else if (key === "scale" && isPlainObject(value)) {
154
155
  append(references, reference(
155
156
  "scale", value.id, "markConfig", ownerId, [...path, "id"]
156
157
  ));
157
158
  }
158
- if (MARK_ID_KEYS.has(key)) {
159
+ const inactiveMark = (root === "boxPlot" && key === "outlierId" && !Object.hasOwn(program.markConfigs, value)) ||
160
+ (root === "errorBar" && ["lowerCapId", "upperCapId"].includes(key) && config.errorBar.caps === false) ||
161
+ (root === "errorBand" && ["lowerBoundaryId", "upperBoundaryId"].includes(key) &&
162
+ !Object.hasOwn(program.markConfigs, value));
163
+ if (MARK_ID_KEYS.has(key) && !inactiveMark) {
159
164
  append(references, reference("mark", value, "markConfig", ownerId, path));
160
165
  }
161
166
  if (
@@ -316,7 +321,13 @@ export function collectResourceReferences(program, { kind, id }) {
316
321
  throw new Error(`Unknown resource kind "${kind}".`);
317
322
  }
318
323
  const target = validateUserId(id, "Resource id");
319
- const combined = [
324
+ const combined = collectAllResourceReferences(program).filter(value => value.kind === kind && value.id === target);
325
+ const unique = new Map(combined.map(value => [referenceKey(value), value]));
326
+ return freezeOwned([...unique.values()].sort(compareReferences));
327
+ }
328
+
329
+ export function collectAllResourceReferences(program) {
330
+ return [
320
331
  ...collectSemanticDataReferences(program),
321
332
  ...collectSemanticScaleReferences(program),
322
333
  ...collectSemanticCoordinateReferences(program),
@@ -326,7 +337,5 @@ export function collectResourceReferences(program, { kind, id }) {
326
337
  ...collectDataOwnerReferences(program),
327
338
  ...collectCompositionReferences(program),
328
339
  ...collectContextReferences(program)
329
- ].filter(value => value.kind === kind && value.id === target);
330
- const unique = new Map(combined.map(value => [referenceKey(value), value]));
331
- return freezeOwned([...unique.values()].sort(compareReferences));
340
+ ];
332
341
  }
@@ -0,0 +1,28 @@
1
+ import { cloneAndFreeze } from "./immutable.js";
2
+ import { validateUserId } from "./identifiers.js";
3
+ import { validateOptionObject, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString } from "./validation.js";
4
+
5
+ import { textMetricKey } from "./font.js";
6
+
7
+ export function normalizeTextMetricProfile(profile) {
8
+ validateOptionObject(profile, ["schemaVersion", "id", "measurements"], "Text metrics profile");
9
+ if (profile.schemaVersion !== 1) throw new Error("Text metrics require schemaVersion 1.");
10
+ validateUserId(profile.id, "Text metrics profile id");
11
+ if (!Array.isArray(profile.measurements)) throw new TypeError("Text metrics measurements must be an array.");
12
+ const seen = new Set();
13
+ for (const measurement of profile.measurements) {
14
+ validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "width"], "Text measurement");
15
+ if (typeof measurement.text !== "string") throw new TypeError("Text measurement text must be a string.");
16
+ validateNonEmptyString(measurement.fontFamily, "Text measurement fontFamily");
17
+ validatePositiveFinite(measurement.fontSize, "Text measurement fontSize");
18
+ validateNonNegativeFinite(measurement.width, "Text measurement width");
19
+ if (!Number.isInteger(measurement.fontWeight) || measurement.fontWeight < 100 ||
20
+ measurement.fontWeight > 900 || measurement.fontWeight % 100 !== 0) {
21
+ throw new RangeError("Text measurement fontWeight must be 100–900 in steps of 100.");
22
+ }
23
+ const key = textMetricKey(measurement);
24
+ if (seen.has(key)) throw new Error("Duplicate text measurement combination.");
25
+ seen.add(key);
26
+ }
27
+ return cloneAndFreeze(profile);
28
+ }
@@ -1,3 +1,21 @@
1
+ import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
2
+
3
+ import { isOwned } from "./immutable.js";
4
+
5
+ const measurementIndexes = new WeakMap();
6
+
7
+ function measuredWidth(profile, text, style) {
8
+ if (profile === undefined) return undefined;
9
+ let index = measurementIndexes.get(profile);
10
+ if (index === undefined) {
11
+ index = new Map(profile.measurements.map(measurement =>
12
+ [textMetricKey(measurement), measurement.width]));
13
+ if (isOwned(profile)) measurementIndexes.set(profile, index);
14
+ }
15
+ return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
16
+ fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
17
+ }
18
+
1
19
  function codePointWidth(codePoint) {
2
20
  if (/\s/u.test(codePoint)) return 0.28;
3
21
  if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
@@ -25,7 +43,8 @@ export function textBoundsFitCanvas(bounds, canvas) {
25
43
 
26
44
  export function measureTextWidth(
27
45
  text,
28
- { fontSize, fontFamily, fontWeight } = {}
46
+ { fontSize, fontFamily, fontWeight } = {},
47
+ profile
29
48
  ) {
30
49
  if (typeof text !== "string") {
31
50
  throw new TypeError("Text measurement requires a string.");
@@ -33,6 +52,8 @@ export function measureTextWidth(
33
52
  if (!Number.isFinite(fontSize) || fontSize <= 0) {
34
53
  throw new RangeError("Text measurement requires a positive fontSize.");
35
54
  }
55
+ const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
56
+ if (measured !== undefined) return measured;
36
57
  let joined = false;
37
58
  let width = 0;
38
59
  for (const codePoint of text) {
@@ -76,11 +97,11 @@ export function resolveTextBounds({
76
97
  textAlign = "left",
77
98
  textBaseline = "alphabetic",
78
99
  rotation = 0
79
- } = {}) {
100
+ } = {}, profile) {
80
101
  if (![x, y, rotation].every(Number.isFinite)) {
81
102
  throw new TypeError("Text bounds require finite x, y, and rotation values.");
82
103
  }
83
- const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
104
+ const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight }, profile);
84
105
  const [left, right] = textAlign === "center"
85
106
  ? [-width / 2, width / 2]
86
107
  : ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
@@ -1,15 +1,27 @@
1
+ import { annotateError } from "./diagnostics.js";
1
2
  import { isPlainObject } from "./immutable.js";
2
3
 
3
4
  export const MAX_GENERATED_ITEMS = 10_000;
4
5
  export const MAX_WORK_ITEMS = 10_000_000;
5
6
 
7
+ export function validateDataRows(values, label = "Dataset") {
8
+ if (!Array.isArray(values)) {
9
+ throw annotateError(new TypeError(`${label} requires values to be an array.`), { code: "invalid-value", optionPath: "values" });
10
+ }
11
+ for (let index = 0; index < values.length; index += 1) {
12
+ if (!Object.hasOwn(values, index) || !isPlainObject(values[index])) {
13
+ throw annotateError(new TypeError(`${label} requires every row to be a plain object; invalid row at index ${index}.`), { code: "invalid-value", optionPath: `values[${index}]` });
14
+ }
15
+ }
16
+ }
17
+
6
18
  export function validateGeneratedItemLimit(
7
19
  value,
8
20
  label,
9
21
  maximum = MAX_GENERATED_ITEMS
10
22
  ) {
11
23
  if (value > maximum) {
12
- throw new RangeError(`${label} must not exceed ${maximum}.`);
24
+ throw annotateError(new RangeError(`${label} must not exceed ${maximum}.`), { code: "resource-limit", optionPath: label, limit: maximum, actual: value });
13
25
  }
14
26
  return value;
15
27
  }
@@ -21,7 +33,7 @@ export function validateWorkLimit(value, label) {
21
33
  export function validateKeys(value, supported, label) {
22
34
  for (const key of Object.keys(value)) {
23
35
  if (!supported.includes(key)) {
24
- throw new Error(`Unknown ${label} option "${key}".`);
36
+ throw annotateError(new Error(`Unknown ${label} option "${key}".`), { code: "invalid-option", optionPath: key });
25
37
  }
26
38
  }
27
39
  }
@@ -33,18 +45,18 @@ export function validateOptionObject(value, supported, label, {
33
45
  emptyError = TypeError
34
46
  } = {}) {
35
47
  if (!isPlainObject(value)) {
36
- throw new TypeError(plainObjectMessage);
48
+ throw annotateError(new TypeError(plainObjectMessage), { code: "invalid-option" });
37
49
  }
38
50
  if (supported !== undefined) validateKeys(value, supported, label);
39
51
  if (!allowEmpty && Object.keys(value).length === 0) {
40
- throw new emptyError(emptyMessage);
52
+ throw annotateError(new emptyError(emptyMessage), { code: "invalid-option" });
41
53
  }
42
54
  return value;
43
55
  }
44
56
 
45
57
  export function noOptions(args, operation) {
46
58
  if (!isPlainObject(args) || Object.keys(args).length > 0) {
47
- throw new Error(`${operation} does not accept options.`);
59
+ throw annotateError(new Error(`${operation} does not accept options.`), { code: "invalid-option", operation });
48
60
  }
49
61
  }
50
62
 
@@ -59,30 +71,30 @@ export function sameOrderedValues(left, right) {
59
71
 
60
72
  export function validateNonEmptyString(value, label) {
61
73
  if (typeof value !== "string" || value.length === 0) {
62
- throw new TypeError(`${label} must be a non-empty string.`);
74
+ throw annotateError(new TypeError(`${label} must be a non-empty string.`), { code: "invalid-value", optionPath: label });
63
75
  }
64
76
  return value;
65
77
  }
66
78
 
67
79
  export function validateUnitInterval(value, label) {
68
80
  if (!Number.isFinite(value) || value < 0 || value > 1) {
69
- throw new RangeError(
81
+ throw annotateError(new RangeError(
70
82
  `${label} must be between 0 and 1 (values from 0 to 1).`
71
- );
83
+ ), { code: "invalid-value", optionPath: label });
72
84
  }
73
85
  return value;
74
86
  }
75
87
 
76
88
  export function validatePositiveFinite(value, label) {
77
89
  if (!Number.isFinite(value) || value <= 0) {
78
- throw new RangeError(`${label} must be a positive finite number.`);
90
+ throw annotateError(new RangeError(`${label} must be a positive finite number.`), { code: "invalid-value", optionPath: label });
79
91
  }
80
92
  return value;
81
93
  }
82
94
 
83
95
  export function validateNonNegativeFinite(value, label) {
84
96
  if (!Number.isFinite(value) || value < 0) {
85
- throw new RangeError(`${label} must be a non-negative finite number.`);
97
+ throw annotateError(new RangeError(`${label} must be a non-negative finite number.`), { code: "invalid-value", optionPath: label });
86
98
  }
87
99
  return value;
88
100
  }
@@ -0,0 +1 @@
1
+ export { getErrorDetails } from "./core/diagnostics.js";
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  const TRANSFORM_KEYS = Object.freeze([
17
17
  "type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
18
- "members", "as", "weight", "resolved"
18
+ "members", "as", "weight", "missing", "resolved"
19
19
  ]);
20
20
  const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
21
21
  const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
@@ -59,7 +59,8 @@ export function normalizeBinTransform({
59
59
  includeEmpty,
60
60
  members,
61
61
  as,
62
- weight
62
+ weight,
63
+ missing
63
64
  } = {}) {
64
65
  const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
65
66
  if (selected.length > 1) {
@@ -83,7 +84,8 @@ export function normalizeBinTransform({
83
84
  as: normalizeAs(field, as, resolvedMembers),
84
85
  ...(weight === undefined
85
86
  ? {}
86
- : { weight: normalizeStatisticalWeight(weight, "Bin weight") })
87
+ : { weight: normalizeStatisticalWeight(weight, "Bin weight") }),
88
+ ...(missing === undefined ? {} : { missing })
87
89
  };
88
90
  validateBinTransform(transform);
89
91
  return cloneAndFreeze(transform);
@@ -101,6 +103,9 @@ export function validateBinTransform(transform) {
101
103
  if (transform.weight !== undefined) {
102
104
  normalizeStatisticalWeight(transform.weight, "Bin weight");
103
105
  }
106
+ if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
107
+ throw new Error('Bin missing must be "error" or "drop".');
108
+ }
104
109
  normalizeHistogramBin(transform.bin);
105
110
  if (
106
111
  transform.extent !== "auto" &&
@@ -142,22 +147,31 @@ export function validateBinTransform(transform) {
142
147
  return transform;
143
148
  }
144
149
 
145
- function readValues(rows, field) {
146
- return rows.map((row, index) => {
150
+ function readValues(rows, field, missing) {
151
+ const eligible = [];
152
+ let excluded = 0;
153
+ rows.forEach((row, index) => {
147
154
  const value = row[field];
155
+ if (value === null || value === undefined) {
156
+ if (missing === "drop") { excluded += 1; return; }
157
+ throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
158
+ }
148
159
  if (!Number.isFinite(value)) {
149
160
  throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
150
161
  }
151
- return value;
162
+ eligible.push({ row, index });
152
163
  });
164
+ return { eligible, excluded };
153
165
  }
154
166
 
155
167
  export function deriveBinRows(rows, transform) {
156
168
  validateBinTransform(transform);
157
- const values = readValues(rows, transform.field);
169
+ const selected = readValues(rows, transform.field, transform.missing);
170
+ const eligibleRows = selected.eligible.map(entry => entry.row);
171
+ const values = eligibleRows.map(row => row[transform.field]);
158
172
  const weights = transform.weight === undefined
159
173
  ? undefined
160
- : readStatisticalWeights(rows, transform.weight, "Bin");
174
+ : readStatisticalWeights(eligibleRows, transform.weight, "Bin");
161
175
  if (weights !== undefined) {
162
176
  validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
163
177
  }
@@ -188,7 +202,7 @@ export function deriveBinRows(rows, transform) {
188
202
  });
189
203
  const members = resolved.boundaries.slice(0, -1).map(() => []);
190
204
  const entries = weightSummary === undefined
191
- ? rows.map((row, index) => ({ row, index }))
205
+ ? selected.eligible
192
206
  : weightSummary.positive;
193
207
  entries.forEach(entry => {
194
208
  const value = entry.row[transform.field];
@@ -217,7 +231,22 @@ export function deriveBinRows(rows, transform) {
217
231
  domain: resolved.domain,
218
232
  ...(resolved.step === undefined ? {} : { step: resolved.step }),
219
233
  boundaries: resolved.boundaries
220
- }
234
+ },
235
+ ...(transform.missing === undefined ? {} : {
236
+ report: {
237
+ version: 1,
238
+ owner: { kind: "data", id: "pending" },
239
+ inputs: [],
240
+ units: [{
241
+ role: transform.as.count,
242
+ group: {},
243
+ inputRows: rows.length,
244
+ usedRows: eligibleRows.length,
245
+ excludedRows: selected.excluded,
246
+ excludedByReason: selected.excluded === 0 ? {} : { "missing-value": selected.excluded }
247
+ }]
248
+ }
249
+ })
221
250
  };
222
251
  }
223
252