ggaction 0.0.16 → 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 (83) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -1
  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 +8 -2
  7. package/src/actions/data/bin.js +1 -1
  8. package/src/actions/data/create.js +18 -5
  9. package/src/actions/data/density.js +1 -1
  10. package/src/actions/data/derived.js +15 -1
  11. package/src/actions/data/edit.js +19 -6
  12. package/src/actions/data/filter.js +15 -4
  13. package/src/actions/data/index.js +3 -0
  14. package/src/actions/data/interval.js +2 -0
  15. package/src/actions/data/regression.js +4 -1
  16. package/src/actions/data/revise.js +37 -5
  17. package/src/actions/data/shared.js +30 -2
  18. package/src/actions/data/sort.js +21 -0
  19. package/src/actions/data/summary.js +1 -1
  20. package/src/actions/encodings/angle.js +4 -1
  21. package/src/actions/encodings/appearance.js +6 -2
  22. package/src/actions/encodings/color/continuous.js +4 -1
  23. package/src/actions/encodings/color/index.js +4 -1
  24. package/src/actions/encodings/offset.js +4 -1
  25. package/src/actions/encodings/position/resolve.js +6 -1
  26. package/src/actions/encodings/ranged.js +6 -1
  27. package/src/actions/encodings/ruleAppearance.js +4 -1
  28. package/src/actions/encodings/stroke.js +4 -1
  29. package/src/actions/encodings/strokeDash.js +6 -1
  30. package/src/actions/encodings/text.js +6 -1
  31. package/src/actions/marks/bar/create.js +8 -1
  32. package/src/actions/marks/bar/edit.js +12 -5
  33. package/src/actions/marks/point/create.js +8 -1
  34. package/src/actions/marks/point/edit.js +12 -5
  35. package/src/actions/marks/point/materialize.js +4 -2
  36. package/src/actions/marks/rect/actions.js +23 -8
  37. package/src/actions/marks/rule/actions.js +24 -8
  38. package/src/actions/marks/text/actions.js +25 -6
  39. package/src/actions/marks/tick/actions.js +23 -8
  40. package/src/actions/primitives/semanticAction.js +37 -3
  41. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  42. package/src/actions/primitives/semanticValidation/layer.js +22 -2
  43. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  44. package/src/actions/regression/create.js +25 -4
  45. package/src/actions/regression/edit.js +47 -12
  46. package/src/actions/regression/follow.js +22 -0
  47. package/src/actions/scales/channels.js +11 -11
  48. package/src/actions/scales/consumers/common.js +2 -1
  49. package/src/actions/scales/create.js +2 -1
  50. package/src/actions/scales/definitions.js +1 -1
  51. package/src/actions/scales/edit.js +1 -1
  52. package/src/actions/scales/parallel.js +1 -1
  53. package/src/actions/scales/patch.js +1 -1
  54. package/src/actions/scales/preview.js +28 -1
  55. package/src/actions/theme/reconcile.js +2 -0
  56. package/src/core/action.js +3 -2
  57. package/src/core/programState.js +5 -1
  58. package/src/grammar/bin.js +39 -10
  59. package/src/grammar/datasetSchema.js +386 -0
  60. package/src/grammar/density.js +53 -5
  61. package/src/grammar/filter.js +133 -39
  62. package/src/grammar/interval.js +32 -1
  63. package/src/grammar/itemMissing.js +59 -0
  64. package/src/grammar/regression/derive.js +57 -16
  65. package/src/grammar/regression/index.js +1 -0
  66. package/src/grammar/regression/models.js +51 -22
  67. package/src/grammar/regression/parameters.js +56 -4
  68. package/src/grammar/scales/definition.js +7 -0
  69. package/src/grammar/schemas/semanticPath.js +3 -2
  70. package/src/grammar/sort.js +88 -0
  71. package/src/grammar/summary.js +117 -19
  72. package/src/grammar/transformTopology.js +1 -0
  73. package/src/grammar/transforms.js +21 -11
  74. package/src/inspection.js +495 -0
  75. package/src/inspectionDescriptors.js +3 -0
  76. package/src/materialization/bars/resolve.js +4 -2
  77. package/src/materialization/revisionIdentity.js +19 -0
  78. package/src/materialization/scales/resolve.js +67 -4
  79. package/src/materialization/text.js +4 -2
  80. package/src/persistence.js +79 -6
  81. package/src/version.js +1 -1
  82. package/types/inspection.d.ts +17 -0
  83. package/types/program.d.ts +147 -27
