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
@@ -13,9 +13,10 @@ import {
13
13
  import { resolveEligibleLayer } from "../../../selectors/layers.js";
14
14
  import { rematerializeExistingLegend } from "../../encodings/shared.js";
15
15
  import { validateMarkOptions } from "../shared.js";
16
+ import { validateItemMissing } from "../../../grammar/itemMissing.js";
16
17
 
17
18
  const OPTIONS = Object.freeze([
18
- "target", "shape", "fill", "opacity", "stroke", "strokeWidth",
19
+ "target", "missing", "shape", "fill", "opacity", "stroke", "strokeWidth",
19
20
  ...STROKE_STYLE_PROPERTIES
20
21
  ]);
21
22
 
@@ -28,12 +29,12 @@ export const editPointMark = /* @__PURE__ */ action(
28
29
  validateMarkOptions(args, OPTIONS, "editPointMark");
29
30
  const strokeDetails = requestedStrokeDetails(args, "editPointMark");
30
31
  const editable = [
31
- "shape", "fill", "opacity", "stroke", "strokeWidth",
32
+ "missing", "shape", "fill", "opacity", "stroke", "strokeWidth",
32
33
  ...STROKE_STYLE_PROPERTIES
33
34
  ];
34
35
  if (!editable.some(property => Object.hasOwn(args, property))) {
35
36
  throw new Error(
36
- "editPointMark requires shape, fill, opacity, stroke, or strokeWidth."
37
+ "editPointMark requires shape, fill, opacity, stroke, strokeWidth, or missing."
37
38
  );
38
39
  }
39
40
  if (args.target !== undefined) validateUserId(args.target, "Point mark id");
@@ -61,7 +62,13 @@ export const editPointMark = /* @__PURE__ */ action(
61
62
  if (Object.hasOwn(args, "opacity") && layer.encoding?.opacity?.field !== undefined) {
62
63
  throw new Error("editPointMark opacity conflicts with a field encoding; use encodeOpacity with value to replace it.");
63
64
  }
64
- const config = { ...this.markConfigs[id] };
65
+ const semantic = Object.hasOwn(args, "missing")
66
+ ? this.editSemantic({
67
+ property: `layer[${id}].mark.missing`,
68
+ value: validateItemMissing(args.missing, "Point missing")
69
+ })
70
+ : this;
71
+ const config = { ...semantic.markConfigs[id] };
65
72
  Object.assign(config, strokeDetails);
66
73
  if (Object.hasOwn(args, "shape")) {
67
74
  config.shape = validatePointShape(args.shape);
@@ -97,7 +104,7 @@ export const editPointMark = /* @__PURE__ */ action(
97
104
  "Point strokeWidth"
98
105
  );
99
106
  }
100
- const next = this
107
+ const next = semantic
101
108
  ._withMarkConfig(id, config)
102
109
  .rematerializePointMark({ id });
103
110
  return rematerializeExistingLegend(next);
@@ -40,6 +40,7 @@ import { offsetCategoryPositions } from
40
40
  "../../../materialization/categorySlotOffset.js";
41
41
  import { requestedStrokeDetails } from
42
42
  "../../../grammar/strokeStyle.js";
43
+ import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
43
44
 
44
45
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
45
46
  const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
@@ -337,10 +338,11 @@ export const rematerializePointMark = /* @__PURE__ */ action(
337
338
  if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
338
339
  throw new Error(`Point mark "${id}" requires point graphics.`);
339
340
  }
340
- const dataset = findDataset(resolved, layer.data);
341
- if (dataset === undefined) {
341
+ const sourceDataset = findDataset(resolved, layer.data);
342
+ if (sourceDataset === undefined) {
342
343
  throw new Error(`Point mark "${id}" requires an existing dataset.`);
343
344
  }
345
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
344
346
 
345
347
  if (graphic.items.length === 0 && !canMaterializePoint(resolved, layer)) {
346
348
  return resolved;
@@ -29,13 +29,15 @@ import {
29
29
  } from "../../../grammar/roundedRect.js";
30
30
  import { STROKE_STYLE_PROPERTIES } from
31
31
  "../../../grammar/strokeStyle.js";
32
+ import { applyItemMissingPolicy, validateItemMissing } from
33
+ "../../../grammar/itemMissing.js";
32
34
 
33
35
  const STYLE_OPTIONS = Object.freeze([
34
36
  "fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
35
37
  ...STROKE_STYLE_PROPERTIES
36
38
  ]);
37
- const CREATE_OPTIONS = Object.freeze(["id", "data", ...STYLE_OPTIONS]);
38
- const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
39
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...STYLE_OPTIONS]);
40
+ const EDIT_OPTIONS = Object.freeze(["target", "missing", ...STYLE_OPTIONS]);
39
41
  const REMATERIALIZE_OPTIONS = Object.freeze(["id", "scales"]);
40
42
 
41
43
  function requireRectLayer(program, requested, operation) {
@@ -71,6 +73,12 @@ const createRectMark = /* @__PURE__ */ action(
71
73
  let next = this
72
74
  .editSemantic({ property: `layer[${id}].mark.type`, value: "rect" })
73
75
  .editSemantic({ property: `layer[${id}].data`, value: data });
76
+ if (Object.hasOwn(args, "missing")) {
77
+ next = next.editSemantic({
78
+ property: `layer[${id}].mark.missing`,
79
+ value: validateItemMissing(args.missing, "Rect missing")
80
+ });
81
+ }
74
82
  next = applyLayeredMarkInheritance(next, id, inherited)
75
83
  .createGraphics({
76
84
  id,
@@ -120,10 +128,11 @@ const rematerializeRectMark = /* @__PURE__ */ action(
120
128
  if (this.markConfigs[id]?.gradientPlot?.materialized === true) {
121
129
  return this.materializeGradientPlotFill({ id });
122
130
  }
123
- const dataset = findDataset(this, layer.data);
124
- if (dataset === undefined) {
131
+ const sourceDataset = findDataset(this, layer.data);
132
+ if (sourceDataset === undefined) {
125
133
  throw new Error(`Rect mark "${id}" requires an existing dataset.`);
126
134
  }
135
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
127
136
  if (!canMaterializeRect(this, layer)) {
128
137
  return replaceGraphicItems(this, id, "rect", []);
129
138
  }
@@ -151,7 +160,7 @@ const editRectMark = /* @__PURE__ */ action(
151
160
  function (args = {}) {
152
161
  validateMarkOptions(args, EDIT_OPTIONS, "editRectMark");
153
162
  requestedRectStyleDetails(args, "editRectMark");
154
- if (!STYLE_OPTIONS.some(option => Object.hasOwn(args, option))) {
163
+ if (!["missing", ...STYLE_OPTIONS].some(option => Object.hasOwn(args, option))) {
155
164
  throw new Error("editRectMark requires at least one editable property.");
156
165
  }
157
166
  const layer = requireRectLayer(this, args.target, "editRectMark");
@@ -163,16 +172,22 @@ const editRectMark = /* @__PURE__ */ action(
163
172
  "editRectMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
164
173
  );
165
174
  }
166
- const next = this._withMarkConfig(
175
+ const semantic = Object.hasOwn(args, "missing")
176
+ ? this.editSemantic({
177
+ property: `layer[${layer.id}].mark.missing`,
178
+ value: validateItemMissing(args.missing, "Rect missing")
179
+ })
180
+ : this;
181
+ const next = semantic._withMarkConfig(
167
182
  layer.id,
168
183
  {
169
184
  ...normalizeRectMarkConfig(
170
185
  args,
171
- this.markConfigs[layer.id] ?? DEFAULT_RECT_MARK
186
+ semantic.markConfigs[layer.id] ?? DEFAULT_RECT_MARK
172
187
  ),
173
188
  fillExplicit: Object.hasOwn(args, "fill")
174
189
  ? true
175
- : this.markConfigs[layer.id]?.fillExplicit ?? false
190
+ : semantic.markConfigs[layer.id]?.fillExplicit ?? false
176
191
  }
177
192
  );
178
193
  const materialized = canMaterializeRect(next, layer)
@@ -39,13 +39,15 @@ import {
39
39
  requestedStrokeDetails,
40
40
  STROKE_STYLE_PROPERTIES
41
41
  } from "../../../grammar/strokeStyle.js";
42
+ import { applyItemMissingPolicy, validateItemMissing } from
43
+ "../../../grammar/itemMissing.js";
42
44
 
43
45
  const APPEARANCE_OPTIONS = Object.freeze([
44
46
  "stroke", "strokeWidth", "strokeDash", "opacity",
45
47
  ...STROKE_STYLE_PROPERTIES
46
48
  ]);
47
- const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
48
- const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
49
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...APPEARANCE_OPTIONS]);
50
+ const EDIT_OPTIONS = Object.freeze(["target", "missing", ...APPEARANCE_OPTIONS]);
49
51
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
50
52
  const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
51
53
  const DEFAULT_RULE_CONFIG = Object.freeze({
@@ -66,10 +68,11 @@ function requireRule(program, id) {
66
68
  if (layer?.mark?.type !== "rule") {
67
69
  throw new Error(`Unknown rule mark "${id}".`);
68
70
  }
69
- const dataset = findDataset(program, layer.data);
70
- if (dataset === undefined) {
71
+ const sourceDataset = findDataset(program, layer.data);
72
+ if (sourceDataset === undefined) {
71
73
  throw new Error(`Rule mark "${id}" requires an existing dataset.`);
72
74
  }
75
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
73
76
  const graphic = program.graphicSpec.objects[id];
74
77
  if (graphic?.type !== "line" || graphic.items === undefined) {
75
78
  throw new Error(`Rule mark "${id}" requires line collection graphics.`);
@@ -118,6 +121,12 @@ const createRuleMark = /* @__PURE__ */ action(
118
121
  let created = this
119
122
  .editSemantic({ property: `layer[${id}].mark.type`, value: "rule" })
120
123
  .editSemantic({ property: `layer[${id}].data`, value: data });
124
+ if (Object.hasOwn(args, "missing")) {
125
+ created = created.editSemantic({
126
+ property: `layer[${id}].mark.missing`,
127
+ value: validateItemMissing(args.missing, "Rule missing")
128
+ });
129
+ }
121
130
  created = applyLayeredMarkInheritance(created, id, inherited);
122
131
  created = created
123
132
  .createGraphics({
@@ -154,11 +163,18 @@ const editRuleMark = /* @__PURE__ */ action(
154
163
  label: "rule mark"
155
164
  });
156
165
  const appearance = planRuleAppearance(args, layer);
157
- if (appearance.length === 0 && Object.keys(strokeDetails).length === 0) {
158
- throw new Error("editRuleMark requires an appearance change.");
166
+ if (appearance.length === 0 && Object.keys(strokeDetails).length === 0 &&
167
+ !Object.hasOwn(args, "missing")) {
168
+ throw new Error("editRuleMark requires missing or an appearance change.");
159
169
  }
160
- const configured = this._withMarkConfig(layer.id, {
161
- ...this.markConfigs[layer.id],
170
+ const semantic = Object.hasOwn(args, "missing")
171
+ ? this.editSemantic({
172
+ property: `layer[${layer.id}].mark.missing`,
173
+ value: validateItemMissing(args.missing, "Rule missing")
174
+ })
175
+ : this;
176
+ const configured = semantic._withMarkConfig(layer.id, {
177
+ ...semantic.markConfigs[layer.id],
162
178
  ...strokeDetails
163
179
  });
164
180
  const materialized = appearance.length === 0
@@ -39,13 +39,14 @@ import {
39
39
  } from "../../charts/shared.js";
40
40
  import { resolveMarkGraphicPlacement } from
41
41
  "../../../materialization/graphicHierarchy.js";
42
+ import { validateItemMissing } from "../../../grammar/itemMissing.js";
42
43
 
43
44
  const STYLE_OPTIONS = Object.freeze([
44
45
  "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
45
46
  "align", "baseline", "rotation", "dx", "dy"
46
47
  ]);
47
- const CREATE_OPTIONS = Object.freeze(["id", "data", "source", "text", ...STYLE_OPTIONS]);
48
- const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
48
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "source", "text", "missing", ...STYLE_OPTIONS]);
49
+ const EDIT_OPTIONS = Object.freeze(["target", "missing", ...STYLE_OPTIONS]);
49
50
  const REMATERIALIZE_OPTIONS = Object.freeze(["id", "replayLayout"]);
50
51
 
51
52
  function sourceMatchesData(program, layer, requestedData) {
@@ -139,6 +140,9 @@ const createTextMark = /* @__PURE__ */ action(
139
140
  operation: "createTextMark"
140
141
  });
141
142
  const inherited = resolveTextInheritance(this, args);
143
+ if (Object.hasOwn(args, "missing") && inherited?.source !== undefined) {
144
+ throw new Error("Text missing policy requires a row-backed text mark, not source-owned labels.");
145
+ }
142
146
  const { data } = resolveMarkData(this, {
143
147
  ...args,
144
148
  ...(args.data === undefined && inherited?.data !== undefined
@@ -150,6 +154,12 @@ const createTextMark = /* @__PURE__ */ action(
150
154
  let next = this
151
155
  .editSemantic({ property: `layer[${id}].mark.type`, value: "text" })
152
156
  .editSemantic({ property: `layer[${id}].data`, value: data });
157
+ if (Object.hasOwn(args, "missing")) {
158
+ next = next.editSemantic({
159
+ property: `layer[${id}].mark.missing`,
160
+ value: validateItemMissing(args.missing, "Text missing")
161
+ });
162
+ }
153
163
  if (inherited?.source !== undefined) {
154
164
  next = next.editSemantic({
155
165
  property: `layer[${id}].source`,
@@ -465,20 +475,29 @@ const editTextMark = /* @__PURE__ */ action(
465
475
  },
466
476
  function (args = {}) {
467
477
  validateMarkOptions(args, EDIT_OPTIONS, "editTextMark");
468
- if (!STYLE_OPTIONS.some(option => Object.hasOwn(args, option))) {
478
+ if (!["missing", ...STYLE_OPTIONS].some(option => Object.hasOwn(args, option))) {
469
479
  throw new Error("editTextMark requires at least one editable property.");
470
480
  }
471
481
  const layer = requireTextLayer(this, args.target, "editTextMark");
472
- const next = this._withMarkConfig(
482
+ if (Object.hasOwn(args, "missing") && layer.source !== undefined) {
483
+ throw new Error("Text missing policy requires a row-backed text mark, not source-owned labels.");
484
+ }
485
+ const semantic = Object.hasOwn(args, "missing")
486
+ ? this.editSemantic({
487
+ property: `layer[${layer.id}].mark.missing`,
488
+ value: validateItemMissing(args.missing, "Text missing")
489
+ })
490
+ : this;
491
+ const next = semantic._withMarkConfig(
473
492
  layer.id,
474
493
  {
475
494
  ...normalizeTextMarkConfig(
476
495
  args,
477
- this.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
496
+ semantic.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
478
497
  ),
479
498
  fillExplicit: Object.hasOwn(args, "fill")
480
499
  ? true
481
- : this.markConfigs[layer.id]?.fillExplicit ?? false
500
+ : semantic.markConfigs[layer.id]?.fillExplicit ?? false
482
501
  }
483
502
  );
484
503
  return canMaterializeText(next, layer)
@@ -21,6 +21,8 @@ import {
21
21
  requestedStrokeDetails,
22
22
  STROKE_STYLE_PROPERTIES
23
23
  } from "../../../grammar/strokeStyle.js";
24
+ import { applyItemMissingPolicy, validateItemMissing } from
25
+ "../../../grammar/itemMissing.js";
24
26
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
25
27
  import { rematerializeExistingLegend } from "../../encodings/shared.js";
26
28
  import {
@@ -37,8 +39,8 @@ import {
37
39
  const APPEARANCE_OPTIONS = Object.freeze([
38
40
  "length", "stroke", "strokeWidth", "opacity", ...STROKE_STYLE_PROPERTIES
39
41
  ]);
40
- const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
41
- const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
42
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...APPEARANCE_OPTIONS]);
43
+ const EDIT_OPTIONS = Object.freeze(["target", "missing", ...APPEARANCE_OPTIONS]);
42
44
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
43
45
 
44
46
  export const DEFAULT_TICK_CONFIG = Object.freeze({
@@ -114,6 +116,12 @@ export const createTickMark = /* @__PURE__ */ action(
114
116
  let created = this
115
117
  .editSemantic({ property: `layer[${id}].mark.type`, value: "tick" })
116
118
  .editSemantic({ property: `layer[${id}].data`, value: data });
119
+ if (Object.hasOwn(args, "missing")) {
120
+ created = created.editSemantic({
121
+ property: `layer[${id}].mark.missing`,
122
+ value: validateItemMissing(args.missing, "Tick missing")
123
+ });
124
+ }
117
125
  created = applyLayeredMarkInheritance(created, id, inherited)
118
126
  .createGraphics({
119
127
  id,
@@ -141,9 +149,9 @@ export const editTickMark = /* @__PURE__ */ action(
141
149
  function (args = {}) {
142
150
  validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
143
151
  requestedStrokeDetails(args, "editTickMark");
144
- if (!APPEARANCE_OPTIONS.some(property => Object.hasOwn(args, property))) {
152
+ if (!["missing", ...APPEARANCE_OPTIONS].some(property => Object.hasOwn(args, property))) {
145
153
  throw new Error(
146
- "editTickMark requires length, stroke, strokeWidth, or opacity."
154
+ "editTickMark requires length, stroke, strokeWidth, opacity, or missing."
147
155
  );
148
156
  }
149
157
  const layer = resolveTick(this, args.target, "editTickMark");
@@ -152,11 +160,17 @@ export const editTickMark = /* @__PURE__ */ action(
152
160
  "editTickMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
153
161
  );
154
162
  }
163
+ const semantic = Object.hasOwn(args, "missing")
164
+ ? this.editSemantic({
165
+ property: `layer[${layer.id}].mark.missing`,
166
+ value: validateItemMissing(args.missing, "Tick missing")
167
+ })
168
+ : this;
155
169
  const config = validateTickConfig(args, {
156
170
  ...DEFAULT_TICK_CONFIG,
157
- ...this.markConfigs[layer.id]
171
+ ...semantic.markConfigs[layer.id]
158
172
  });
159
- const materialized = this
173
+ const materialized = semantic
160
174
  ._withMarkConfig(layer.id, config)
161
175
  .rematerializeTickMark({ id: layer.id });
162
176
  return rematerializeExistingLegend(materialized);
@@ -190,10 +204,11 @@ export const rematerializeTickMark = /* @__PURE__ */ action(
190
204
  if (graphic?.type !== "line" || !Array.isArray(graphic.items)) {
191
205
  throw new Error(`Tick mark "${id}" requires line collection graphics.`);
192
206
  }
193
- const dataset = findDataset(this, layer.data);
194
- if (dataset === undefined) {
207
+ const sourceDataset = findDataset(this, layer.data);
208
+ if (sourceDataset === undefined) {
195
209
  throw new Error(`Tick mark "${id}" requires an existing dataset.`);
196
210
  }
211
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
197
212
  if (
198
213
  layer.encoding?.x?.scale === undefined ||
199
214
  layer.encoding?.y?.scale === undefined
@@ -7,6 +7,7 @@ import {
7
7
  removeOwnedPath
8
8
  } from "../../core/immutable.js";
9
9
  import { parseSemanticPath } from "../../grammar/schemas/semanticPath.js";
10
+ import { deriveTransformSchema, inferDatasetSchema, validateRowsAgainstSchema } from "../../grammar/datasetSchema.js";
10
11
 
11
12
 
12
13
  const CONTEXT_KEYS = Object.freeze({
@@ -44,7 +45,39 @@ function updateEntity(spec, parsed, value) {
44
45
  }
45
46
 
46
47
  const current = index === -1 ? { id: parsed.id } : collection[index];
47
- const updated = setNestedProperty(current, parsed.path, value);
48
+ let updated = setNestedProperty(current, parsed.path, value);
49
+ if (
50
+ parsed.kind === "dataset" && parsed.path.length === 1 &&
51
+ parsed.path[0] === "transform" && current.schema === undefined &&
52
+ current.source !== undefined && value.length === 1
53
+ ) {
54
+ const source = collection.find(dataset => dataset.id === current.source);
55
+ if (source?.schema !== undefined) {
56
+ updated = setNestedProperty(updated, ["schema"], deriveTransformSchema(
57
+ source.schema,
58
+ value[0],
59
+ undefined,
60
+ { sourceId: source.id, ownerId: parsed.id }
61
+ ));
62
+ }
63
+ }
64
+ if (
65
+ parsed.kind === "dataset" && parsed.path.length === 1 &&
66
+ parsed.path[0] === "values" &&
67
+ (current.schema === undefined || current.schema.origin === "derived")
68
+ ) {
69
+ const source = current.source === undefined
70
+ ? undefined
71
+ : collection.find(dataset => dataset.id === current.source);
72
+ const schema = source?.schema !== undefined && current.transform?.length === 1
73
+ ? deriveTransformSchema(source.schema, current.transform[0], value, {
74
+ sourceId: source.id,
75
+ ownerId: parsed.id
76
+ })
77
+ : inferDatasetSchema(value);
78
+ validateRowsAgainstSchema(value, schema, `Dataset "${parsed.id}"`);
79
+ updated = setNestedProperty(updated, ["schema"], schema);
80
+ }
48
81
  const nextCollection = [...collection];
49
82
 
50
83
  if (index === -1) {
@@ -155,13 +188,14 @@ export function withRematerializedDerivedDataset(program, {
155
188
  id,
156
189
  source,
157
190
  transform,
158
- values
191
+ values,
192
+ schema
159
193
  }) {
160
194
  const semanticSpec = {
161
195
  ...program.semanticSpec,
162
196
  datasets: program.semanticSpec.datasets.map(dataset =>
163
197
  dataset.id === id
164
- ? { ...dataset, source, transform: [transform], values }
198
+ ? { ...dataset, source, transform: [transform], values, ...(schema === undefined ? {} : { schema }) }
165
199
  : dataset
166
200
  )
167
201
  };
@@ -1,6 +1,10 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
2
  import { validateDataRows } from "../../../core/validation.js";
3
- import { hasDataset } from "../../../selectors/datasets.js";
3
+ import { findDataset, hasDataset } from "../../../selectors/datasets.js";
4
+ import {
5
+ validateDatasetSchema,
6
+ validateRowsAgainstSchema
7
+ } from "../../../grammar/datasetSchema.js";
4
8
 
5
9
  export function validateDatasetSemanticValue(
6
10
  program,
@@ -11,6 +15,14 @@ export function validateDatasetSemanticValue(
11
15
  const property = parsed.path[0];
12
16
  if (property === "values") {
13
17
  validateDataRows(value, `Dataset "${parsed.id}"`);
18
+ const schema = findDataset(program, parsed.id)?.schema;
19
+ if (schema !== undefined && schema.origin !== "derived") {
20
+ validateRowsAgainstSchema(value, schema, `Dataset "${parsed.id}"`);
21
+ }
22
+ return;
23
+ }
24
+ if (property === "schema") {
25
+ validateDatasetSchema(value);
14
26
  return;
15
27
  }
16
28
  if (property === "source") {
@@ -19,6 +19,8 @@ import { findLayer } from "../../../selectors/layers.js";
19
19
  import { validateNonEmptySemanticString } from "./shared.js";
20
20
  import { normalizeStatisticalWeight } from
21
21
  "../../../grammar/weightedStatistics.js";
22
+ import { ITEM_MISSING_MARK_TYPES, validateItemMissing } from
23
+ "../../../grammar/itemMissing.js";
22
24
 
23
25
  function validateLayerSource(program, parsed, value, sourceMarkTypes) {
24
26
  validateUserId(value, "Layer source id");
@@ -52,8 +54,26 @@ export function validateLayerSemanticValue(
52
54
  if (property === "mark.type" && !MARK_TYPES.includes(value)) {
53
55
  throw new Error(`Unknown mark type "${value}".`);
54
56
  }
55
- if (property === "mark.missing" && !["error", "break"].includes(value)) {
56
- throw new Error(`Unsupported area missing policy "${value}".`);
57
+ if (property === "mark.missing") {
58
+ const markType = findLayer(program, parsed.id)?.mark?.type;
59
+ if (markType === "area") {
60
+ if (!["error", "break"].includes(value)) {
61
+ throw new Error(`Unsupported area missing policy "${value}".`);
62
+ }
63
+ } else if (ITEM_MISSING_MARK_TYPES.includes(markType)) {
64
+ validateItemMissing(value, `${markType} missing`);
65
+ } else {
66
+ throw new Error(`Mark type "${markType}" does not support a missing policy.`);
67
+ }
68
+ }
69
+ if (property === "derivedBindings.regression") {
70
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
71
+ Object.keys(value).length !== 2 || value.mode !== "follow" ||
72
+ !Array.isArray(value.roles) || value.roles.length !== 3 ||
73
+ value.roles.join(",") !== "data,x,y") {
74
+ throw new Error("Regression derived binding must follow data, x, and y roles.");
75
+ }
76
+ return;
57
77
  }
58
78
  if (property === "layout.mode" && !COLOR_LAYOUTS.includes(value)) {
59
79
  throw new Error(`Unsupported series layout "${value}".`);
@@ -55,6 +55,12 @@ export function validateScaleSemanticValue(program, parsed, value) {
55
55
  return;
56
56
  }
57
57
  if (property === "domain") return validateSemanticScaleDomain(value);
58
+ if (property === "emptyDomain") {
59
+ if (!["preserve", "require-explicit"].includes(value)) {
60
+ throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');
61
+ }
62
+ return;
63
+ }
58
64
  if (property === "range") return validateSemanticScaleRange(value);
59
65
  if (["nice", "zero", "clamp", "reverse"].includes(property)) {
60
66
  if (typeof value !== "boolean") {
@@ -1,6 +1,6 @@
1
1
  import { closedAction } from "../../core/action.js";
2
2
  import { validateKeys } from "../../core/validation.js";
3
- import { normalizeRegressionParameters } from "../../grammar/regression/index.js";
3
+ import { normalizeRegressionParameters, normalizeRegressionPredict } from "../../grammar/regression/index.js";
4
4
  import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
5
5
  import {
6
6
  findRegressionPoint,
@@ -12,6 +12,7 @@ import {
12
12
  const REGRESSION_OPTIONS = Object.freeze([
13
13
  "target", "x", "y", "groupBy", "method", "degree", "span",
14
14
  "confidenceMethod", "level", "confidence", "interval", "band", "line"
15
+ , "predict", "sourceBinding", "missing"
15
16
  ]);
16
17
 
17
18
  export const createRegression = /* @__PURE__ */ closedAction(
@@ -20,6 +21,10 @@ export const createRegression = /* @__PURE__ */ closedAction(
20
21
  description: "Fit and layer regression lines with optional interval bands."
21
22
  }, REGRESSION_OPTIONS,
22
23
  function (args = {}) {
24
+ const sourceBinding = args.sourceBinding ?? "fixed";
25
+ if (!["fixed", "follow"].includes(sourceBinding)) {
26
+ throw new Error('Regression sourceBinding must be "fixed" or "follow".');
27
+ }
23
28
  const point = findRegressionPoint(this, args.target);
24
29
  const x = args.x === undefined
25
30
  ? point.encoding.x.field
@@ -37,11 +42,21 @@ export const createRegression = /* @__PURE__ */ closedAction(
37
42
  );
38
43
  }
39
44
  const parameters = normalizeRegressionParameters(args);
45
+ const predict = normalizeRegressionPredict(args.predict);
46
+ const regressionDefinition = {
47
+ ...parameters,
48
+ ...(predict === undefined ? {} : { predict }),
49
+ ...(args.missing === undefined ? {} : { missing: args.missing })
50
+ };
40
51
  let band = false;
41
52
  if (parameters.method === "loess") {
42
53
  if (args.band !== undefined && args.band !== false) {
43
54
  throw new Error("LOESS regression does not support a band object.");
44
55
  }
56
+ } else if (parameters.interval === false) {
57
+ if (args.band !== undefined && args.band !== false) {
58
+ throw new Error("Regression interval false cannot include a band object.");
59
+ }
45
60
  } else if (args.band !== false) {
46
61
  band = requireRegressionObject(args.band ?? {}, "Regression band");
47
62
  }
@@ -78,7 +93,7 @@ export const createRegression = /* @__PURE__ */ closedAction(
78
93
  x,
79
94
  y,
80
95
  ...(groupBy === undefined ? {} : { groupBy }),
81
- ...parameters
96
+ ...regressionDefinition
82
97
  });
83
98
  if (band !== false) {
84
99
  next = next.createRegressionBand({
@@ -105,7 +120,7 @@ export const createRegression = /* @__PURE__ */ closedAction(
105
120
  yScale,
106
121
  ...line
107
122
  });
108
- return next._withMarkConfig(point.id, {
123
+ next = next._withMarkConfig(point.id, {
109
124
  ...next.markConfigs[point.id],
110
125
  regression: {
111
126
  source: point.data,
@@ -119,8 +134,14 @@ export const createRegression = /* @__PURE__ */ closedAction(
119
134
  dataId,
120
135
  bandId: band === false ? undefined : bandId,
121
136
  lineId,
122
- parameters
137
+ parameters: regressionDefinition
123
138
  }
124
139
  });
140
+ return sourceBinding === "follow"
141
+ ? next.editSemantic({
142
+ property: `layer[${point.id}].derivedBindings.regression`,
143
+ value: { mode: "follow", roles: ["data", "x", "y"] }
144
+ })
145
+ : next;
125
146
  }
126
147
  );