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,5 +1,5 @@
1
1
  import { hasSemanticScaleReferences } from "../../selectors/scales.js";
2
- import { action } from "../../core/action.js";
2
+ import { closedAction } from "../../core/action.js";
3
3
  import {
4
4
  cloneAndFreeze,
5
5
  freezeOwned,
@@ -7,6 +7,8 @@ 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";
11
+
10
12
 
11
13
  const CONTEXT_KEYS = Object.freeze({
12
14
  dataset: "currentData",
@@ -43,7 +45,39 @@ function updateEntity(spec, parsed, value) {
43
45
  }
44
46
 
45
47
  const current = index === -1 ? { id: parsed.id } : collection[index];
46
- 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
+ }
47
81
  const nextCollection = [...collection];
48
82
 
49
83
  if (index === -1) {
@@ -154,13 +188,14 @@ export function withRematerializedDerivedDataset(program, {
154
188
  id,
155
189
  source,
156
190
  transform,
157
- values
191
+ values,
192
+ schema
158
193
  }) {
159
194
  const semanticSpec = {
160
195
  ...program.semanticSpec,
161
196
  datasets: program.semanticSpec.datasets.map(dataset =>
162
197
  dataset.id === id
163
- ? { ...dataset, source, transform: [transform], values }
198
+ ? { ...dataset, source, transform: [transform], values, ...(schema === undefined ? {} : { schema }) }
164
199
  : dataset
165
200
  )
166
201
  };
@@ -191,13 +226,15 @@ export function withoutPreviewLayerEncodings(program, { id, channels }) {
191
226
  }
192
227
 
193
228
  export function createSemanticPrimitiveAction(validateSemanticValue, planNamedRemoval) {
194
- return action(
229
+ return closedAction(
195
230
  {
196
231
  op: "editSemantic",
197
232
  description: "Create, replace, or remove one semantic property.",
198
233
  scope: "any"
199
- },
200
- function ({ property, value, remove = false } = {}) {
234
+ }, ["property", "value", "remove"],
235
+ function (args = {}) {
236
+
237
+ const { property, value, remove = false } = args;
201
238
  if (typeof remove !== "boolean") {
202
239
  throw new TypeError("editSemantic remove must be a boolean.");
203
240
  }
@@ -1,6 +1,10 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
- import { isPlainObject } from "../../../core/immutable.js";
3
- import { hasDataset } from "../../../selectors/datasets.js";
2
+ import { validateDataRows } from "../../../core/validation.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,
@@ -10,11 +14,17 @@ export function validateDatasetSemanticValue(
10
14
  ) {
11
15
  const property = parsed.path[0];
12
16
  if (property === "values") {
13
- if (!Array.isArray(value) || !value.every(isPlainObject)) {
14
- throw new TypeError("Dataset values must be an array of plain row objects.");
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}"`);
15
21
  }
16
22
  return;
17
23
  }
24
+ if (property === "schema") {
25
+ validateDatasetSchema(value);
26
+ return;
27
+ }
18
28
  if (property === "source") {
19
29
  validateUserId(value, "Dataset source id");
20
30
  if (!hasDataset(program, value)) {
@@ -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
- import { action } from "../../core/action.js";
1
+ import { closedAction } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
- import { validateKeys } from "../../core/validation.js";
3
+
4
4
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
5
5
  import { findDataset } from "../../selectors/datasets.js";
6
6
  import { resolveEligibleLayer } from "../../selectors/layers.js";
@@ -72,13 +72,12 @@ function requireRegressionBandDataset(program, args) {
72
72
  return { dataset, transform };
73
73
  }
74
74
 
75
- export const createRegressionBand = action(
75
+ export const createRegressionBand = /* @__PURE__ */ closedAction(
76
76
  {
77
77
  op: "createRegressionBand",
78
78
  description: "Create and encode a grouped regression confidence band."
79
- },
79
+ }, BAND_OPTIONS,
80
80
  function (args = {}) {
81
- validateKeys(args, BAND_OPTIONS, "createRegressionBand");
82
81
  const id = validateUserId(args.id, "Regression band id");
83
82
  const { transform } = requireRegressionBandDataset(this, args);
84
83
  validateAreaCreateOutline(args, "createRegressionBand");
@@ -121,13 +120,12 @@ export const createRegressionBand = action(
121
120
  }
122
121
  );
123
122
 
124
- export const editRegressionBand = action(
123
+ export const editRegressionBand = /* @__PURE__ */ closedAction(
125
124
  {
126
125
  op: "editRegressionBand",
127
126
  description: "Edit regression-band fill, opacity, and outline."
128
- },
127
+ }, EDIT_BAND_OPTIONS,
129
128
  function (args = {}) {
130
- validateKeys(args, EDIT_BAND_OPTIONS, "editRegressionBand");
131
129
  const changes = [
132
130
  "color", "opacity", "stroke", "strokeWidth", "curve",
133
131
  ...STROKE_STYLE_PROPERTIES
@@ -157,13 +155,12 @@ export const editRegressionBand = action(
157
155
  }
158
156
  );
159
157
 
160
- export const editRegressionLine = action(
158
+ export const editRegressionLine = /* @__PURE__ */ closedAction(
161
159
  {
162
160
  op: "editRegressionLine",
163
161
  description: "Edit regression-line width or curve."
164
- },
162
+ }, EDIT_LINE_OPTIONS,
165
163
  function (args = {}) {
166
- validateKeys(args, EDIT_LINE_OPTIONS, "editRegressionLine");
167
164
  if (
168
165
  !["strokeWidth", "curve", ...STROKE_STYLE_PROPERTIES]
169
166
  .some(property => Object.hasOwn(args, property))
@@ -187,13 +184,12 @@ export const editRegressionLine = action(
187
184
  }
188
185
  );
189
186
 
190
- export const createRegressionLine = action(
187
+ export const createRegressionLine = /* @__PURE__ */ closedAction(
191
188
  {
192
189
  op: "createRegressionLine",
193
190
  description: "Create and encode grouped regression line paths."
194
- },
191
+ }, LINE_OPTIONS,
195
192
  function (args = {}) {
196
- validateKeys(args, LINE_OPTIONS, "createRegressionLine");
197
193
  const id = validateUserId(args.id, "Regression line id");
198
194
  let next = this
199
195
  .createLineMark({
@@ -1,6 +1,6 @@
1
- import { action } from "../../core/action.js";
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,15 +12,19 @@ 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
- export const createRegression = action(
18
+ export const createRegression = /* @__PURE__ */ closedAction(
18
19
  {
19
20
  op: "createRegression",
20
21
  description: "Fit and layer regression lines with optional interval bands."
21
- },
22
+ }, REGRESSION_OPTIONS,
22
23
  function (args = {}) {
23
- validateKeys(args, REGRESSION_OPTIONS, "createRegression");
24
+ const sourceBinding = args.sourceBinding ?? "fixed";
25
+ if (!["fixed", "follow"].includes(sourceBinding)) {
26
+ throw new Error('Regression sourceBinding must be "fixed" or "follow".');
27
+ }
24
28
  const point = findRegressionPoint(this, args.target);
25
29
  const x = args.x === undefined
26
30
  ? point.encoding.x.field
@@ -38,11 +42,21 @@ export const createRegression = action(
38
42
  );
39
43
  }
40
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
+ };
41
51
  let band = false;
42
52
  if (parameters.method === "loess") {
43
53
  if (args.band !== undefined && args.band !== false) {
44
54
  throw new Error("LOESS regression does not support a band object.");
45
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
+ }
46
60
  } else if (args.band !== false) {
47
61
  band = requireRegressionObject(args.band ?? {}, "Regression band");
48
62
  }
@@ -79,7 +93,7 @@ export const createRegression = action(
79
93
  x,
80
94
  y,
81
95
  ...(groupBy === undefined ? {} : { groupBy }),
82
- ...parameters
96
+ ...regressionDefinition
83
97
  });
84
98
  if (band !== false) {
85
99
  next = next.createRegressionBand({
@@ -106,7 +120,7 @@ export const createRegression = action(
106
120
  yScale,
107
121
  ...line
108
122
  });
109
- return next._withMarkConfig(point.id, {
123
+ next = next._withMarkConfig(point.id, {
110
124
  ...next.markConfigs[point.id],
111
125
  regression: {
112
126
  source: point.data,
@@ -120,8 +134,14 @@ export const createRegression = action(
120
134
  dataId,
121
135
  bandId: band === false ? undefined : bandId,
122
136
  lineId,
123
- parameters
137
+ parameters: regressionDefinition
124
138
  }
125
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;
126
146
  }
127
147
  );
@@ -1,4 +1,4 @@
1
- import { action } from "../../core/action.js";
1
+ import { closedAction } from "../../core/action.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateUserId } from "../../core/identifiers.js";
4
4
  import {
@@ -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) {
@@ -234,27 +247,42 @@ function updateRegressionBandSemantics(program, bandId, { x, groupBy }) {
234
247
  });
235
248
  }
236
249
 
237
- export const editRegression = action(
250
+ export const editRegression = /* @__PURE__ */ closedAction(
238
251
  {
239
252
  op: "editRegression",
240
253
  description: "Revise a regression model and its owned visible components."
241
- },
254
+ }, OPTIONS,
242
255
  function (args = {}) {
243
- validateKeys(args, OPTIONS, "editRegression");
244
256
  if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
245
257
  throw new Error("editRegression requires a statistical or component option.");
246
258
  }
247
259
  const owner = resolveRegressionOwner(this, args.target);
248
260
  const current = this.markConfigs[owner.id].regression;
249
- 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")
250
278
  ? validateUserId(args.data, "Regression data id")
251
- : current.source;
252
- const x = Object.hasOwn(args, "x")
279
+ : current.source);
280
+ const x = followed?.x ?? (Object.hasOwn(args, "x")
253
281
  ? requireRegressionField(args.x, "Regression x")
254
- : current.x;
255
- const y = Object.hasOwn(args, "y")
282
+ : current.x);
283
+ const y = followed?.y ?? (Object.hasOwn(args, "y")
256
284
  ? requireRegressionField(args.y, "Regression y")
257
- : current.y;
285
+ : current.y);
258
286
  const groupBy = Object.hasOwn(args, "groupBy")
259
287
  ? args.groupBy === false
260
288
  ? undefined
@@ -273,12 +301,12 @@ export const editRegression = action(
273
301
  const linePatch = Object.hasOwn(args, "line")
274
302
  ? validateLinePatch(args.line)
275
303
  : undefined;
276
- if (parameters.method === "loess" && bandPatch !== undefined && bandPatch !== false) {
304
+ if ((parameters.method === "loess" || parameters.interval === false) && bandPatch !== undefined && bandPatch !== false) {
277
305
  throw new Error("LOESS regression does not support a band object.");
278
306
  }
279
307
 
280
308
  const hadBand = current.bandId !== undefined;
281
- const wantsBand = parameters.method !== "loess" &&
309
+ const wantsBand = parameters.method !== "loess" && parameters.interval !== false &&
282
310
  (bandPatch === false
283
311
  ? false
284
312
  : hadBand || bandPatch !== undefined || current.parameters.method === "loess");
@@ -386,6 +414,12 @@ export const editRegression = action(
386
414
  if (changesStatistics) {
387
415
  next = next.releaseDerivedData(revision.release);
388
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
+ }
389
423
  return next;
390
424
  };
391
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
+ }
@@ -1,3 +1,4 @@
1
+ import { annotateError } from "../../core/diagnostics.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import { validateOptionObject } from "../../core/validation.js";
@@ -70,20 +71,20 @@ export function resolveResourceOwnership(program, { kind, id }) {
70
71
  if (resource !== undefined) return { ownership: "named", resource };
71
72
  const other = existingKind(program, id);
72
73
  if (other !== undefined) {
73
- throw new Error(`Resource "${id}" exists as ${other}, not ${kind}.`);
74
+ throw annotateError(new Error(`Resource "${id}" exists as ${other}, not ${kind}.`), { code: "incompatible-resource", resourceId: id });
74
75
  }
75
- throw new Error(`Unknown ${kind} "${id}".`);
76
+ throw annotateError(new Error(`Unknown ${kind} "${id}".`), { code: "missing-resource", resourceId: id });
76
77
  }
77
78
 
78
79
  const owners = dataOwners(program).filter(owner => owner.owner === id);
79
80
  if (owners.length > 1) {
80
- throw new Error(`Data resource "${id}" has ambiguous logical ownership.`);
81
+ throw annotateError(new Error(`Data resource "${id}" has ambiguous logical ownership.`), { code: "ambiguous-resource", resourceId: id });
81
82
  }
82
83
  if (owners.length === 1) {
83
84
  const owner = owners[0];
84
85
  const current = exactResource(program, "data", owner.current);
85
86
  if (current === undefined) {
86
- throw new Error(`Data owner "${id}" has no current dataset.`);
87
+ throw annotateError(new Error(`Data owner "${id}" has no current dataset.`), { code: "missing-resource", resourceId: id });
87
88
  }
88
89
  return { ownership: "standalone", resource: current, dataOwner: owner };
89
90
  }
@@ -98,9 +99,9 @@ export function resolveResourceOwnership(program, { kind, id }) {
98
99
  }
99
100
  const other = existingKind(program, id);
100
101
  if (other !== undefined) {
101
- throw new Error(`Resource "${id}" exists as ${other}, not data.`);
102
+ throw annotateError(new Error(`Resource "${id}" exists as ${other}, not data.`), { code: "incompatible-resource", resourceId: id });
102
103
  }
103
- throw new Error(`Unknown data "${id}".`);
104
+ throw annotateError(new Error(`Unknown data "${id}".`), { code: "missing-resource", resourceId: id });
104
105
  }
105
106
 
106
107
  function assertSupportedProgram(program, operation) {
@@ -126,10 +127,10 @@ export function planResourceRemoval(program, { kind, id, operation }) {
126
127
  value.path.at(-1) === "current"
127
128
  ));
128
129
  if (live.length > 0) {
129
- throw new Error(
130
+ throw annotateError(new Error(
130
131
  `Cannot remove ${kind} "${id}"; live references: ` +
131
132
  live.map(formatReference).join("; ")
132
- );
133
+ ), { code: "resource-in-use", resourceId: id, candidates: [...new Set(live.map(value => value.ownerId))] });
133
134
  }
134
135
  return Object.freeze({
135
136
  kind,
@@ -161,12 +162,12 @@ function removeResource(kind, operation, description) {
161
162
  );
162
163
  }
163
164
 
164
- export const removeData = removeResource(
165
+ export const removeData = /* @__PURE__ */ removeResource(
165
166
  "data", "removeData", "Remove one unreferenced named dataset."
166
167
  );
167
- export const removeScale = removeResource(
168
+ export const removeScale = /* @__PURE__ */ removeResource(
168
169
  "scale", "removeScale", "Remove one unreferenced named scale."
169
170
  );
170
- export const removeCoordinate = removeResource(
171
+ export const removeCoordinate = /* @__PURE__ */ removeResource(
171
172
  "coordinate", "removeCoordinate", "Remove one unreferenced named coordinate."
172
173
  );
@@ -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