@@ -7,7 +7,7 @@ import {
7
7
  validateNonNegativeFinite,
8
8
  validateUnitInterval
9
9
  } from "../../core/validation.js";
10
- import { normalizeRegressionParameters } from "../../grammar/regression/index.js";
10
+ import { normalizeRegressionParameters, normalizeRegressionPredict } from "../../grammar/regression/index.js";
11
11
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
12
12
  import {
13
13
  requestedStrokeDetails,
@@ -23,6 +23,7 @@ import { requireRegressionField } from "./resolve.js";
23
23
  const OPTIONS = Object.freeze([
24
24
  "target", "data", "x", "y", "groupBy", "method", "degree", "span",
25
25
  "confidenceMethod", "level", "confidence", "interval", "band", "line"
26
+ , "predict", "sourceBinding", "missing"
26
27
  ]);
27
28
  const BAND_OPTIONS = Object.freeze([
28
29
  "color", "opacity", "stroke", "strokeWidth", "curve",
@@ -115,7 +116,7 @@ function resolveParameters(previous, args) {
115
116
  if (Object.hasOwn(args, key)) raw[key] = args[key];
116
117
  }
117
118
  const inheritsLevel = !Object.hasOwn(raw, "confidence");
118
- if (method === previous.method) {
119
+ if (method === previous.method && args.interval !== false) {
119
120
  for (const key of [
120
121
  "degree", "span", "confidenceMethod", "level", "interval"
121
122
  ]) {
@@ -127,7 +128,7 @@ function resolveParameters(previous, args) {
127
128
  raw[key] = previous[key];
128
129
  }
129
130
  }
130
- } else if (method !== "loess" && previous.method !== "loess") {
131
+ } else if (method !== "loess" && previous.method !== "loess" && args.interval !== false) {
131
132
  for (const key of ["confidenceMethod", "level", "interval"]) {
132
133
  if (
133
134
  !Object.hasOwn(raw, key) &&
@@ -138,7 +139,19 @@ function resolveParameters(previous, args) {
138
139
  }
139
140
  }
140
141
  }
141
- return normalizeRegressionParameters(raw);
142
+ const parameters = normalizeRegressionParameters(raw);
143
+ const predict = Object.hasOwn(args, "predict")
144
+ ? args.predict === false ? undefined : normalizeRegressionPredict(args.predict)
145
+ : previous.predict;
146
+ const missing = Object.hasOwn(args, "missing") ? args.missing : previous.missing;
147
+ if (missing !== undefined && !["error", "drop"].includes(missing)) {
148
+ throw new Error('Regression missing must be "error" or "drop".');
149
+ }
150
+ return {
151
+ ...parameters,
152
+ ...(predict === undefined ? {} : { predict }),
153
+ ...(missing === undefined ? {} : { missing })
154
+ };
142
155
  }
143
156
 
144
157
  function regressionColorScale(owner, current, groupBy) {
@@ -245,15 +258,31 @@ export const editRegression = /* @__PURE__ */ closedAction(
245
258
  }
246
259
  const owner = resolveRegressionOwner(this, args.target);
247
260
  const current = this.markConfigs[owner.id].regression;
248
- const source = Object.hasOwn(args, "data")
261
+ const currentBinding = owner.derivedBindings?.regression?.mode === "follow" ? "follow" : "fixed";
262
+ const sourceBinding = args.sourceBinding ?? currentBinding;
263
+ if (!["fixed", "follow"].includes(sourceBinding)) throw new Error('Regression sourceBinding must be "fixed" or "follow".');
264
+ const followed = sourceBinding === "follow" ? {
265
+ data: owner.data,
266
+ x: owner.encoding?.x?.fieldType === "quantitative" ? owner.encoding.x.field : undefined,
267
+ y: owner.encoding?.y?.fieldType === "quantitative" ? owner.encoding.y.field : undefined
268
+ } : undefined;
269
+ if (sourceBinding === "follow" && (!followed.data || !followed.x || !followed.y)) {
270
+ throw new Error(`Following regression source "${owner.id}" requires quantitative x/y fields and data.`);
271
+ }
272
+ for (const key of ["data", "x", "y"]) {
273
+ if (followed !== undefined && Object.hasOwn(args, key) && args[key] !== followed[key]) {
274
+ throw new Error(`Following regression ${key} must match source mark "${owner.id}".`);
275
+ }
276
+ }
277
+ const source = followed?.data ?? (Object.hasOwn(args, "data")
249
278
  ? validateUserId(args.data, "Regression data id")
250
- : current.source;
251
- const x = Object.hasOwn(args, "x")
279
+ : current.source);
280
+ const x = followed?.x ?? (Object.hasOwn(args, "x")
252
281
  ? requireRegressionField(args.x, "Regression x")
253
- : current.x;
254
- const y = Object.hasOwn(args, "y")
282
+ : current.x);
283
+ const y = followed?.y ?? (Object.hasOwn(args, "y")
255
284
  ? requireRegressionField(args.y, "Regression y")
256
- : current.y;
285
+ : current.y);
257
286
  const groupBy = Object.hasOwn(args, "groupBy")
258
287
  ? args.groupBy === false
259
288
  ? undefined
@@ -272,12 +301,12 @@ export const editRegression = /* @__PURE__ */ closedAction(
272
301
  const linePatch = Object.hasOwn(args, "line")
273
302
  ? validateLinePatch(args.line)
274
303
  : undefined;
275
- if (parameters.method === "loess" && bandPatch !== undefined && bandPatch !== false) {
304
+ if ((parameters.method === "loess" || parameters.interval === false) && bandPatch !== undefined && bandPatch !== false) {
276
305
  throw new Error("LOESS regression does not support a band object.");
277
306
  }
278
307
 
279
308
  const hadBand = current.bandId !== undefined;
280
- const wantsBand = parameters.method !== "loess" &&
309
+ const wantsBand = parameters.method !== "loess" && parameters.interval !== false &&
281
310
  (bandPatch === false
282
311
  ? false
283
312
  : hadBand || bandPatch !== undefined || current.parameters.method === "loess");
@@ -385,6 +414,12 @@ export const editRegression = /* @__PURE__ */ closedAction(
385
414
  if (changesStatistics) {
386
415
  next = next.releaseDerivedData(revision.release);
387
416
  }
417
+ const binding = findLayer(next, owner.id)?.derivedBindings?.regression;
418
+ if (sourceBinding === "follow" && binding?.mode !== "follow") {
419
+ next = next.editSemantic({ property: `layer[${owner.id}].derivedBindings.regression`, value: { mode: "follow", roles: ["data", "x", "y"] } });
420
+ } else if (sourceBinding === "fixed" && binding !== undefined) {
421
+ next = next.editSemantic({ property: `layer[${owner.id}].derivedBindings.regression`, remove: true });
422
+ }
388
423
  return next;
389
424
  };
390
425
  return applyEdit(this);
@@ -0,0 +1,22 @@
1
+ export function reconcileFollowingRegressions(program) {
2
+ let next = program;
3
+ for (const layer of program.semanticSpec.layers) {
4
+ if (layer.derivedBindings?.regression?.mode !== "follow") continue;
5
+ const config = next.markConfigs[layer.id]?.regression;
6
+ if (config === undefined) continue;
7
+ const x = layer.encoding?.x;
8
+ const y = layer.encoding?.y;
9
+ if (typeof layer.data !== "string" || x?.fieldType !== "quantitative" || y?.fieldType !== "quantitative" ||
10
+ typeof x.field !== "string" || typeof y.field !== "string") {
11
+ throw new Error(`Following regression source "${layer.id}" requires quantitative x/y fields and data.`);
12
+ }
13
+ if (config.source === layer.data && config.x === x.field && config.y === y.field) continue;
14
+ next = next.editRegression({
15
+ target: layer.id,
16
+ data: layer.data,
17
+ x: x.field,
18
+ y: y.field
19
+ });
20
+ }
21
+ return next;
22
+ }
@@ -10,7 +10,7 @@ const DEFINITIONS = Object.freeze({
10
10
  editXScale: Object.freeze({
11
11
  channel: "x",
12
12
  options: Object.freeze([
13
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
13
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
14
14
  "base", "exponent", "constant", "paddingInner", "paddingOuter",
15
15
  "padding", "align", "unknown"
16
16
  ])
@@ -18,7 +18,7 @@ const DEFINITIONS = Object.freeze({
18
18
  editYScale: Object.freeze({
19
19
  channel: "y",
20
20
  options: Object.freeze([
21
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
21
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
22
22
  "base", "exponent", "constant", "paddingInner", "paddingOuter",
23
23
  "padding", "align", "unknown"
24
24
  ])
@@ -26,21 +26,21 @@ const DEFINITIONS = Object.freeze({
26
26
  editThetaScale: Object.freeze({
27
27
  channel: "theta",
28
28
  options: Object.freeze([
29
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
29
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
30
30
  "paddingInner", "paddingOuter", "padding", "align"
31
31
  ])
32
32
  }),
33
33
  editRScale: Object.freeze({
34
34
  channel: "radius",
35
35
  options: Object.freeze([
36
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
36
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
37
37
  "base", "exponent", "constant", "radialMapping"
38
38
  ])
39
39
  }),
40
40
  editColorScale: Object.freeze({
41
41
  channel: "color",
42
42
  options: Object.freeze([
43
- "type", "domain", "range", "clamp", "reverse", "palette",
43
+ "type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
44
44
  "interpolate", "midpoint", "unknown"
45
45
  ])
46
46
  }),
@@ -48,37 +48,37 @@ const DEFINITIONS = Object.freeze({
48
48
  channel: "stroke",
49
49
  targetOnly: true,
50
50
  options: Object.freeze([
51
- "type", "domain", "range", "clamp", "reverse", "palette",
51
+ "type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
52
52
  "interpolate", "midpoint", "unknown"
53
53
  ])
54
54
  }),
55
55
  editSizeScale: Object.freeze({
56
56
  channel: "size",
57
57
  options: Object.freeze([
58
- "type", "domain", "range", "unknown", "clamp", "reverse", "base",
58
+ "type", "domain", "range", "emptyDomain", "unknown", "clamp", "reverse", "base",
59
59
  "exponent"
60
60
  ])
61
61
  }),
62
62
  editOpacityScale: Object.freeze({
63
63
  channel: "opacity",
64
64
  options: Object.freeze([
65
- "type", "domain", "range", "nice", "zero", "clamp", "reverse", "unknown"
65
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse", "unknown"
66
66
  ])
67
67
  }),
68
68
  editShapeScale: Object.freeze({
69
69
  channel: "shape",
70
- options: Object.freeze(["type", "domain", "range", "unknown"])
70
+ options: Object.freeze(["type", "domain", "range", "emptyDomain", "unknown"])
71
71
  }),
72
72
  editStrokeWidthScale: Object.freeze({
73
73
  channel: "strokeWidth",
74
74
  options: Object.freeze([
75
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
75
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
76
76
  "base", "exponent", "constant"
77
77
  ])
78
78
  }),
79
79
  editStrokeDashScale: Object.freeze({
80
80
  channel: "strokeDash",
81
- options: Object.freeze(["type", "domain", "range"])
81
+ options: Object.freeze(["type", "domain", "range", "emptyDomain"])
82
82
  })
83
83
  });
84
84
 
@@ -5,6 +5,7 @@ import { readScaleField } from "../../../grammar/scales/index.js";
5
5
  import { findDataset } from "../../../selectors/datasets.js";
6
6
  import { requireSemanticScale } from "../../../selectors/scales.js";
7
7
  import { SCALED_ENCODING_CHANNELS } from "../../../core/vocabulary.js";
8
+ import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
8
9
 
9
10
  export function findScale(program, id) {
10
11
  return requireSemanticScale(program, id);
@@ -39,7 +40,7 @@ export function requireConsumerDataset(program, consumer) {
39
40
  `Mark "${consumer.layer.id}" references unknown dataset "${consumer.layer.data}".`
40
41
  );
41
42
  }
42
- return dataset;
43
+ return applyItemMissingPolicy(consumer.layer, dataset);
43
44
  }
44
45
 
45
46
  export function isDirectCategoricalConsumer(consumer) {
@@ -24,6 +24,7 @@ const CREATE_SCALE_OPTIONS = Object.freeze([
24
24
  "id",
25
25
  "type",
26
26
  "domain",
27
+ "emptyDomain",
27
28
  "range",
28
29
  "nice",
29
30
  "zero",
@@ -164,7 +165,7 @@ export const createScale = /* @__PURE__ */ action(
164
165
  .editSemantic({ property: `scale[${id}].range`, value: definition.range });
165
166
 
166
167
  for (const property of [
167
- "nice", "zero", "clamp", "reverse", "base", "exponent", "constant",
168
+ "emptyDomain", "nice", "zero", "clamp", "reverse", "base", "exponent", "constant",
168
169
  "paddingInner", "paddingOuter", "padding", "align", "interpolate", "midpoint",
169
170
  "radialMapping", "unknown"
170
171
  ]) {
@@ -37,7 +37,7 @@ import { resolveRequestedOffsetPolicy } from
37
37
  "../../materialization/scales/policies/offset.js";
38
38
  import { findScaleConsumers } from "./consumers/index.js";
39
39
 
40
- const BASE_OPTIONS = ["id", "type", "domain", "range"];
40
+ const BASE_OPTIONS = ["id", "type", "domain", "range", "emptyDomain"];
41
41
  const UNKNOWN_OPTIONS = [...BASE_OPTIONS, "unknown"];
42
42
  const CLAMP_REVERSE = ["clamp", "reverse"];
43
43
  const BOOLEAN_OPTIONS = ["nice", "zero", ...CLAMP_REVERSE];
@@ -26,7 +26,7 @@ import { hasExactLegendSamplingForScale } from
26
26
  "../guides/legends/sampling.js";
27
27
 
28
28
  const OPTIONS = Object.freeze([
29
- "id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
29
+ "id", "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
30
30
  "base", "exponent", "constant", "paddingInner", "paddingOuter", "padding",
31
31
  "align", "interpolate", "midpoint", "unknown", "palette", "radialMapping"
32
32
  ]);
@@ -6,7 +6,7 @@ import { requireLayer } from "../../selectors/layers.js";
6
6
  import { requireSemanticScale } from "../../selectors/scales.js";
7
7
 
8
8
  const QUANTITATIVE_OPTIONS = Object.freeze([
9
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
9
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
10
10
  "base", "exponent", "constant", "unknown"
11
11
  ]);
12
12
  const ORDINAL_OPTIONS = Object.freeze([
@@ -1,5 +1,5 @@
1
1
  const SCALE_EDIT_PROPERTIES = Object.freeze([
2
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
2
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
3
3
  "base", "exponent", "constant", "paddingInner", "paddingOuter",
4
4
  "padding", "align", "interpolate", "midpoint", "unknown"
5
5
  ]);
@@ -9,7 +9,10 @@ import {
9
9
  resolveScaleConsumerBounds
10
10
  } from "../../materialization/coordinateBounds.js";
11
11
  import { requireCoordinate } from "../../selectors/coordinates.js";
12
- import { resolveScaleMaterialization } from "../../materialization/scales/resolve.js";
12
+ import {
13
+ rememberResolvedScaleBinding,
14
+ resolveScaleMaterialization
15
+ } from "../../materialization/scales/resolve.js";
13
16
  import { findScale, findScaleConsumers, resolveConsumerCategoryOrder,
14
17
  resolveConsumerValues, resolveSeriesLayoutScaleValues } from "./consumers/index.js";
15
18
 
@@ -126,6 +129,30 @@ export function resolveScalePreview(program, id) {
126
129
  return resolveScalePreviewAtBounds(program, id, bounds);
127
130
  }
128
131
 
132
+ export function restoreResolvedScaleBindings(program) {
133
+ for (const scale of program.semanticSpec.scales) {
134
+ const resolved = program.resolvedScales[scale.id];
135
+ if (resolved === undefined) continue;
136
+ const consumers = findScaleConsumers(program, scale.id);
137
+ const channels = new Set(consumers.map(consumer =>
138
+ normalizePositionScaleChannel(consumer.channel)
139
+ ));
140
+ const families = new Set([...channels].map(channel =>
141
+ channel === "stroke" ? "color" : channel
142
+ ));
143
+ if (families.size === 1) {
144
+ const channel = channels.size === 1
145
+ ? channels.values().next().value
146
+ : families.values().next().value;
147
+ rememberResolvedScaleBinding(resolved, channel, consumers);
148
+ }
149
+ }
150
+ for (const child of Object.values(program.children)) {
151
+ restoreResolvedScaleBindings(child);
152
+ }
153
+ return program;
154
+ }
155
+
129
156
  export function validatePendingMeasuredScale(program, scale, consumers) {
130
157
  const bounds = resolveScaleConsumerBounds(program, consumers);
131
158
  const range = resolveArcAutoPositionRange({ consumers, scale, channel: "radius",
@@ -6,6 +6,7 @@ import {
6
6
  setThemeStateOverrides
7
7
  } from "./state.js";
8
8
  import { findLayer } from "../../selectors/layers.js";
9
+ import { reconcileFollowingRegressions } from "../regression/follow.js";
9
10
 
10
11
  let rematerializeThemeHighlights = program => program;
11
12
 
@@ -979,6 +980,7 @@ function collectOverrides(program) {
979
980
  }
980
981
 
981
982
  export function reconcileProgramTheme(program, { source, metadata }) {
983
+ program = reconcileFollowingRegressions(program);
982
984
  const storedState = program.materializationConfigs.theme;
983
985
  if (storedState === undefined) return program;
984
986
  // Data/context/trace-only transitions cannot change authored appearance.
@@ -192,7 +192,8 @@ export function action(metadata, implementation) {
192
192
  const ownedMetadata = freezeOwned({
193
193
  op: metadata.op,
194
194
  description: metadata.description,
195
- scope
195
+ scope,
196
+ ...(Array.isArray(metadata.options) ? { options: [...metadata.options] } : {})
196
197
  });
197
198
 
198
199
  const wrappedAction = function wrappedAction(args = {}) {
@@ -240,7 +241,7 @@ export function action(metadata, implementation) {
240
241
  // Private built-in convenience: action() owns the object boundary and trace,
241
242
  // while each definition supplies its closed option vocabulary exactly once.
242
243
  export function closedAction(metadata, options, implementation) {
243
- return action(metadata, function (args = {}) {
244
+ return action({ ...metadata, options }, function (args = {}) {
244
245
  if (options !== undefined) validateKeys(args, options, metadata.op);
245
246
  return implementation.call(this, args);
246
247
  });
@@ -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;
@@ -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