ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -15,18 +15,26 @@ export const BOX_PLOT_OPTIONS = Object.freeze([
15
15
  "width", "outliers", "box", "median", "outlier", "guides"
16
16
  ]);
17
17
 
18
- const DEFAULT_BOX = Object.freeze({
18
+ const DEFAULT_BOX = {
19
19
  fill: DEFAULT_COLORS.mark,
20
20
  opacity: 1,
21
21
  stroke: DEFAULT_COLORS.mark,
22
22
  strokeWidth: 1.5
23
- });
24
- const DEFAULT_MEDIAN = Object.freeze({ stroke: "#1f2937", strokeWidth: 1.5 });
25
- const DEFAULT_OUTLIER = Object.freeze({
23
+ };
24
+ const DEFAULT_MEDIAN = { stroke: "#1f2937", strokeWidth: 1.5 };
25
+ const DEFAULT_OUTLIER = {
26
26
  shape: "diamond",
27
27
  radius: 3,
28
28
  opacity: 0.75
29
- });
29
+ };
30
+ const APPEARANCE_VALIDATORS = {
31
+ fill: validateNonEmptyString,
32
+ opacity: validateUnitInterval,
33
+ stroke: validateNonEmptyString,
34
+ strokeWidth: validateNonNegativeFinite,
35
+ shape: validatePointShape,
36
+ radius: validatePositiveFinite
37
+ };
30
38
 
