ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -0,0 +1,109 @@
1
+ import { isPlainObject } from "../../../core/immutable.js";
2
+ import { findSemanticScale } from "../../../selectors/scales.js";
3
+
4
+ export function requireDensityField(value, label) {
5
+ if (typeof value !== "string" || value.length === 0) {
6
+ throw new TypeError(`${label} must be a non-empty string.`);
7
+ }
8
+ return value;
9
+ }
10
+
11
+ export function resolveDensityScaleOptions(value, defaults, label) {
12
+ if (value !== undefined && !isPlainObject(value)) {
13
+ throw new TypeError(`${label} must be a plain object.`);
14
+ }
15
+ return { ...defaults, ...(value ?? {}) };
16
+ }
17
+
18
+ export function resolveDensityCategoryScaleOptions(value, fallbackId) {
19
+ const scale = resolveDensityScaleOptions(
20
+ value,
21
+ { type: "band" },
22
+ "Density placement scale"
23
+ );
24
+ if ((scale.type ?? "band") !== "band") {
25
+ throw new Error('Density category placement requires scale type "band".');
26
+ }
27
+ return {
28
+ ...(fallbackId === undefined ? {} : { id: fallbackId }),
29
+ ...scale,
30
+ type: "band"
31
+ };
32
+ }
33
+
34
+ function valueScaleForTransition(program, layer, transform) {
35
+ const densityChannel = transform.placement?.channel ?? (
36
+ layer.encoding.x.field === transform.as[1] ? "x" : "y"
37
+ );
38
+ const valueChannel = densityChannel === "x" ? "y" : "x";
39
+ return findSemanticScale(program, layer.encoding[valueChannel].scale);
40
+ }
41
+
42
+ export function resolveDensityTransitionScaleDefinitions(
43
+ program,
44
+ layer,
45
+ transform,
46
+ placement,
47
+ rawPlacement
48
+ ) {
49
+ const valueScale = valueScaleForTransition(program, layer, transform);
50
+ if (valueScale === undefined) {
51
+ throw new Error(`Density area "${layer.id}" requires its value scale.`);
52
+ }
53
+ const { id: _valueId, range: _valueRange, ...valueDefinition } = valueScale;
54
+ void _valueId;
55
+ void _valueRange;
56
+ const valueChannel = placement?.channel === "x" ? "y" : "x";
57
+ const companionChannel = valueChannel === "x" ? "y" : "x";
58
+ const companionDefinition = placement === undefined
59
+ ? { type: "linear", domain: "auto", range: "auto", nice: true, zero: true }
60
+ : resolveDensityCategoryScaleOptions(rawPlacement.scale);
61
+ return {
62
+ [valueChannel]: { ...valueDefinition, range: "auto" },
63
+ [companionChannel]: companionDefinition
64
+ };
65
+ }
66
+
67
+ export function resolveDensityPositionDefinition({
68
+ layer,
69
+ output,
70
+ groupBy,
71
+ placement,
72
+ coordinate,
73
+ valueScale,
74
+ densityScale,
75
+ placementScale
76
+ }) {
77
+ if (placement === undefined) {
78
+ const xIsDensity = layer.densityChannel === "x";
79
+ return {
80
+ x: {
81
+ field: output[xIsDensity ? 1 : 0],
82
+ fieldType: "quantitative",
83
+ scale: xIsDensity ? densityScale : valueScale,
84
+ coordinate
85
+ },
86
+ y: {
87
+ field: output[xIsDensity ? 0 : 1],
88
+ fieldType: "quantitative",
89
+ scale: xIsDensity ? valueScale : densityScale,
90
+ coordinate
91
+ }
92
+ };
93
+ }
94
+ const category = {
95
+ field: placement.categoryField,
96
+ fieldType: "nominal",
97
+ scale: placementScale,
98
+ coordinate
99
+ };
100
+ const value = {
101
+ field: output[0],
102
+ fieldType: "quantitative",
103
+ scale: valueScale,
104
+ coordinate
105
+ };
106
+ return placement.channel === "x"
107
+ ? { x: category, y: value }
108
+ : { x: value, y: category };
109
+ }
@@ -1,6 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
- import { isPlainObject } from "../../core/immutable.js";
4
3
  import {
5
4
  findDataset,
6
5
  findLayer,
@@ -11,15 +10,28 @@ import { applyMaterializationPlan } from
11
10
  "../../materialization/dependencies.js";
12
11
  import { planDensityRematerialization } from
13
12
  "../../materialization/density.js";
13
+ import { planDerivedDataRevision } from
14
+ "../../materialization/dataProvenance.js";
14
15
  import { validateOptions } from "./shared.js";
16
+ import { normalizeDensityPlacement } from "../../grammar/density.js";
17
+ import { scaleEditPatch } from "../scales/patch.js";
18
+ import { removeLegendKinds } from "../guides/legends/remove.js";
19
+ import {
20
+ requireDensityField,
21
+ resolveDensityCategoryScaleOptions,
22
+ resolveDensityPositionDefinition,
23
+ resolveDensityScaleOptions,
24
+ resolveDensityTransitionScaleDefinitions
25
+ } from "./density/resolve.js";
15
26
 
16
27
  const OPTIONS = Object.freeze([
17
28
  "field", "target", "source", "groupBy", "bandwidth", "extent", "steps",
18
29
  "kernel", "normalization", "as", "densityChannel", "coordinate",
19
- "valueScale", "densityScale"
30
+ "valueScale", "densityScale", "placement"
20
31
  ]);
21
32
  const EDIT_OPTIONS = Object.freeze([
22
- "target", "bandwidth", "extent", "steps", "kernel", "normalization"
33
+ "target", "source", "field", "groupBy", "bandwidth", "extent", "steps",
34
+ "kernel", "normalization", "placement"
23
35
  ]);
24
36
  const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
25
37
 
@@ -42,32 +54,35 @@ function findArea(program, requested) {
42
54
  throw new Error("encodeDensity target is ambiguous; provide target.");
43
55
  }
44
56
 
45
- function requireField(value, label) {
46
- if (typeof value !== "string" || value.length === 0) {
47
- throw new TypeError(`${label} must be a non-empty string.`);
48
- }
49
- return value;
50
- }
51
-
52
- function scaleOptions(value, defaults, label) {
53
- if (value !== undefined && !isPlainObject(value)) {
54
- throw new TypeError(`${label} must be a plain object.`);
57
+ function applyDensityPosition(program, layerId, definition, groupBy) {
58
+ const withCoordinate = value => ({
59
+ target: layerId,
60
+ field: value.field,
61
+ fieldType: value.fieldType,
62
+ scale: value.scale,
63
+ ...(value.coordinate === undefined ? {} : { coordinate: value.coordinate })
64
+ });
65
+ let next = program
66
+ .encodeX(withCoordinate(definition.x))
67
+ .encodeY(withCoordinate(definition.y));
68
+ if (groupBy !== undefined) {
69
+ next = next.encodeGroup({ target: layerId, field: groupBy });
55
70
  }
56
- return { ...defaults, ...(value ?? {}) };
71
+ return next;
57
72
  }
58
73
 
59
74
  const encodeDensity = action(
60
75
  {
61
76
  op: "encodeDensity",
62
- description: "Derive and encode a baseline-oriented kernel density area."
77
+ description: "Derive and encode a baseline or categorical kernel-density area."
63
78
  },
64
79
  function (args = {}) {
65
80
  validateOptions(args, OPTIONS, "encodeDensity");
66
81
  const layer = findArea(this, args.target);
67
- const field = requireField(args.field, "Density field");
82
+ const field = requireDensityField(args.field, "Density field");
68
83
  const groupBy = args.groupBy === undefined
69
84
  ? undefined
70
- : requireField(args.groupBy, "Density groupBy");
85
+ : requireDensityField(args.groupBy, "Density groupBy");
71
86
  const source = validateUserId(
72
87
  args.source ?? layer.data,
73
88
  "Density source dataset id"
@@ -82,7 +97,8 @@ const encodeDensity = action(
82
97
  `Density area target "${layer.id}" already has positional or group encodings.`
83
98
  );
84
99
  }
85
- const densityChannel = args.densityChannel ?? "y";
100
+ const categoryPlacement = args.placement?.type === "category";
101
+ const densityChannel = args.densityChannel ?? (categoryPlacement ? "x" : "y");
86
102
  if (!["x", "y"].includes(densityChannel)) {
87
103
  throw new Error(`Unsupported densityChannel "${densityChannel}".`);
88
104
  }
@@ -94,56 +110,67 @@ const encodeDensity = action(
94
110
  if (hasDataset(this, derivedId)) {
95
111
  throw new Error(`Dataset "${derivedId}" already exists.`);
96
112
  }
97
- const valueScale = scaleOptions(
113
+ const valueScale = resolveDensityScaleOptions(
98
114
  args.valueScale,
99
115
  { nice: false, zero: false },
100
116
  "Density valueScale"
101
117
  );
102
- const densityScale = scaleOptions(
103
- args.densityScale,
104
- { nice: true, zero: true },
105
- "Density densityScale"
106
- );
107
- const xIsDensity = densityChannel === "x";
108
- const xField = output[xIsDensity ? 1 : 0];
109
- const yField = output[xIsDensity ? 0 : 1];
110
- const xScale = xIsDensity ? densityScale : valueScale;
111
- const yScale = xIsDensity ? valueScale : densityScale;
118
+ const placement = args.placement === undefined
119
+ ? undefined
120
+ : normalizeDensityPlacement(args.placement, {
121
+ densityChannel,
122
+ groupBy,
123
+ categoryField: groupBy ?? `${layer.id}DensityCategory`
124
+ });
125
+ if (placement !== undefined && args.densityScale !== undefined) {
126
+ throw new Error(
127
+ "Density category placement cannot be combined with densityScale."
128
+ );
129
+ }
130
+ const densityScale = placement === undefined
131
+ ? resolveDensityScaleOptions(
132
+ args.densityScale,
133
+ { nice: true, zero: true },
134
+ "Density densityScale"
135
+ )
136
+ : undefined;
137
+ const placementScale = placement === undefined
138
+ ? undefined
139
+ : resolveDensityCategoryScaleOptions(args.placement.scale);
112
140
 
113
- let next = this
114
- .createDensityData({
115
- id: derivedId,
116
- source,
117
- field,
118
- ...(groupBy === undefined ? {} : { groupBy }),
119
- ...(args.bandwidth === undefined ? {} : { bandwidth: args.bandwidth }),
120
- ...(args.extent === undefined ? {} : { extent: args.extent }),
121
- ...(args.steps === undefined ? {} : { steps: args.steps }),
122
- ...(args.kernel === undefined ? {} : { kernel: args.kernel }),
123
- ...(args.normalization === undefined
124
- ? {}
125
- : { normalization: args.normalization }),
126
- ...(args.as === undefined ? {} : { as: args.as })
127
- })
141
+ const dataArgs = {
142
+ id: derivedId,
143
+ source,
144
+ field,
145
+ ...(groupBy === undefined ? {} : { groupBy }),
146
+ ...(args.bandwidth === undefined ? {} : { bandwidth: args.bandwidth }),
147
+ ...(args.extent === undefined ? {} : { extent: args.extent }),
148
+ ...(args.steps === undefined ? {} : { steps: args.steps }),
149
+ ...(args.kernel === undefined ? {} : { kernel: args.kernel }),
150
+ ...(args.normalization === undefined
151
+ ? {}
152
+ : { normalization: args.normalization }),
153
+ ...(args.as === undefined ? {} : { as: args.as }),
154
+ ...(placement === undefined ? {} : { placement })
155
+ };
156
+ let next = placement === undefined
157
+ ? this.createDensityData(dataArgs)
158
+ : this.createCategoricalDensityData(dataArgs);
159
+ next = next
128
160
  .editSemantic({
129
161
  property: `layer[${layer.id}].data`,
130
162
  value: derivedId
131
- })
132
- .encodeX({
133
- target: layer.id,
134
- field: xField,
135
- ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
136
- scale: xScale
137
- })
138
- .encodeY({
139
- target: layer.id,
140
- field: yField,
141
- ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
142
- scale: yScale
143
163
  });
144
- if (groupBy !== undefined) {
145
- next = next.encodeGroup({ target: layer.id, field: groupBy });
146
- }
164
+ next = applyDensityPosition(next, layer.id, resolveDensityPositionDefinition({
165
+ layer: { densityChannel },
166
+ output,
167
+ groupBy,
168
+ placement,
169
+ coordinate: args.coordinate,
170
+ valueScale,
171
+ densityScale,
172
+ placementScale
173
+ }), groupBy);
147
174
  return next.rematerializeAreaMark({ id: layer.id });
148
175
  }
149
176
  );
@@ -164,12 +191,41 @@ function findDensityArea(program, requested) {
164
191
  });
165
192
  }
166
193
 
167
- function nextDensityRevisionId(program, target) {
168
- let revision = 1;
169
- while (hasDataset(program, `${target}DensityDataRevision${revision}`)) {
170
- revision += 1;
194
+ function densityCategoryField(layer, transform, groupBy) {
195
+ if (transform.placement === undefined) return undefined;
196
+ if (transform.placement.categoryField !== transform.groupBy) {
197
+ return transform.placement.categoryField;
171
198
  }
172
- return `${target}DensityDataRevision${revision}`;
199
+ return groupBy ?? `${layer.id}DensityCategory`;
200
+ }
201
+
202
+ function assertGroupingSelectionCompatibility(program, layer, changesGroup) {
203
+ if (!changesGroup) return;
204
+ const referenced = Object.entries(
205
+ program.materializationConfigs.selections ?? {}
206
+ ).find(([, selection]) =>
207
+ selection.target === layer.id &&
208
+ ["group", "color"].includes(selection.selector?.channel)
209
+ );
210
+ if (referenced !== undefined) {
211
+ throw new Error(
212
+ `editDensity cannot change groupBy while selection "${referenced[0]}" ` +
213
+ `references ${referenced[1].selector.channel}.`
214
+ );
215
+ }
216
+ }
217
+
218
+ function removeOwnedColorLegends(program, target) {
219
+ const kinds = Object.entries(program.guideConfigs.legend ?? {})
220
+ .filter(([kind, config]) =>
221
+ config?.target === target &&
222
+ (
223
+ config.channels?.includes("color") ||
224
+ ["gradient", "interval"].includes(kind)
225
+ )
226
+ )
227
+ .map(([kind]) => kind);
228
+ return kinds.length === 0 ? program : removeLegendKinds(program, kinds);
173
229
  }
174
230
 
175
231
  const editDensity = action(
@@ -185,37 +241,221 @@ const editDensity = action(
185
241
  const layer = findDensityArea(this, args.target);
186
242
  const previous = findDataset(this, layer.data);
187
243
  const transform = previous.transform[0];
188
- const revisionId = nextDensityRevisionId(this, layer.id);
244
+ const source = Object.hasOwn(args, "source")
245
+ ? validateUserId(args.source, "Density source dataset id")
246
+ : previous.source;
247
+ const field = Object.hasOwn(args, "field")
248
+ ? requireDensityField(args.field, "Density field")
249
+ : transform.field;
250
+ const groupBy = Object.hasOwn(args, "groupBy")
251
+ ? args.groupBy === false
252
+ ? undefined
253
+ : requireDensityField(args.groupBy, "Density groupBy")
254
+ : transform.groupBy;
255
+ const changesGroup = groupBy !== transform.groupBy;
256
+ assertGroupingSelectionCompatibility(this, layer, changesGroup);
257
+ const categoryField = densityCategoryField(layer, transform, groupBy);
258
+ const requestedPlacement = Object.hasOwn(args, "placement")
259
+ ? normalizeDensityPlacement(args.placement, {
260
+ densityChannel: transform.placement?.channel ?? "x",
261
+ groupBy,
262
+ categoryField: categoryField ?? groupBy ??
263
+ `${layer.id}DensityCategory`
264
+ })
265
+ : transform.placement === undefined || !changesGroup
266
+ ? transform.placement
267
+ : normalizeDensityPlacement({
268
+ type: "category",
269
+ ...(transform.placement.side === undefined
270
+ ? {}
271
+ : { side: transform.placement.side }),
272
+ width: transform.placement.width,
273
+ ...(transform.placement.split === undefined
274
+ ? {}
275
+ : { split: transform.placement.split })
276
+ }, {
277
+ densityChannel: transform.placement.channel,
278
+ groupBy,
279
+ categoryField
280
+ });
281
+ const colorField = layer.encoding?.color?.field;
282
+ const colorFollowsGroup = transform.groupBy !== undefined &&
283
+ colorField === transform.groupBy;
284
+ const availableColorFields = [
285
+ groupBy,
286
+ requestedPlacement?.split?.field
287
+ ].filter(Boolean);
288
+ if (
289
+ colorField !== undefined &&
290
+ !colorFollowsGroup &&
291
+ !availableColorFields.includes(colorField)
292
+ ) {
293
+ throw new Error(
294
+ `editDensity placement would remove color field "${colorField}" from the density series.`
295
+ );
296
+ }
297
+ const revision = planDerivedDataRevision(this, {
298
+ owner: layer.id,
299
+ role: "DensityData",
300
+ previous: previous.id,
301
+ consumers: [layer.id]
302
+ });
189
303
  const option = property => Object.hasOwn(args, property)
190
304
  ? args[property]
191
305
  : transform[property];
192
306
 
193
- let next = this
194
- .createDensityData({
195
- id: revisionId,
196
- source: previous.source,
197
- field: transform.field,
198
- ...(transform.groupBy === undefined
199
- ? {}
200
- : { groupBy: transform.groupBy }),
201
- bandwidth: option("bandwidth"),
202
- extent: option("extent"),
203
- steps: option("steps"),
204
- kernel: option("kernel") ?? "gaussian",
205
- normalization: option("normalization") ?? "unit",
206
- as: transform.as
207
- })
208
- .editSemantic({
209
- property: `layer[${layer.id}].data`,
210
- value: revisionId
211
- })
212
- .releaseDerivedData({ id: previous.id });
307
+ const dataArgs = {
308
+ id: revision.id,
309
+ source,
310
+ field,
311
+ ...(groupBy === undefined
312
+ ? {}
313
+ : { groupBy }),
314
+ bandwidth: option("bandwidth"),
315
+ extent: option("extent"),
316
+ steps: option("steps"),
317
+ kernel: option("kernel") ?? "gaussian",
318
+ normalization: option("normalization") ?? "unit",
319
+ as: transform.as,
320
+ ...(requestedPlacement === undefined
321
+ ? {}
322
+ : { placement: requestedPlacement })
323
+ };
324
+ const changesPlacementMode = Object.hasOwn(args, "placement") &&
325
+ (transform.placement === undefined) !== (requestedPlacement === undefined);
326
+ const applyEdit = program => {
327
+ let next = requestedPlacement === undefined
328
+ ? program.createDensityData(dataArgs)
329
+ : program.createCategoricalDensityData(dataArgs);
330
+ next = next.rebindLayerData(revision.rebinds[0])
331
+ .releaseDerivedData(revision.release);
213
332
 
214
- next = applyMaterializationPlan(
215
- next,
216
- planDensityRematerialization(next, layer.id)
217
- );
218
- return next;
333
+ if (changesGroup) {
334
+ if (groupBy === undefined) {
335
+ if (findLayer(next, layer.id).encoding?.group !== undefined) {
336
+ next = next.editSemantic({
337
+ property: `layer[${layer.id}].encoding.group`,
338
+ remove: true
339
+ });
340
+ }
341
+ if (colorFollowsGroup) {
342
+ next = next.editSemantic({
343
+ property: `layer[${layer.id}].encoding.color`,
344
+ remove: true
345
+ });
346
+ next = removeOwnedColorLegends(next, layer.id);
347
+ }
348
+ } else {
349
+ next = next
350
+ .editSemantic({
351
+ property: `layer[${layer.id}].encoding.group.field`,
352
+ value: groupBy
353
+ })
354
+ .editSemantic({
355
+ property: `layer[${layer.id}].encoding.group.fieldType`,
356
+ value: "nominal"
357
+ });
358
+ if (colorFollowsGroup) {
359
+ next = next.editSemantic({
360
+ property: `layer[${layer.id}].encoding.color.field`,
361
+ value: groupBy
362
+ });
363
+ }
364
+ }
365
+ if (
366
+ requestedPlacement !== undefined &&
367
+ transform.placement?.categoryField !== requestedPlacement.categoryField
368
+ ) {
369
+ next = next.editSemantic({
370
+ property: `layer[${layer.id}].encoding.${requestedPlacement.channel}.field`,
371
+ value: requestedPlacement.categoryField
372
+ });
373
+ }
374
+ }
375
+
376
+ if (changesPlacementMode) {
377
+ const xScaleId = layer.encoding?.x?.scale;
378
+ const yScaleId = layer.encoding?.y?.scale;
379
+ const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
380
+ program,
381
+ layer,
382
+ transform,
383
+ requestedPlacement,
384
+ args.placement
385
+ );
386
+ next = next
387
+ .editSemantic({
388
+ property: `layer[${layer.id}].encoding.x`,
389
+ remove: true
390
+ })
391
+ .editSemantic({
392
+ property: `layer[${layer.id}].encoding.y`,
393
+ remove: true
394
+ });
395
+ next = next
396
+ .editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
397
+ .editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
398
+ const densityChannel = requestedPlacement?.channel ?? (
399
+ layer.encoding.x.field === transform.as[1] ? "x" : "y"
400
+ );
401
+ const valueChannel = requestedPlacement?.channel === "x" ? "y" : "x";
402
+ const definition = resolveDensityPositionDefinition({
403
+ layer: { densityChannel },
404
+ output: transform.as,
405
+ groupBy,
406
+ placement: requestedPlacement,
407
+ coordinate: layer.coordinate,
408
+ valueScale: {
409
+ id: valueChannel === "y" ? yScaleId : xScaleId,
410
+ ...scaleDefinitions[valueChannel]
411
+ },
412
+ densityScale: requestedPlacement === undefined
413
+ ? {
414
+ id: densityChannel === "x" ? xScaleId : yScaleId,
415
+ ...scaleDefinitions[densityChannel]
416
+ }
417
+ : undefined,
418
+ placementScale: requestedPlacement === undefined
419
+ ? undefined
420
+ : {
421
+ id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
422
+ ...scaleDefinitions[requestedPlacement.channel]
423
+ }
424
+ });
425
+ next = applyDensityPosition(next, layer.id, definition, groupBy);
426
+ } else if (
427
+ requestedPlacement !== undefined &&
428
+ Object.hasOwn(args.placement ?? {}, "scale")
429
+ ) {
430
+ const channel = requestedPlacement.channel;
431
+ const scaleId = layer.encoding[channel].scale;
432
+ const requestedScale = resolveDensityCategoryScaleOptions(
433
+ args.placement.scale,
434
+ scaleId
435
+ );
436
+ if (requestedScale.id !== scaleId) {
437
+ throw new Error("editDensity placement scale cannot change its id.");
438
+ }
439
+ next = next.editScale(scaleEditPatch(scaleId, requestedScale));
440
+ }
441
+
442
+ next = applyMaterializationPlan(
443
+ next,
444
+ planDensityRematerialization(next, layer.id)
445
+ );
446
+ if (
447
+ changesGroup &&
448
+ colorFollowsGroup &&
449
+ groupBy !== undefined &&
450
+ Object.values(next.guideConfigs.legend ?? {})
451
+ .some(config => config?.target === layer.id)
452
+ ) {
453
+ next = next.rematerializeLegend();
454
+ }
455
+ return next;
456
+ };
457
+ applyEdit(this);
458
+ return applyEdit(this);
219
459
  }
220
460
  );
221
461