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
@@ -1,5 +1,9 @@
1
1
  import { createData } from "./create.js";
2
- import { createDerivedData, releaseDerivedData } from "./derived.js";
2
+ import {
3
+ createDerivedData,
4
+ rebindLayerData,
5
+ releaseDerivedData
6
+ } from "./derived.js";
3
7
  import {
4
8
  filterData,
5
9
  filterMarks,
@@ -7,24 +11,52 @@ import {
7
11
  materializeMarkFilteredData
8
12
  } from "./filter.js";
9
13
  import { createRegressionData, materializeRegressionData } from "./regression.js";
10
- import { createDensityData, materializeDensityData } from "./density.js";
14
+ import {
15
+ createCategoricalDensityData,
16
+ createDensityData,
17
+ materializeDensityData
18
+ } from "./density.js";
19
+ import {
20
+ createGradientProfileData,
21
+ materializeGradientProfileData
22
+ } from "./gradientProfile.js";
11
23
  import { createIntervalData, materializeIntervalData } from "./interval.js";
24
+ import { createHorizonData, materializeHorizonData } from "./horizon.js";
25
+ import { createWindowData, materializeWindowData } from "./window.js";
26
+ import {
27
+ createBin2DData,
28
+ editBin2DData,
29
+ materializeBin2DData
30
+ } from "./bin2d.js";
12
31
  import { createBoxSummaryData, createBoxOutlierData, materializeBoxSummaryData, materializeBoxOutlierData } from "./box.js";
13
32
 
14
33
  export function registerDataActions(ProgramClass) {
15
34
  ProgramClass.prototype.createData = createData;
16
35
  ProgramClass.prototype.createDerivedData = createDerivedData;
17
36
  ProgramClass.prototype.releaseDerivedData = releaseDerivedData;
37
+ ProgramClass.prototype.rebindLayerData = rebindLayerData;
18
38
  ProgramClass.prototype.createDensityData = createDensityData;
39
+ ProgramClass.prototype.createCategoricalDensityData =
40
+ createCategoricalDensityData;
19
41
  ProgramClass.prototype.materializeFilteredData = materializeFilteredData;
20
42
  ProgramClass.prototype.materializeMarkFilteredData = materializeMarkFilteredData;
21
43
  ProgramClass.prototype.filterData = filterData;
22
44
  ProgramClass.prototype.filterMarks = filterMarks;
23
45
  ProgramClass.prototype.materializeRegressionData = materializeRegressionData;
24
46
  ProgramClass.prototype.materializeDensityData = materializeDensityData;
47
+ ProgramClass.prototype.createGradientProfileData = createGradientProfileData;
48
+ ProgramClass.prototype.materializeGradientProfileData =
49
+ materializeGradientProfileData;
25
50
  ProgramClass.prototype.createRegressionData = createRegressionData;
26
51
  ProgramClass.prototype.materializeIntervalData = materializeIntervalData;
27
52
  ProgramClass.prototype.createIntervalData = createIntervalData;
53
+ ProgramClass.prototype.createHorizonData = createHorizonData;
54
+ ProgramClass.prototype.materializeHorizonData = materializeHorizonData;
55
+ ProgramClass.prototype.createWindowData = createWindowData;
56
+ ProgramClass.prototype.materializeWindowData = materializeWindowData;
57
+ ProgramClass.prototype.createBin2DData = createBin2DData;
58
+ ProgramClass.prototype.editBin2DData = editBin2DData;
59
+ ProgramClass.prototype.materializeBin2DData = materializeBin2DData;
28
60
  ProgramClass.prototype.createBoxSummaryData = createBoxSummaryData;
29
61
  ProgramClass.prototype.createBoxOutlierData = createBoxOutlierData;
30
62
  ProgramClass.prototype.materializeBoxSummaryData = materializeBoxSummaryData;
@@ -0,0 +1,76 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { normalizeIntervalParameters } from "../../grammar/interval.js";
4
+ import { planDerivedDataRevision } from
5
+ "../../materialization/dataProvenance.js";
6
+ import { findDataset } from "../../selectors/datasets.js";
7
+
8
+ const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "level"]);
9
+
10
+ export function planIntervalEdit(program, {
11
+ owner,
12
+ data,
13
+ consumers,
14
+ statistics,
15
+ operation
16
+ }) {
17
+ if (!isPlainObject(statistics)) {
18
+ throw new TypeError(`${operation} statistics must be a plain object.`);
19
+ }
20
+ validateKeys(statistics, STATISTICS_OPTIONS, `${operation} statistics`);
21
+ if (!STATISTICS_OPTIONS.some(key => Object.hasOwn(statistics, key))) {
22
+ throw new Error(
23
+ `${operation} statistics requires center, extent, or level.`
24
+ );
25
+ }
26
+ const previous = findDataset(program, data);
27
+ const transform = previous?.transform?.length === 1
28
+ ? previous.transform[0]
29
+ : undefined;
30
+ if (transform?.type !== "interval") {
31
+ throw new Error(
32
+ `${operation} statistics requires a statistical interval owner; ` +
33
+ "explicit interval fields cannot be converted by edit."
34
+ );
35
+ }
36
+ const center = Object.hasOwn(statistics, "center")
37
+ ? statistics.center
38
+ : transform.center;
39
+ const extent = Object.hasOwn(statistics, "extent")
40
+ ? statistics.extent
41
+ : transform.extent;
42
+ const raw = { center, extent };
43
+ if (Object.hasOwn(statistics, "level")) {
44
+ raw.level = statistics.level;
45
+ } else if (extent === "ci" && transform.extent === "ci") {
46
+ raw.level = transform.level;
47
+ }
48
+ const parameters = normalizeIntervalParameters(raw);
49
+ const current = {
50
+ center: transform.center,
51
+ extent: transform.extent,
52
+ ...(transform.level === undefined ? {} : { level: transform.level })
53
+ };
54
+ const changed = JSON.stringify(parameters) !== JSON.stringify(current);
55
+ if (!changed) return { changed, parameters };
56
+
57
+ const revision = planDerivedDataRevision(program, {
58
+ owner,
59
+ role: "IntervalData",
60
+ previous: previous.id,
61
+ consumers
62
+ });
63
+ return {
64
+ changed,
65
+ parameters,
66
+ revision,
67
+ dataArgs: {
68
+ id: revision.id,
69
+ source: previous.source,
70
+ field: transform.field,
71
+ groupBy: transform.groupBy,
72
+ ...parameters,
73
+ as: transform.as
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,50 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveWindowRows,
6
+ normalizeWindowTransform
7
+ } from "../../grammar/window.js";
8
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
9
+
10
+ const OPTIONS = Object.freeze([
11
+ "id", "source", "partitionBy", "sortBy", "operations"
12
+ ]);
13
+
14
+ export const materializeWindowData = action(
15
+ {
16
+ op: "materializeWindowData",
17
+ description: "Materialize one immutable window-derived dataset."
18
+ },
19
+ function (args = {}) {
20
+ validateKeys(args, MATERIALIZE_OPTIONS, "materializeWindowData");
21
+ const { id, source, transform } = requireDerivedDataset(
22
+ this,
23
+ args.id,
24
+ "window"
25
+ );
26
+ return this.editSemantic({
27
+ property: `dataset[${id}].values`,
28
+ value: deriveWindowRows(source.values, transform)
29
+ });
30
+ }
31
+ );
32
+
33
+ export const createWindowData = action(
34
+ {
35
+ op: "createWindowData",
36
+ description: "Create immutable partitioned window values."
37
+ },
38
+ function (args = {}) {
39
+ validateKeys(args, OPTIONS, "createWindowData");
40
+ const id = validateUserId(args.id, "Window dataset id");
41
+ const source = validateUserId(
42
+ args.source ?? this.context.currentData,
43
+ "Source dataset id"
44
+ );
45
+ const transform = normalizeWindowTransform(args);
46
+ return this
47
+ .createDerivedData({ id, source, transform: [transform] })
48
+ .materializeWindowData({ id });
49
+ }
50
+ );
@@ -0,0 +1,326 @@
1
+ import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
2
+ import { findLayer } from "../../selectors/layers.js";
3
+ import { findSemanticScale } from "../../selectors/scales.js";
4
+ import { resolvePositionScaleDefinition } from "../scales/definitions.js";
5
+
6
+ const POSITION_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
7
+
8
+ export function resolveDistributionScalePlan(program, {
9
+ channel,
10
+ fieldType,
11
+ requested,
12
+ fallback,
13
+ defaults
14
+ }) {
15
+ const options = requested === undefined
16
+ ? { id: fallback }
17
+ : typeof requested === "string"
18
+ ? { id: requested }
19
+ : { ...requested, id: requested.id ?? fallback };
20
+ const existing = findSemanticScale(program, options.id);
21
+ return {
22
+ id: options.id,
23
+ definition: resolvePositionScaleDefinition(
24
+ program,
25
+ channel,
26
+ fieldType,
27
+ options,
28
+ defaults
29
+ ),
30
+ create: existing === undefined,
31
+ edit: existing !== undefined && typeof requested === "object" &&
32
+ Object.keys(requested).some(key => key !== "id")
33
+ ? options
34
+ : undefined
35
+ };
36
+ }
37
+
38
+ export function setCartesianPosition(program, id, channel, {
39
+ field,
40
+ fieldType,
41
+ scale,
42
+ title
43
+ }) {
44
+ let next = program
45
+ .editSemantic({
46
+ property: `layer[${id}].encoding.${channel}.field`,
47
+ value: field
48
+ })
49
+ .editSemantic({
50
+ property: `layer[${id}].encoding.${channel}.fieldType`,
51
+ value: fieldType
52
+ })
53
+ .editSemantic({
54
+ property: `layer[${id}].encoding.${channel}.scale`,
55
+ value: scale
56
+ });
57
+ if (title !== undefined) {
58
+ next = next.editSemantic({
59
+ property: `layer[${id}].encoding.${channel}.title`,
60
+ value: title
61
+ });
62
+ }
63
+ return next;
64
+ }
65
+
66
+ export function setCartesianRange(
67
+ program,
68
+ id,
69
+ channel,
70
+ lower,
71
+ upper,
72
+ scale,
73
+ title
74
+ ) {
75
+ return setCartesianPosition(program, id, channel, {
76
+ field: lower,
77
+ fieldType: "quantitative",
78
+ scale,
79
+ title
80
+ })
81
+ .editSemantic({
82
+ property: `layer[${id}].encoding.${channel}2.field`,
83
+ value: upper
84
+ })
85
+ .editSemantic({
86
+ property: `layer[${id}].encoding.${channel}2.fieldType`,
87
+ value: "quantitative"
88
+ })
89
+ .editSemantic({
90
+ property: `layer[${id}].encoding.${channel}2.scale`,
91
+ value: scale
92
+ });
93
+ }
94
+
95
+ function axisMethods(channel) {
96
+ const prefix = channel === "x" ? "X" : "Y";
97
+ return {
98
+ ticks: `edit${prefix}AxisTicks`,
99
+ labels: `edit${prefix}AxisLabels`,
100
+ title: `edit${prefix}AxisTitle`
101
+ };
102
+ }
103
+
104
+ function isDiscretePosition(scale) {
105
+ return ["ordinal", "band", "point"].includes(scale?.type);
106
+ }
107
+
108
+ function normalizeTickConfig(config, scale) {
109
+ if (config === undefined) return undefined;
110
+ if (isDiscretePosition(scale)) {
111
+ if (config.mode === "count" || config.inferredValues === true) {
112
+ const next = {
113
+ ...config,
114
+ mode: "values",
115
+ values: scale.domain,
116
+ inferredValues: true
117
+ };
118
+ delete next.count;
119
+ return next;
120
+ }
121
+ return config;
122
+ }
123
+ if (config.mode === "values" && config.inferredValues === true) {
124
+ const next = {
125
+ ...config,
126
+ mode: "count",
127
+ count: DEFAULT_TICK_COUNT,
128
+ inferredValues: true
129
+ };
130
+ delete next.values;
131
+ return next;
132
+ }
133
+ return config;
134
+ }
135
+
136
+ function outsideConsumer(program, owned, channel, scale) {
137
+ return program.semanticSpec.layers.some(layer =>
138
+ !owned.has(layer.id) && layer.encoding?.[channel]?.scale === scale
139
+ );
140
+ }
141
+
142
+ export function assertDistributionScaleHandoff(program, {
143
+ owned,
144
+ oldXScale,
145
+ oldYScale,
146
+ newXScale,
147
+ newYScale
148
+ }) {
149
+ const ids = new Set(owned);
150
+ for (const [channel, previous, next] of [
151
+ ["x", oldXScale, newXScale],
152
+ ["y", oldYScale, newYScale]
153
+ ]) {
154
+ if (
155
+ previous !== next &&
156
+ outsideConsumer(program, ids, channel, previous)
157
+ ) {
158
+ throw new Error(
159
+ `Cannot hand off ${channel} scale "${previous}" while unrelated consumers remain.`
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ export function clearCartesianPositions(program, id) {
166
+ const layer = findLayer(program, id);
167
+ if (layer === undefined) return program;
168
+ let next = program;
169
+ for (const channel of POSITION_CHANNELS) {
170
+ if (layer.encoding?.[channel] === undefined) continue;
171
+ next = next.editSemantic({
172
+ property: `layer[${id}].encoding.${channel}`,
173
+ remove: true
174
+ });
175
+ }
176
+ return next;
177
+ }
178
+
179
+ function rebindAxis(program, channel, {
180
+ previousScale,
181
+ nextScale,
182
+ previousTitle,
183
+ nextTitle
184
+ }) {
185
+ const semantic = program.semanticSpec.guides.axis?.[channel];
186
+ if (semantic?.scale !== previousScale) return program;
187
+ let next = program.editSemantic({
188
+ property: `guide.axis.${channel}.scale`,
189
+ value: nextScale
190
+ });
191
+ if (semantic.title === previousTitle) {
192
+ next = next.editSemantic({
193
+ property: `guide.axis.${channel}.title`,
194
+ value: nextTitle
195
+ });
196
+ }
197
+ const scale = next.resolvedScales[nextScale];
198
+ const ticks = normalizeTickConfig(
199
+ next.guideConfigs.axis?.[channel]?.ticks,
200
+ scale
201
+ );
202
+ if (ticks !== undefined) {
203
+ next = next._withGuideConfig(channel, "ticks", {
204
+ ...ticks,
205
+ scale: nextScale
206
+ });
207
+ }
208
+ const labels = next.guideConfigs.axis?.[channel]?.labels;
209
+ if (labels !== undefined) {
210
+ const tickConfig = next.guideConfigs.axis[channel].ticks;
211
+ next = next._withGuideConfig(channel, "labels", {
212
+ ...labels,
213
+ scale: nextScale,
214
+ mode: tickConfig.mode,
215
+ inferredValues: tickConfig.inferredValues,
216
+ ...(tickConfig.mode === "values"
217
+ ? { values: tickConfig.values }
218
+ : { count: tickConfig.count })
219
+ });
220
+ if (tickConfig.mode === "values") {
221
+ const config = next.guideConfigs.axis[channel].labels;
222
+ if (Object.hasOwn(config, "count")) {
223
+ const { count: _count, ...withoutCount } = config;
224
+ next = next._withGuideConfig(channel, "labels", withoutCount);
225
+ }
226
+ } else {
227
+ const config = next.guideConfigs.axis[channel].labels;
228
+ if (Object.hasOwn(config, "values")) {
229
+ const { values: _values, ...withoutValues } = config;
230
+ next = next._withGuideConfig(channel, "labels", withoutValues);
231
+ }
232
+ }
233
+ }
234
+ const title = next.guideConfigs.axis?.[channel]?.title;
235
+ if (title !== undefined) {
236
+ next = next._withGuideConfig(channel, "title", {
237
+ ...title,
238
+ scale: nextScale
239
+ });
240
+ }
241
+ const methods = axisMethods(channel);
242
+ if (ticks !== undefined) next = next[methods.ticks]();
243
+ if (labels !== undefined) next = next[methods.labels]();
244
+ if (title !== undefined) next = next[methods.title]();
245
+ return next;
246
+ }
247
+
248
+ function gridDirection(channel) {
249
+ return channel === "x" ? "vertical" : "horizontal";
250
+ }
251
+
252
+ function rebindGrid(program, {
253
+ previousMeasureChannel,
254
+ nextMeasureChannel,
255
+ previousScale,
256
+ nextScale
257
+ }) {
258
+ const previousDirection = gridDirection(previousMeasureChannel);
259
+ const nextDirection = gridDirection(nextMeasureChannel);
260
+ const stored = program.guideConfigs.grid?.[previousDirection];
261
+ const semantic = program.semanticSpec.guides.grid?.[previousDirection];
262
+ if (stored === undefined || semantic?.scale !== previousScale) return program;
263
+ if (previousDirection === nextDirection) {
264
+ return program
265
+ .editSemantic({
266
+ property: `guide.grid.${previousDirection}.scale`,
267
+ value: nextScale
268
+ })
269
+ ._withGridConfig(previousDirection, {
270
+ ...stored,
271
+ scale: nextScale
272
+ })[`rematerialize${previousDirection === "horizontal" ? "Horizontal" : "Vertical"}Grid`]();
273
+ }
274
+ if (program.semanticSpec.guides.grid?.[nextDirection] !== undefined) {
275
+ throw new Error(
276
+ `Cannot hand off the ${previousDirection} grid because a ${nextDirection} grid already exists.`
277
+ );
278
+ }
279
+ const options = {
280
+ scale: nextScale,
281
+ coordinate: stored.coordinate,
282
+ color: stored.color,
283
+ lineWidth: stored.lineWidth,
284
+ strokeDash: stored.strokeDash,
285
+ ...(stored.inferredValues === true
286
+ ? {}
287
+ : stored.mode === "values"
288
+ ? { values: stored.values }
289
+ : { count: stored.count })
290
+ };
291
+ return program
292
+ .removeGrid({ [previousDirection]: true })
293
+ [`create${nextDirection === "horizontal" ? "Horizontal" : "Vertical"}Grid`](options);
294
+ }
295
+
296
+ export function rebindDistributionGuides(program, {
297
+ oldXScale,
298
+ oldYScale,
299
+ newXScale,
300
+ newYScale,
301
+ oldXTitle,
302
+ oldYTitle,
303
+ newXTitle,
304
+ newYTitle,
305
+ oldMeasureChannel,
306
+ newMeasureChannel
307
+ }) {
308
+ let next = rebindAxis(program, "x", {
309
+ previousScale: oldXScale,
310
+ nextScale: newXScale,
311
+ previousTitle: oldXTitle,
312
+ nextTitle: newXTitle
313
+ });
314
+ next = rebindAxis(next, "y", {
315
+ previousScale: oldYScale,
316
+ nextScale: newYScale,
317
+ previousTitle: oldYTitle,
318
+ nextTitle: newYTitle
319
+ });
320
+ return rebindGrid(next, {
321
+ previousMeasureChannel: oldMeasureChannel,
322
+ nextMeasureChannel: newMeasureChannel,
323
+ previousScale: oldMeasureChannel === "x" ? oldXScale : oldYScale,
324
+ nextScale: newMeasureChannel === "x" ? newXScale : newYScale
325
+ });
326
+ }
@@ -9,7 +9,10 @@ import {
9
9
  resolveAppearanceScaleDefinition,
10
10
  resolveOpacityScaleDefinition
11
11
  } from "../scales/definitions.js";
12
- import { findLayer } from "../../selectors/layers.js";
12
+ import {
13
+ findLayer,
14
+ resolveEligibleLayer
15
+ } from "../../selectors/layers.js";
13
16
  import { applyMaterializationPlan } from "../../materialization/dependencies.js";
14
17
  import { planEncodingRematerialization } from "../../materialization/encodings.js";
15
18
  import {
@@ -20,6 +23,7 @@ import {
20
23
  } from "./shared.js";
21
24
 
22
25
  const RADIUS_OPTIONS = Object.freeze(["value", "target"]);
26
+ const REMOVE_RADIUS_OPTIONS = Object.freeze(["target"]);
23
27
  const OPACITY_OPTIONS = Object.freeze([
24
28
  "value", "field", "target", "fieldType", "scale"
25
29
  ]);
@@ -129,6 +133,35 @@ const encodePointRadius = action(
129
133
  }
130
134
  );
131
135
 
136
+ const removePointRadius = action(
137
+ {
138
+ op: "removePointRadius",
139
+ description: "Remove a constant point radius and restore the theme default."
140
+ },
141
+ function (args = {}) {
142
+ validateOptions(args, REMOVE_RADIUS_OPTIONS, "removePointRadius");
143
+ const requested = args.target === undefined
144
+ ? undefined
145
+ : args.target;
146
+ const layer = resolveEligibleLayer(this, {
147
+ target: requested,
148
+ predicate: candidate =>
149
+ candidate.mark?.type === "point" &&
150
+ this.markConfigs[candidate.id]?.radius !== undefined,
151
+ label: "point mark with an explicit radius"
152
+ });
153
+ const next = this
154
+ ._withoutMaterializationConfig(["marks", layer.id, "radius"]);
155
+ const graphic = next.graphicSpec.objects[layer.id];
156
+ const baseline = graphic === undefined
157
+ ? next
158
+ : graphic.type === "collection"
159
+ ? next.editGraphics({ target: layer.id, property: "items", value: [] })
160
+ : next.editGraphics({ target: layer.id, property: "length", value: 0 });
161
+ return baseline.rematerializePointMark({ id: layer.id });
162
+ }
163
+ );
164
+
132
165
  const encodeSize = action(
133
166
  {
134
167
  op: "encodeSize",
@@ -247,6 +280,7 @@ const encodeOpacity = action(
247
280
  export function registerAppearanceEncodingAction(ProgramClass) {
248
281
  ProgramClass.prototype.encodeRadius = encodeRadius;
249
282
  ProgramClass.prototype.encodePointRadius = encodePointRadius;
283
+ ProgramClass.prototype.removePointRadius = removePointRadius;
250
284
  ProgramClass.prototype.encodeSize = encodeSize;
251
285
  ProgramClass.prototype.encodeShape = encodeShape;
252
286
  ProgramClass.prototype.encodeOpacity = encodeOpacity;
@@ -19,6 +19,8 @@ import { applyMaterializationPlan } from "../../../materialization/dependencies.
19
19
  import {
20
20
  planEncodingRematerialization
21
21
  } from "../../../materialization/encodings.js";
22
+ import { findUpstreamTransform } from
23
+ "../../../materialization/dataProvenance.js";
22
24
  import { encodeContinuousColor } from "./continuous.js";
23
25
  import {
24
26
  applyColorLayoutCompanion,
@@ -56,10 +58,24 @@ const encodeColor = action(
56
58
  "color mark"
57
59
  );
58
60
  assertNoConstantColor(this, layer);
61
+ const densityTransform = dataset.transform?.length === 1 &&
62
+ dataset.transform[0].type === "density"
63
+ ? dataset.transform[0]
64
+ : undefined;
65
+ const densitySeriesFields = densityTransform?.placement?.type === "category"
66
+ ? [densityTransform.groupBy, densityTransform.placement.split?.field]
67
+ .filter(Boolean)
68
+ : [];
69
+ const horizonTransform = findUpstreamTransform(this, dataset, "horizon");
70
+ const horizonColor = horizonTransform !== undefined &&
71
+ horizonTransform.as?.color === args.field &&
72
+ layer.encoding?.group?.field === horizonTransform.as?.group;
59
73
  if (
60
74
  layer.mark.type === "area" &&
61
75
  (layer.encoding?.group?.field === undefined ||
62
- layer.encoding.group.field !== args.field)
76
+ layer.encoding.group.field !== args.field) &&
77
+ !densitySeriesFields.includes(args.field) &&
78
+ !horizonColor
63
79
  ) {
64
80
  throw new Error(
65
81
  "Area color encoding must match an existing group encoding."
@@ -18,6 +18,14 @@ export function applyColorLayoutCompanion(
18
18
  ) {
19
19
  return program;
20
20
  }
21
+ if (
22
+ layer.mark.type === "area" &&
23
+ [layer.encoding?.x, layer.encoding?.y].some(
24
+ encoding => ["nominal", "ordinal"].includes(encoding?.fieldType)
25
+ )
26
+ ) {
27
+ return program;
28
+ }
21
29
  if (isRangedArea(layer)) return program;
22
30
  const channels = layer.mark.type === "bar"
23
31
  ? resolveBarChannels(layer)