31
39
  function plainOptions(value, keys, label, operation = "createBoxPlot") {
32
40
  if (value === undefined) return {};
@@ -37,13 +45,29 @@ function plainOptions(value, keys, label, operation = "createBoxPlot") {
37
45
  return value;
38
46
  }
39
47
 
48
+ function resolveAppearance(value, defaults, label, operation) {
49
+ const options = plainOptions(
50
+ value,
51
+ Object.keys(defaults),
52
+ label,
53
+ operation
54
+ );
55
+ return Object.freeze(Object.fromEntries(
56
+ Object.entries(defaults).map(([key, fallback]) => [
57
+ key,
58
+ options[key] === undefined
59
+ ? fallback
60
+ : APPEARANCE_VALIDATORS[key](
61
+ options[key],
62
+ `createBoxPlot ${label}.${key}`
63
+ )
64
+ ])
65
+ ));
66
+ }
67
+
40
68
  export function resolveBoxPosition(value, label, operation = "createBoxPlot") {
41
69
  if (value === undefined) return undefined;
42
- if (!isPlainObject(value)) {
43
- throw new TypeError(`${operation} ${label} must be a plain object.`);
44
- }
45
- validateKeys(value, ["field", "fieldType", "scale"], `${operation} ${label}`);
46
- return value;
70
+ return plainOptions(value, ["field", "fieldType", "scale"], label, operation);
47
71
  }
48
72
 
49
73
  export function boxEncodingArgs(value) {
@@ -61,21 +85,23 @@ export function resolveBoxGuides(value) {
61
85
 
62
86
  export function resolveBoxWhisker(value, operation = "createBoxPlot") {
63
87
  if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
64
- if (!isPlainObject(value)) {
65
- throw new TypeError(`${operation} whisker must be a plain object.`);
66
- }
67
- validateKeys(value, ["type", "factor"], `${operation} whisker`);
68
- const type = value.type ?? "tukey";
88
+ const options = plainOptions(
89
+ value,
90
+ ["type", "factor"],
91
+ "whisker",
92
+ operation
93
+ );
94
+ const type = options.type ?? "tukey";
69
95
  if (!["tukey", "minmax"].includes(type)) {
70
96
  throw new Error(`Unsupported createBoxPlot whisker type "${type}".`);
71
97
  }
72
98
  if (type === "minmax") {
73
- if (value.factor !== undefined) {
99
+ if (options.factor !== undefined) {
74
100
  throw new Error("createBoxPlot minmax whiskers do not accept factor.");
75
101
  }
76
102
  return Object.freeze({ type });
77
103
  }
78
- const factor = value.factor ?? 1.5;
104
+ const factor = options.factor ?? 1.5;
79
105
  if (!Number.isFinite(factor) || factor <= 0) {
80
106
  throw new RangeError(
81
107
  "createBoxPlot whisker factor must be positive and finite."
@@ -96,70 +122,13 @@ export function resolveBoxWidth(value, operation = "createBoxPlot") {
96
122
  }
97
123
 
98
124
  export function resolveBoxAppearance(value, operation = "createBoxPlot") {
99
- const options = plainOptions(
100
- value,
101
- ["fill", "opacity", "stroke", "strokeWidth"],
102
- "box",
103
- operation
104
- );
105
- return Object.freeze({
106
- fill: options.fill === undefined
107
- ? DEFAULT_BOX.fill
108
- : validateNonEmptyString(options.fill, "createBoxPlot box.fill"),
109
- opacity: options.opacity === undefined
110
- ? DEFAULT_BOX.opacity
111
- : validateUnitInterval(options.opacity, "createBoxPlot box.opacity"),
112
- stroke: options.stroke === undefined
113
- ? DEFAULT_BOX.stroke
114
- : validateNonEmptyString(options.stroke, "createBoxPlot box.stroke"),
115
- strokeWidth: options.strokeWidth === undefined
116
- ? DEFAULT_BOX.strokeWidth
117
- : validateNonNegativeFinite(
118
- options.strokeWidth,
119
- "createBoxPlot box.strokeWidth"
120
- )
121
- });
125
+ return resolveAppearance(value, DEFAULT_BOX, "box", operation);
122
126
  }
123
127
 
124
128
  export function resolveBoxMedianAppearance(value, operation = "createBoxPlot") {
125
- const options = plainOptions(
126
- value,
127
- ["stroke", "strokeWidth"],
128
- "median",
129
- operation
130
- );
131
- return Object.freeze({
132
- stroke: options.stroke === undefined
133
- ? DEFAULT_MEDIAN.stroke
134
- : validateNonEmptyString(options.stroke, "createBoxPlot median.stroke"),
135
- strokeWidth: options.strokeWidth === undefined
136
- ? DEFAULT_MEDIAN.strokeWidth
137
- : validateNonNegativeFinite(
138
- options.strokeWidth,
139
- "createBoxPlot median.strokeWidth"
140
- )
141
- });
129
+ return resolveAppearance(value, DEFAULT_MEDIAN, "median", operation);
142
130
  }
143
131
 
144
132
  export function resolveBoxOutlierAppearance(value, operation = "createBoxPlot") {
145
- const options = plainOptions(
146
- value,
147
- ["shape", "radius", "opacity"],
148
- "outlier",
149
- operation
150
- );
151
- return Object.freeze({
152
- shape: options.shape === undefined
153
- ? DEFAULT_OUTLIER.shape
154
- : validatePointShape(options.shape),
155
- radius: options.radius === undefined
156
- ? DEFAULT_OUTLIER.radius
157
- : validatePositiveFinite(options.radius, "createBoxPlot outlier.radius"),
158
- opacity: options.opacity === undefined
159
- ? DEFAULT_OUTLIER.opacity
160
- : validateUnitInterval(
161
- options.opacity,
162
- "createBoxPlot outlier.opacity"
163
- )
164
- });
133
+ return resolveAppearance(value, DEFAULT_OUTLIER, "outlier", operation);
165
134
  }
@@ -1,7 +1,7 @@
1
1
  import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
2
2
  import { findLayer, hasLayer } from "../../selectors/layers.js";
3
3
 
4
- const CATEGORY_TYPES = Object.freeze(["nominal", "ordinal", "temporal"]);
4
+ const CATEGORY_TYPES = Object.freeze(["nominal", "ordinal"]);
5
5
 
6
6
  export function resolveBoxOrientation(x, y) {
7
7
  const xType = x?.fieldType ?? "quantitative";
@@ -1,6 +1,7 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import {
3
3
  applyFacadeGuides,
4
+ inferFacadeFieldType,
4
5
  normalizeAppearance,
5
6
  normalizeEncoding,
6
7
  normalizeFieldEncoding,
@@ -35,6 +36,8 @@ export const createBarPlot = action(
35
36
  const bar = normalizeAppearance(args.bar, BAR_OPTIONS, "createBarPlot bar");
36
37
  const x = normalizeFieldEncoding(args.x, "createBarPlot x");
37
38
  const y = normalizeFieldEncoding(args.y, "createBarPlot y");
39
+ x.fieldType = inferFacadeFieldType(this, data, x, "createBarPlot x");
40
+ y.fieldType = inferFacadeFieldType(this, data, y, "createBarPlot y");
38
41
  const color = normalizeEncoding(args.color, "createBarPlot color");
39
42
  const width = normalizeTargetOptions(args.width, "createBarPlot width");
40
43
  const guides = normalizeGuides(args.guides, "createBarPlot");
@@ -17,41 +17,43 @@ import {
17
17
  validateFacadeOptions
18
18
  } from "./shared.js";
19
19
 
20
- const OPTIONS = Object.freeze([
20
+ const OPERATION = "createHeatmap";
21
+ const OPTIONS = [
21
22
  "id", "data", "coordinate", "x", "y", "bin", "color", "rect", "guides"
22
- ]);
23
- const RECT_OPTIONS = Object.freeze([
23
+ ];
24
+ const RECT_OPTIONS = [
24
25
  "opacity", "stroke", "strokeWidth"
25
- ]);
26
- const BIN_OPTIONS = Object.freeze([
26
+ ];
27
+ const BIN_OPTIONS = [
27
28
  "bins", "extent", "includeEmpty"
28
- ]);
29
- const BINNED_POSITION_OPTIONS = Object.freeze([
29
+ ];
30
+ const BINNED_POSITION_OPTIONS = [
30
31
  "field", "fieldType", "scale"
31
- ]);
32
- const BINNED_COLOR_OPTIONS = Object.freeze([
32
+ ];
33
+ const BINNED_COLOR_OPTIONS = [
33
34
  "scale", "palette"
34
- ]);
35
+ ];
35
36
 
36
37
  function normalizeBin(value) {
37
38
  if (!isPlainObject(value)) {
38
- throw new TypeError("createHeatmap bin must be a plain object.");
39
+ throw new TypeError(`${OPERATION} bin must be a plain object.`);
39
40
  }
40
- validateOptionObject(value, BIN_OPTIONS, "createHeatmap bin");
41
+ validateOptionObject(value, BIN_OPTIONS, `${OPERATION} bin`);
41
42
  return {
42
43
  ...value,
43
44
  includeEmpty: value.includeEmpty ?? true
44
45
  };
45
46
  }
46
47
 
47
- function normalizeBinnedPosition(value, channel) {
48
- const label = `createHeatmap ${channel}`;
48
+ function normalizePosition(value, channel, categorical = false) {
49
+ const label = `${OPERATION} ${channel}`;
49
50
  const encoding = normalizeFieldEncoding(value, label);
50
51
  validateOptionObject(encoding, BINNED_POSITION_OPTIONS, label);
51
52
  const field = validateNonEmptyString(encoding.field, `${label} field`);
52
- const fieldType = encoding.fieldType ?? "quantitative";
53
- if (fieldType !== "quantitative") {
54
- throw new Error(`${label} requires a quantitative field.`);
53
+ const types = categorical ? ["nominal", "ordinal"] : ["quantitative"];
54
+ const fieldType = encoding.fieldType ?? types[0];
55
+ if (!types.includes(fieldType)) {
56
+ throw new Error(`${label} requires a ${categorical ? "categorical" : "quantitative"} field.`);
55
57
  }
56
58
  if (encoding.scale !== undefined && !isPlainObject(encoding.scale)) {
57
59
  throw new TypeError(`${label} scale must be a plain object.`);
@@ -62,9 +64,9 @@ function normalizeBinnedPosition(value, channel) {
62
64
  function normalizeBinnedColor(value) {
63
65
  if (value === undefined) return {};
64
66
  if (!isPlainObject(value)) {
65
- throw new TypeError("createHeatmap binned color must be a plain object.");
67
+ throw new TypeError(`${OPERATION} binned color must be a plain object.`);
66
68
  }
67
- validateOptionObject(value, BINNED_COLOR_OPTIONS, "createHeatmap binned color");
69
+ validateOptionObject(value, BINNED_COLOR_OPTIONS, `${OPERATION} binned color`);
68
70
  return { ...value };
69
71
  }
70
72
 
@@ -76,7 +78,7 @@ function resolvedPosition(encoding, field, extent) {
76
78
  scale: {
77
79
  type: "linear",
78
80
  nice: false,
79
- zero: false,
81
+ ...(scale.type !== "log" && { zero: false }),
80
82
  ...scale,
81
83
  domain: scale.domain === undefined || scale.domain === "auto"
82
84
  ? extent
@@ -88,10 +90,10 @@ function resolvedPosition(encoding, field, extent) {
88
90
  function axisWithTitle(value, text) {
89
91
  if (value === false) return false;
90
92
  if (value !== undefined && !isPlainObject(value)) {
91
- throw new TypeError("createHeatmap binned axis must be false or a plain object.");
93
+ throw new TypeError(`${OPERATION} binned axis must be false or a plain object.`);
92
94
  }
93
95
  if (value?.title !== undefined && !isPlainObject(value.title)) {
94
- throw new TypeError("createHeatmap binned axis title must be a plain object.");
96
+ throw new TypeError(`${OPERATION} binned axis title must be a plain object.`);
95
97
  }
96
98
  return {
97
99
  ...value,
@@ -111,7 +113,7 @@ function binnedGuides(guides, xTitle, yTitle) {
111
113
  !isPlainObject(guides[key])
112
114
  ) {
113
115
  throw new TypeError(
114
- `createHeatmap guides ${key} must be false or a plain object.`
116
+ `${OPERATION} guides ${key} must be false or a plain object.`
115
117
  );
116
118
  }
117
119
  }
@@ -133,73 +135,61 @@ function binnedGuides(guides, xTitle, yTitle) {
133
135
  };
134
136
  }
135
137
 
136
- function createPreGriddedHeatmap(program, args, { id, data, rect, guides }) {
137
- const x = normalizeFieldEncoding(args.x, "createHeatmap x");
138
- const y = normalizeFieldEncoding(args.y, "createHeatmap y");
139
- const color = normalizeFieldEncoding(args.color, "createHeatmap color");
140
- const next = program
141
- .createRectMark({ id, data, ...rect })
142
- .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
143
- .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }))
144
- .encodeColor(targetArgs(color, id));
145
- return applyFacadeGuides(next, guides);
146
- }
147
-
148
- function createBinnedHeatmap(program, args, { id, data, rect, guides }) {
149
- const x = normalizeBinnedPosition(args.x, "x");
150
- const y = normalizeBinnedPosition(args.y, "y");
151
- const bin = normalizeBin(args.bin);
152
- const color = normalizeBinnedColor(args.color);
153
- const resolvedGuides = binnedGuides(guides, x.field, y.field);
154
- const generatedData = `${id}Bin2DData`;
155
- const binned = program.createBin2DData({
156
- id: generatedData,
157
- source: data,
158
- x: x.field,
159
- y: y.field,
160
- ...bin
161
- });
162
- const dataset = requireDataset(binned, generatedData);
163
- const transform = dataset.transform[0];
164
- const resolved = transform.resolved;
165
- const xEncoding = resolvedPosition(x, transform.as.x0, resolved.extent.x);
166
- const yEncoding = resolvedPosition(y, transform.as.y0, resolved.extent.y);
167
-
168
- const next = binned
169
- .createRectMark({ id, data: generatedData, ...rect })
170
- .encodeX(positionArgs(xEncoding, { target: id, coordinate: args.coordinate }))
171
- .encodeX2({ target: id, field: transform.as.x1, fieldType: "quantitative" })
172
- .encodeY(positionArgs(yEncoding, { target: id, coordinate: args.coordinate }))
173
- .encodeY2({ target: id, field: transform.as.y1, fieldType: "quantitative" })
174
- .encodeColor({
175
- target: id,
176
- field: transform.as.count,
177
- fieldType: "quantitative",
178
- ...color
179
- });
180
- return applyFacadeGuides(
181
- next,
182
- resolvedGuides
183
- );
184
- }
185
-
186
138
  export const createHeatmap = action(
187
139
  {
188
- op: "createHeatmap",
140
+ op: OPERATION,
189
141
  description: "Create a pre-gridded or rectangularly binned heatmap."
190
142
  },
191
143
  function (args = {}) {
192
- validateFacadeOptions(args, OPTIONS, "createHeatmap");
144
+ validateFacadeOptions(args, OPTIONS, OPERATION);
193
145
  const id = resolveFacadeId(this, args.id, {
194
146
  defaultId: "heatmap",
195
- operation: "createHeatmap"
147
+ operation: OPERATION
148
+ });
149
+ const data = resolveFacadeData(this, args.data, OPERATION);
150
+ const rect = normalizeAppearance(args.rect, RECT_OPTIONS, `${OPERATION} rect`);
151
+ const guides = normalizeGuides(args.guides, OPERATION);
152
+ if (args.bin === undefined) {
153
+ const x = normalizePosition(args.x, "x", true);
154
+ const y = normalizePosition(args.y, "y", true);
155
+ const color = normalizeFieldEncoding(args.color, `${OPERATION} color`);
156
+ const next = this
157
+ .createRectMark({ id, data, ...rect })
158
+ .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
159
+ .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }))
160
+ .encodeColor(targetArgs(color, id));
161
+ return applyFacadeGuides(next, guides);
162
+ }
163
+
164
+ const x = normalizePosition(args.x, "x");
165
+ const y = normalizePosition(args.y, "y");
166
+ const bin = normalizeBin(args.bin);
167
+ const color = normalizeBinnedColor(args.color);
168
+ const resolvedGuides = binnedGuides(guides, x.field, y.field);
169
+ const generatedData = `${id}Bin2DData`;
170
+ const binned = this.createBin2DData({
171
+ id: generatedData,
172
+ source: data,
173
+ x: x.field,
174
+ y: y.field,
175
+ ...bin
196
176
  });
197
- const data = resolveFacadeData(this, args.data, "createHeatmap");
198
- const rect = normalizeAppearance(args.rect, RECT_OPTIONS, "createHeatmap rect");
199
- const guides = normalizeGuides(args.guides, "createHeatmap");
200
- const shared = { id, data, rect, guides };
201
- return args.bin === undefined
202
- ? createPreGriddedHeatmap(this, args, shared)
203
- : createBinnedHeatmap(this, args, shared);
177
+ const transform = requireDataset(binned, generatedData).transform[0];
178
+ const resolved = transform.resolved;
179
+ const xEncoding = resolvedPosition(x, transform.as.x0, resolved.extent.x);
180
+ const yEncoding = resolvedPosition(y, transform.as.y0, resolved.extent.y);
181
+ const next = binned
182
+ .createRectMark({ id, data: generatedData, ...rect })
183
+ .encodeX(positionArgs(xEncoding, { target: id, coordinate: args.coordinate }))
184
+ .encodeX2({ target: id, field: transform.as.x1, fieldType: "quantitative" })
185
+ .encodeY(positionArgs(yEncoding, { target: id, coordinate: args.coordinate }))
186
+ .encodeY2({ target: id, field: transform.as.y1, fieldType: "quantitative" })
187
+ .encodeColor({
188
+ target: id,
189
+ field: transform.as.count,
190
+ fieldType: "quantitative",
191
+ ...color
192
+ });
193
+ return applyFacadeGuides(next, resolvedGuides);
204
194
  }
205
195
  );
@@ -1,5 +1,5 @@
1
- import { createBarPlot } from "./bar.js";
2
1
  import { createHeatmap } from "./heatmap.js";
2
+ import { createBarPlot } from "./bar.js";
3
3
  import { createHistogram } from "./histogram.js";
4
4
  import { createLinePlot } from "./line.js";
5
5
  import { createScatterPlot } from "./scatter.js";
@@ -20,6 +20,37 @@ const LINE_OPTIONS = Object.freeze([
20
20
  "strokeWidth", "stroke", "opacity", "curve", "closed"
21
21
  ]);
22
22
 
23
+ function scopeParallelGuides(guides, coordinate) {
24
+ if (guides === false) return guides;
25
+ if (guides.grid !== undefined && guides.grid !== false) {
26
+ throw new Error("createParallelCoordinates guides do not accept grid options.");
27
+ }
28
+ if (guides.axes === false) return guides;
29
+ const axes = guides.axes ?? {};
30
+ const descriptor = axes.coordinate ?? {};
31
+ if (
32
+ descriptor.id !== undefined && descriptor.id !== coordinate ||
33
+ descriptor.type !== undefined &&
34
+ descriptor.type !== "auto" && descriptor.type !== "parallel"
35
+ ) {
36
+ throw new Error(
37
+ "createParallelCoordinates guides must use the chart's Parallel coordinate."
38
+ );
39
+ }
40
+ if (["x", "y", "theta", "radius"].some(channel => axes[channel] !== undefined)) {
41
+ throw new Error(
42
+ "createParallelCoordinates guides do not accept Cartesian or Polar axis channels."
43
+ );
44
+ }
45
+ return {
46
+ ...guides,
47
+ axes: {
48
+ ...axes,
49
+ coordinate: { ...descriptor, id: coordinate, type: "parallel" }
50
+ }
51
+ };
52
+ }
53
+
23
54
  export const createParallelCoordinates = action(
24
55
  {
25
56
  op: "createParallelCoordinates",
@@ -55,7 +86,10 @@ export const createParallelCoordinates = action(
55
86
  args.strokeDash,
56
87
  "createParallelCoordinates strokeDash"
57
88
  );
58
- const guides = normalizeGuides(args.guides, "createParallelCoordinates");
89
+ const guides = scopeParallelGuides(
90
+ normalizeGuides(args.guides, "createParallelCoordinates"),
91
+ coordinate
92
+ );
59
93
 
60
94
  let next = this
61
95
  .createCoordinate({ id: coordinate, type: "parallel" })
@@ -2,6 +2,7 @@ import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateOptionObject } from "../../core/validation.js";
4
4
  import { findDataset } from "../../selectors/datasets.js";
5
+ import { isNominalValue } from "../../grammar/scales/fields.js";
5
6
  import { hasLayer } from "../../selectors/layers.js";
6
7
 
7
8
  export function validateFacadeOptions(args, supported, operation) {
@@ -62,6 +63,25 @@ export function normalizeEncoding(value, label) {
62
63
  return normalizeFieldEncoding(value, label);
63
64
  }
64
65
 
66
+ export function inferFacadeFieldType(program, data, encoding, label) {
67
+ if (encoding.fieldType !== undefined) return encoding.fieldType;
68
+ if (typeof encoding.field !== "string" || encoding.field.length === 0) {
69
+ throw new TypeError(`${label} field must be a non-empty string.`);
70
+ }
71
+ const dataset = findDataset(program, data);
72
+ const values = dataset.values
73
+ .map(row => row?.[encoding.field])
74
+ .filter(value => value !== undefined && value !== null && value !== "");
75
+ if (values.length === 0) {
76
+ throw new Error(`${label} field "${encoding.field}" has no values.`);
77
+ }
78
+ if (values.every(Number.isFinite)) return "quantitative";
79
+ if (values.every(isNominalValue)) return "nominal";
80
+ throw new Error(
81
+ `${label} field "${encoding.field}" requires an explicit fieldType.`
82
+ );
83
+ }
84
+
65
85
  export function normalizeStrokeDashEncoding(
66
86
  value,
67
87
  label = "createLinePlot strokeDash"
@@ -21,6 +21,10 @@ const LAYOUT_EDIT_OPTIONS = Object.freeze([
21
21
  ]);
22
22
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
23
23
 
24
+ function option(args, name, fallback) {
25
+ return Object.hasOwn(args, name) ? args[name] : fallback;
26
+ }
27
+
24
28
  function normalizeChildEntry(entry, index) {
25
29
  const fallbackId = `view-${index + 1}`;
26
30
  if (entry instanceof CoreChartProgram) {
@@ -64,7 +68,11 @@ function childDescriptor({ id, program }) {
64
68
  }
65
69
 
66
70
  function normalizeCompositionInput(args, direction) {
67
- validateOptionObject(args, CONCAT_OPTIONS, direction === "horizontal" ? "hconcat" : "vconcat");
71
+ validateOptionObject(
72
+ args,
73
+ CONCAT_OPTIONS,
74
+ direction === "horizontal" ? "hconcat" : "vconcat"
75
+ );
68
76
  if (!Array.isArray(args.programs) || args.programs.length < 2) {
69
77
  throw new TypeError("Composition requires at least two programs.");
70
78
  }
@@ -98,6 +106,12 @@ function normalizeCompositionInput(args, direction) {
98
106
  };
99
107
  }
100
108
 
109
+ export function applyCompositionState(program, state, tracedChildren = []) {
110
+ let next = program._withCompositionState(state);
111
+ for (const id of tracedChildren) next = next.useProgram({ id });
112
+ return next.materializeComposition();
113
+ }
114
+
101
115
  const useProgram = action(
102
116
  {
103
117
  op: "useProgram",
@@ -136,11 +150,7 @@ function concatAction(direction, op) {
136
150
  },
137
151
  function (args = {}) {
138
152
  const state = normalizeCompositionInput(args, direction);
139
- let next = this._withCompositionState(state);
140
- for (const id of state.compositionSpec.children) {
141
- next = next.useProgram({ id });
142
- }
143
- return next.materializeComposition();
153
+ return applyCompositionState(this, state, state.compositionSpec.children);
144
154
  }
145
155
  );
146
156
  }
@@ -160,60 +170,49 @@ const editCompositionLayout = action(
160
170
  throw new TypeError("editCompositionLayout requires at least one layout option.");
161
171
  }
162
172
  const current = this.compositionSpec;
173
+ if (current.type !== "facet" && Object.hasOwn(args, "columns")) {
174
+ throw new Error(
175
+ "editCompositionLayout columns is available only on a facet composition."
176
+ );
177
+ }
178
+ const padding = Object.hasOwn(args, "padding")
179
+ ? normalizeCompositionPadding(args.padding, current.padding)
180
+ : current.padding;
181
+ let layout;
163
182
  if (current.type === "facet") {
164
- const layout = resolveFacetLayout({
183
+ layout = resolveFacetLayout({
165
184
  children: current.children.map((id, index) => ({
166
185
  ...childDescriptor({ id, program: this.children[id] }),
167
186
  value: current.facet.values[index]
168
187
  })),
169
- columns: Object.hasOwn(args, "columns")
170
- ? args.columns
171
- : current.columns,
172
- gap: Object.hasOwn(args, "gap") ? args.gap : current.gap,
173
- align: Object.hasOwn(args, "align") ? args.align : current.align,
174
- padding: Object.hasOwn(args, "padding")
175
- ? normalizeCompositionPadding(args.padding, current.padding)
176
- : current.padding,
188
+ columns: option(args, "columns", current.columns),
189
+ gap: option(args, "gap", current.gap),
190
+ align: option(args, "align", current.align),
191
+ padding,
177
192
  sharedLegend: current.facet.guides.legend === "shared"
178
193
  });
179
- return this._withCompositionState({
180
- children: this.children,
181
- compositionSpec: {
182
- ...current,
183
- columns: layout.columns,
184
- gap: layout.gap,
185
- align: layout.align,
186
- padding: layout.padding
187
- }
188
- }).materializeComposition();
189
- }
190
- if (Object.hasOwn(args, "columns")) {
191
- throw new Error(
192
- "editCompositionLayout columns is available only on a facet composition."
193
- );
194
+ } else {
195
+ layout = resolveCompositionLayout({
196
+ direction: current.direction,
197
+ children: current.children.map(id => childDescriptor({
198
+ id,
199
+ program: this.children[id]
200
+ })),
201
+ gap: option(args, "gap", current.gap),
202
+ align: option(args, "align", current.align),
203
+ padding
204
+ });
194
205
  }
195
- const padding = Object.hasOwn(args, "padding")
196
- ? normalizeCompositionPadding(args.padding, current.padding)
197
- : current.padding;
198
- const layout = resolveCompositionLayout({
199
- direction: current.direction,
200
- children: current.children.map(id => childDescriptor({
201
- id,
202
- program: this.children[id]
203
- })),
204
- gap: Object.hasOwn(args, "gap") ? args.gap : current.gap,
205
- align: Object.hasOwn(args, "align") ? args.align : current.align,
206
- padding
207
- });
208
- return this._withCompositionState({
206
+ return applyCompositionState(this, {
209
207
  children: this.children,
210
208
  compositionSpec: {
211
209
  ...current,
210
+ ...(current.type === "facet" ? { columns: layout.columns } : {}),
212
211
  gap: layout.gap,
213
212
  align: layout.align,
214
213
  padding: layout.padding
215
214
  }
216
- }).materializeComposition();
215
+ });
217
216
  }
218
217
  );
219
218
 
@@ -240,12 +239,10 @@ const replaceCompositionChild = action(
240
239
  ...this.children,
241
240
  [target]: args.program
242
241
  });
243
- return this._withCompositionState({
242
+ return applyCompositionState(this, {
244
243
  children,
245
244
  compositionSpec: this.compositionSpec
246
- })
247
- .useProgram({ id: target })
248
- .materializeComposition();
245
+ }, [target]);
249
246
  }
250
247
  );
251
248