ggaction 0.0.4 → 0.0.5

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 (157) hide show
  1. package/CHANGELOG.md +30 -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 +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  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/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -0,0 +1,85 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateUnitInterval
7
+ } from "../../../core/validation.js";
8
+ import { validatePointShape } from "../../../grammar/pointShapes.js";
9
+ import { findLayer } from "../../../selectors/layers.js";
10
+ import { rematerializeExistingLegend } from "../../encodings/shared.js";
11
+ import { validateMarkOptions } from "../shared.js";
12
+
13
+ const OPTIONS = Object.freeze([
14
+ "target", "shape", "fill", "opacity", "stroke", "strokeWidth"
15
+ ]);
16
+
17
+ export const editPointMark = action(
18
+ {
19
+ op: "editPointMark",
20
+ description: "Edit constant point-mark appearance."
21
+ },
22
+ function (args = {}) {
23
+ validateMarkOptions(args, OPTIONS, "editPointMark");
24
+ const editable = ["shape", "fill", "opacity", "stroke", "strokeWidth"];
25
+ if (!editable.some(property => Object.hasOwn(args, property))) {
26
+ throw new Error(
27
+ "editPointMark requires shape, fill, opacity, stroke, or strokeWidth."
28
+ );
29
+ }
30
+ const candidates = this.semanticSpec.layers.filter(
31
+ layer => layer.mark?.type === "point"
32
+ );
33
+ const current = findLayer(this, this.context.currentMark);
34
+ const requested = args.target ??
35
+ (current?.mark?.type === "point" ? current.id : undefined);
36
+ const inferred = requested ?? (candidates.length === 1
37
+ ? candidates[0].id
38
+ : undefined);
39
+ const id = validateUserId(inferred, "Point mark id");
40
+ const layer = findLayer(this, id);
41
+ if (layer?.mark?.type !== "point") {
42
+ throw new Error(`Unknown point mark "${id}".`);
43
+ }
44
+ if (Object.hasOwn(args, "shape") && layer.encoding?.shape !== undefined) {
45
+ throw new Error(
46
+ "editPointMark shape cannot be combined with a shape encoding."
47
+ );
48
+ }
49
+ if (Object.hasOwn(args, "fill") && layer.encoding?.color !== undefined) {
50
+ throw new Error(
51
+ "editPointMark fill cannot be combined with a color encoding."
52
+ );
53
+ }
54
+ const config = { ...this.markConfigs[id] };
55
+ if (Object.hasOwn(args, "shape")) {
56
+ config.shape = validatePointShape(args.shape);
57
+ }
58
+ if (Object.hasOwn(args, "fill")) {
59
+ config.fill = validateNonEmptyString(args.fill, "Point fill");
60
+ }
61
+ if (Object.hasOwn(args, "opacity")) {
62
+ config.opacity = validateUnitInterval(args.opacity, "Point opacity");
63
+ }
64
+ if (Object.hasOwn(args, "stroke")) {
65
+ config.stroke = validateNonEmptyString(args.stroke, "Point stroke");
66
+ config.strokeWidth ??= 1;
67
+ }
68
+ if (Object.hasOwn(args, "strokeWidth")) {
69
+ if (config.stroke === undefined) {
70
+ throw new Error("Point strokeWidth requires an active stroke.");
71
+ }
72
+ config.strokeWidth = validateNonNegativeFinite(
73
+ args.strokeWidth,
74
+ "Point strokeWidth"
75
+ );
76
+ }
77
+ const next = this
78
+ ._withMarkConfig(id, config)
79
+ .rematerializePointMark({ id });
80
+ const legend = next.guideConfigs.legend?.series;
81
+ return legend?.target === id && legend.channels.includes("shape")
82
+ ? rematerializeExistingLegend(next)
83
+ : next;
84
+ }
85
+ );
@@ -1 +1,11 @@
1
- export { registerPointMarkActions } from "./actions.js";
1
+ import { createPointMark } from "./create.js";
2
+ import { editPointMark } from "./edit.js";
3
+ import { rematerializePointMark } from "./materialize.js";
4
+
5
+ export { registerPointJitterActions } from "./jitter.js";
6
+
7
+ export function registerPointMarkActions(ProgramClass) {
8
+ ProgramClass.prototype.createPointMark = createPointMark;
9
+ ProgramClass.prototype.editPointMark = editPointMark;
10
+ ProgramClass.prototype.rematerializePointMark = rematerializePointMark;
11
+ }
@@ -0,0 +1,72 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateKeys } from "../../../core/validation.js";
4
+ import { normalizePointJitterPolicy } from "../../../grammar/jitter.js";
5
+ import { findCoordinate } from "../../../selectors/coordinates.js";
6
+ import { resolveEligibleLayer } from "../../../selectors/layers.js";
7
+
8
+ const JITTER_OPTIONS = Object.freeze([
9
+ "target", "channel", "maxOffset", "seed", "key"
10
+ ]);
11
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
12
+
13
+ function isCartesianPoint(program, layer, channel) {
14
+ return layer.mark?.type === "point" &&
15
+ layer.encoding?.[channel]?.scale !== undefined &&
16
+ layer.encoding?.[channel === "x" ? "y" : "x"]?.scale !== undefined &&
17
+ findCoordinate(program, layer.coordinate)?.type === "cartesian";
18
+ }
19
+
20
+ function resolveJitterTarget(program, target, channel) {
21
+ const requested = target === undefined
22
+ ? undefined
23
+ : validateUserId(target, "Point jitter target");
24
+ return resolveEligibleLayer(program, {
25
+ target: requested,
26
+ predicate: layer => isCartesianPoint(program, layer, channel),
27
+ label: "point jitter"
28
+ });
29
+ }
30
+
31
+ export const jitterPoints = action(
32
+ {
33
+ op: "jitterPoints",
34
+ description: "Apply deterministic bounded graphical jitter to a point mark."
35
+ },
36
+ function (args = {}) {
37
+ validateKeys(args, JITTER_OPTIONS, "jitterPoints");
38
+ const policy = normalizePointJitterPolicy(args);
39
+ const layer = resolveJitterTarget(this, args.target, policy.channel);
40
+ return this
41
+ ._withMaterializationConfig(["jitters", layer.id], policy)
42
+ .rematerializePointMark({ id: layer.id });
43
+ }
44
+ );
45
+
46
+ export const removeJitter = action(
47
+ {
48
+ op: "removeJitter",
49
+ description: "Remove point jitter and restore semantic scale positions."
50
+ },
51
+ function (args = {}) {
52
+ validateKeys(args, REMOVE_OPTIONS, "removeJitter");
53
+ const requested = args.target === undefined
54
+ ? undefined
55
+ : validateUserId(args.target, "Point jitter target");
56
+ const layer = resolveEligibleLayer(this, {
57
+ target: requested,
58
+ predicate: candidate =>
59
+ candidate.mark?.type === "point" &&
60
+ this.materializationConfigs.jitters?.[candidate.id] !== undefined,
61
+ label: "point jitter"
62
+ });
63
+ return this
64
+ ._withoutMaterializationConfig(["jitters", layer.id])
65
+ .rematerializePointMark({ id: layer.id });
66
+ }
67
+ );
68
+
69
+ export function registerPointJitterActions(ProgramClass) {
70
+ ProgramClass.prototype.jitterPoints = jitterPoints;
71
+ ProgramClass.prototype.removeJitter = removeJitter;
72
+ }
@@ -1,103 +1,32 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import { validateUserId } from "../../../core/identifiers.js";
3
- import {
4
- validateNonEmptyString,
5
- validateNonNegativeFinite,
6
- validateUnitInterval
7
- } from "../../../core/validation.js";
8
3
  import { getPointGraphicType } from "../../../grammar/schemas/mark.js";
9
4
  import {
10
5
  createPointShapeGraphic,
6
+ resolvePointShapeExtent,
11
7
  validatePointShape
12
8
  } from "../../../grammar/pointShapes.js";
9
+ import {
10
+ canonicalJitterScalar,
11
+ normalizePointJitterPolicy,
12
+ resolvePointJitter
13
+ } from "../../../grammar/jitter.js";
13
14
  import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
14
15
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
16
+ import { validateMarkOptions } from "../shared.js";
15
17
  import {
16
- assertMarkAvailable,
17
- applyLayeredMarkInheritance,
18
- materializeInheritedMark,
19
- resolveLayeredMarkInheritance,
20
- resolveMarkId,
21
- resolveMarkData,
22
- validateMarkOptions
23
- } from "../shared.js";
24
- import { DEFAULT_COLORS } from "../../../theme/defaults.js";
18
+ DEFAULT_COLORS,
19
+ DEFAULT_POINT_RADIUS
20
+ } from "../../../theme/defaults.js";
25
21
  import { findDataset } from "../../../selectors/datasets.js";
26
22
  import { findLayer } from "../../../selectors/layers.js";
27
- import { rematerializeExistingLegend } from "../../encodings/shared.js";
28
- import { resolveMarkGraphicPlacement } from
29
- "../../../materialization/graphicHierarchy.js";
30
23
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
31
24
  import { resolveRowEncodingValues } from
32
25
  "../../../materialization/rowEncoding.js";
33
26
 
34
- const POINT_MARK_OPTIONS = Object.freeze([
35
- "id", "data", "shape", "fill", "opacity", "stroke", "strokeWidth"
36
- ]);
37
- const EDIT_POINT_OPTIONS = Object.freeze([
38
- "target", "shape", "fill", "opacity", "stroke", "strokeWidth"
39
- ]);
40
27
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
41
28
  const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
42
29
 
43
- const createPointMark = action(
44
- {
45
- op: "createPointMark",
46
- description: "Create a semantic point mark and concrete point graphics."
47
- },
48
- function (args = {}) {
49
- validateMarkOptions(args, POINT_MARK_OPTIONS, "createPointMark");
50
- const id = resolveMarkId(this, args.id, {
51
- defaultId: "point",
52
- label: "Point mark id",
53
- markType: "point",
54
- operation: "createPointMark"
55
- });
56
- const inherited = resolveLayeredMarkInheritance(this, args, "point");
57
- const { data, dataset } = resolveMarkData(this, {
58
- ...args,
59
- ...(args.data === undefined &&
60
- this.context.currentData === undefined &&
61
- inherited?.data !== undefined
62
- ? { data: inherited.data }
63
- : {})
64
- });
65
- const shape = Object.hasOwn(args, "shape") ? args.shape : "circle";
66
- validatePointShape(shape);
67
- const resolvedType = getPointGraphicType(shape);
68
- const graphicType = resolvedType === "path" ? "circle" : resolvedType;
69
-
70
- assertMarkAvailable(this, id);
71
-
72
- let next = this
73
- .editSemantic({
74
- property: `layer[${id}].mark.type`,
75
- value: "point"
76
- })
77
- .editSemantic({
78
- property: `layer[${id}].data`,
79
- value: data
80
- });
81
- next = applyLayeredMarkInheritance(next, id, inherited)
82
- .createGraphics({
83
- id,
84
- type: graphicType,
85
- length: dataset.values.length,
86
- ...resolveMarkGraphicPlacement(next, { data, markType: "point" })
87
- })
88
- ._withMarkConfig(id, { shape });
89
- const created = materializeInheritedMark(next, id);
90
- const appearance = Object.fromEntries(
91
- ["fill", "opacity", "stroke", "strokeWidth"]
92
- .filter(property => Object.hasOwn(args, property))
93
- .map(property => [property, args[property]])
94
- );
95
- return Object.keys(appearance).length === 0
96
- ? created
97
- : created.editPointMark({ target: id, ...appearance });
98
- }
99
- );
100
-
101
30
  function resolvePointPositions(program, layer, dataset) {
102
31
  const hasPolar = layer.encoding?.theta !== undefined ||
103
32
  layer.encoding?.radius !== undefined;
@@ -154,7 +83,122 @@ function existingArea(child, parentType) {
154
83
  return undefined;
155
84
  }
156
85
 
157
- const rematerializePointMark = action(
86
+ function resolvedPointArea(area, index, config, existingChildren, graphicType) {
87
+ return area?.[index] ?? (
88
+ config.radius === undefined
89
+ ? existingArea(existingChildren[index], graphicType) ??
90
+ Math.PI * DEFAULT_POINT_RADIUS ** 2
91
+ : Math.PI * config.radius ** 2
92
+ );
93
+ }
94
+
95
+ function resolveJitterEntries({
96
+ policy,
97
+ dataset,
98
+ x,
99
+ y,
100
+ area,
101
+ shapes,
102
+ config,
103
+ graphic,
104
+ existingChildren
105
+ }) {
106
+ const seen = new Set();
107
+ return dataset.values.flatMap((row, index) => {
108
+ if (!Number.isFinite(x?.[index]) || !Number.isFinite(y?.[index])) return [];
109
+ const identity = policy.key === undefined ? index : row[policy.key];
110
+ const canonical = canonicalJitterScalar(
111
+ identity,
112
+ policy.key === undefined
113
+ ? "Point jitter source index"
114
+ : `Point jitter key field "${policy.key}"`
115
+ );
116
+ if (seen.has(canonical)) {
117
+ throw new Error(
118
+ `Point jitter key field "${policy.key}" must be unique for materialized items.`
119
+ );
120
+ }
121
+ seen.add(canonical);
122
+ const resolvedArea = resolvedPointArea(
123
+ area,
124
+ index,
125
+ config,
126
+ existingChildren,
127
+ graphic.type
128
+ );
129
+ const extent = resolvePointShapeExtent({
130
+ shape: shapes[index],
131
+ area: resolvedArea,
132
+ strokeWidth: config.stroke === undefined ? 0 : config.strokeWidth ?? 1
133
+ });
134
+ return [{
135
+ index,
136
+ identity,
137
+ base: policy.channel === "x" ? x[index] : y[index],
138
+ halfExtent: extent[policy.channel]
139
+ }];
140
+ });
141
+ }
142
+
143
+ function applyPointJitter(program, {
144
+ id,
145
+ layer,
146
+ dataset,
147
+ x,
148
+ y,
149
+ area,
150
+ shapes,
151
+ config,
152
+ graphic,
153
+ existingChildren
154
+ }) {
155
+ const stored = program.materializationConfigs.jitters?.[id];
156
+ if (stored === undefined) return { program, x, y };
157
+ const policy = normalizePointJitterPolicy(stored);
158
+ const encoding = layer.encoding?.[policy.channel];
159
+ const scale = program.resolvedScales[encoding?.scale];
160
+ if (scale === undefined) {
161
+ throw new Error(
162
+ `Point jitter on "${id}" requires a resolved ${policy.channel} scale.`
163
+ );
164
+ }
165
+ const bounds = resolveGraphicBounds(program);
166
+ const entries = resolveJitterEntries({
167
+ policy,
168
+ dataset,
169
+ x,
170
+ y,
171
+ area,
172
+ shapes,
173
+ config,
174
+ graphic,
175
+ existingChildren
176
+ });
177
+ const resolution = resolvePointJitter({
178
+ target: id,
179
+ policy,
180
+ scale,
181
+ entries,
182
+ plotMinimum: policy.channel === "x" ? bounds.x : bounds.y,
183
+ plotMaximum: policy.channel === "x"
184
+ ? bounds.x + bounds.width
185
+ : bounds.y + bounds.height
186
+ });
187
+ const values = [...(policy.channel === "x" ? x : y)];
188
+ for (const item of resolution.items) {
189
+ values[item.index] += item.finalOffset;
190
+ }
191
+ return {
192
+ program: program._withMaterializationConfig(["jitters", id], {
193
+ ...policy,
194
+ resolved: resolution
195
+ }),
196
+ x: policy.channel === "x" ? values : x,
197
+ y: policy.channel === "y" ? values : y
198
+ };
199
+ }
200
+
201
+ export const rematerializePointMark = action(
158
202
  {
159
203
  op: "rematerializePointMark",
160
204
  description: "Recompute concrete point geometry and appearance."
@@ -183,7 +227,7 @@ const rematerializePointMark = action(
183
227
  throw new Error(`Point mark "${id}" requires an existing dataset.`);
184
228
  }
185
229
 
186
- const { x, y } = resolvePointPositions(this, layer, dataset);
230
+ const positions = resolvePointPositions(this, layer, dataset);
187
231
  const mappedFill = resolveRowEncodingValues(this, layer, dataset, "color");
188
232
  const area = resolveRowEncodingValues(this, layer, dataset, "size");
189
233
  const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
@@ -197,6 +241,19 @@ const rematerializePointMark = action(
197
241
  encodedShape !== undefined ||
198
242
  graphic.type === "collection" ||
199
243
  getPointGraphicType(constantShape) !== graphic.type;
244
+ const jittered = applyPointJitter(this, {
245
+ id,
246
+ layer,
247
+ dataset,
248
+ x: positions.x,
249
+ y: positions.y,
250
+ area,
251
+ shapes,
252
+ config,
253
+ graphic,
254
+ existingChildren
255
+ });
256
+ const { x, y } = jittered;
200
257
 
201
258
  if (requiresMixedCollection) {
202
259
  const items = dataset.values.map((_, index) => {
@@ -205,11 +262,13 @@ const rematerializePointMark = action(
205
262
  const color = mappedFill?.[index] ?? config.fill ??
206
263
  existing.fill ?? DEFAULT_POINT_FILL;
207
264
  const opacity = encodedOpacity?.[index] ?? config.opacity ?? existing.opacity;
208
- const resolvedArea = area?.[index] ??
209
- (config.radius === undefined
210
- ? existingArea(existingChildren[index], graphic.type)
211
- :
212
- Math.PI * config.radius ** 2);
265
+ const resolvedArea = resolvedPointArea(
266
+ area,
267
+ index,
268
+ config,
269
+ existingChildren,
270
+ graphic.type
271
+ );
213
272
  const centerX = x?.[index] ?? existing.x;
214
273
  const centerY = y?.[index] ?? existing.y;
215
274
  if (
@@ -237,12 +296,16 @@ const rematerializePointMark = action(
237
296
  opacity
238
297
  });
239
298
  });
240
- return this.editGraphics({ target: id, property: "items", value: items });
299
+ return jittered.program.editGraphics({
300
+ target: id,
301
+ property: "items",
302
+ value: items
303
+ });
241
304
  }
242
305
 
243
306
  let next = graphic.items.length === dataset.values.length
244
- ? this
245
- : this.editGraphics({
307
+ ? jittered.program
308
+ : jittered.program.editGraphics({
246
309
  target: id,
247
310
  property: "length",
248
311
  value: dataset.values.length
@@ -254,14 +317,32 @@ const rematerializePointMark = action(
254
317
  }
255
318
  if (graphic.type === "circle") {
256
319
  const radii = area === undefined
257
- ? config.radius
320
+ ? config.radius ?? (
321
+ x !== undefined && y !== undefined
322
+ ? dataset.values.map((_, index) => {
323
+ const existing = existingChildren[index]?.properties?.radius;
324
+ return Number.isFinite(existing)
325
+ ? existing
326
+ : DEFAULT_POINT_RADIUS;
327
+ })
328
+ : undefined
329
+ )
258
330
  : area.map(value => Math.sqrt(value / Math.PI));
259
331
  if (radii !== undefined) {
260
332
  next = next.editGraphics({ target: id, property: "radius", value: radii });
261
333
  }
262
- } else if (area !== undefined || config.radius !== undefined) {
334
+ } else if (
335
+ area !== undefined ||
336
+ config.radius !== undefined ||
337
+ (x !== undefined && y !== undefined)
338
+ ) {
263
339
  const sides = area === undefined
264
- ? config.radius * Math.sqrt(Math.PI)
340
+ ? dataset.values.map((_, index) => {
341
+ const existing = existingArea(existingChildren[index], graphic.type);
342
+ return Math.sqrt(
343
+ existing ?? Math.PI * (config.radius ?? DEFAULT_POINT_RADIUS) ** 2
344
+ );
345
+ })
265
346
  : area.map(value => Math.sqrt(value));
266
347
  const centersX = x ?? existingChildren.map(child => child.properties.x);
267
348
  const centersY = y ?? existingChildren.map(child => child.properties.y);
@@ -301,79 +382,3 @@ const rematerializePointMark = action(
301
382
  return next;
302
383
  }
303
384
  );
304
-
305
- const editPointMark = action(
306
- {
307
- op: "editPointMark",
308
- description: "Edit constant point-mark appearance."
309
- },
310
- function (args = {}) {
311
- validateMarkOptions(args, EDIT_POINT_OPTIONS, "editPointMark");
312
- const editable = ["shape", "fill", "opacity", "stroke", "strokeWidth"];
313
- if (!editable.some(property => Object.hasOwn(args, property))) {
314
- throw new Error(
315
- "editPointMark requires shape, fill, opacity, stroke, or strokeWidth."
316
- );
317
- }
318
- const candidates = this.semanticSpec.layers.filter(
319
- layer => layer.mark?.type === "point"
320
- );
321
- const current = findLayer(this, this.context.currentMark);
322
- const requested = args.target ??
323
- (current?.mark?.type === "point" ? current.id : undefined);
324
- const inferred = requested ?? (candidates.length === 1
325
- ? candidates[0].id
326
- : undefined);
327
- const id = validateUserId(inferred, "Point mark id");
328
- const layer = findLayer(this, id);
329
- if (layer?.mark?.type !== "point") {
330
- throw new Error(`Unknown point mark "${id}".`);
331
- }
332
- if (Object.hasOwn(args, "shape") && layer.encoding?.shape !== undefined) {
333
- throw new Error(
334
- "editPointMark shape cannot be combined with a shape encoding."
335
- );
336
- }
337
- if (Object.hasOwn(args, "fill") && layer.encoding?.color !== undefined) {
338
- throw new Error(
339
- "editPointMark fill cannot be combined with a color encoding."
340
- );
341
- }
342
- const config = { ...this.markConfigs[id] };
343
- if (Object.hasOwn(args, "shape")) {
344
- config.shape = validatePointShape(args.shape);
345
- }
346
- if (Object.hasOwn(args, "fill")) {
347
- config.fill = validateNonEmptyString(args.fill, "Point fill");
348
- }
349
- if (Object.hasOwn(args, "opacity")) {
350
- config.opacity = validateUnitInterval(args.opacity, "Point opacity");
351
- }
352
- if (Object.hasOwn(args, "stroke")) {
353
- config.stroke = validateNonEmptyString(args.stroke, "Point stroke");
354
- config.strokeWidth ??= 1;
355
- }
356
- if (Object.hasOwn(args, "strokeWidth")) {
357
- if (config.stroke === undefined) {
358
- throw new Error("Point strokeWidth requires an active stroke.");
359
- }
360
- config.strokeWidth = validateNonNegativeFinite(
361
- args.strokeWidth,
362
- "Point strokeWidth"
363
- );
364
- }
365
- const next = this
366
- ._withMarkConfig(id, config)
367
- .rematerializePointMark({ id });
368
- const legend = next.guideConfigs.legend?.series;
369
- return legend?.target === id && legend.channels.includes("shape")
370
- ? rematerializeExistingLegend(next)
371
- : next;
372
- }
373
- );
374
-
375
- export function registerPointMarkActions(ProgramClass) {
376
- ProgramClass.prototype.createPointMark = createPointMark;
377
- ProgramClass.prototype.editPointMark = editPointMark;
378
- ProgramClass.prototype.rematerializePointMark = rematerializePointMark;
379
- }
@@ -102,6 +102,9 @@ const rematerializeRectMark = action(
102
102
  if (graphic?.type !== "rect" || !Array.isArray(graphic.items)) {
103
103
  throw new Error(`Rect mark "${id}" requires rect collection graphics.`);
104
104
  }
105
+ if (this.markConfigs[id]?.gradientPlot?.materialized === true) {
106
+ return this.materializeGradientPlotFill({ id });
107
+ }
105
108
  const dataset = findDataset(this, layer.data);
106
109
  if (dataset === undefined) {
107
110
  throw new Error(`Rect mark "${id}" requires an existing dataset.`);
@@ -27,7 +27,8 @@ function ownedChildren(program, id) {
27
27
  config.regression?.lineId,
28
28
  config.boxPlot?.whiskerId,
29
29
  config.boxPlot?.medianId,
30
- config.boxPlot?.outlierId
30
+ config.boxPlot?.outlierId,
31
+ config.gradientPlot?.centerId
31
32
  ].concat(
32
33
  program.semanticSpec.layers
33
34
  .filter(layer => layer.source === id)
@@ -88,7 +89,8 @@ function ownedDerivedData(program, ids) {
88
89
  config.errorBand?.data,
89
90
  config.regression?.dataId,
90
91
  config.boxPlot?.summaryId,
91
- config.boxPlot?.outlierDataId
92
+ config.boxPlot?.outlierDataId,
93
+ config.gradientPlot?.profileId
92
94
  ]) {
93
95
  if (candidate !== undefined) candidates.add(candidate);
94
96
  }
@@ -189,6 +191,13 @@ export const removeMark = action(
189
191
  const selectionCleanup = cleanupSelectionState(next, ids);
190
192
  next = selectionCleanup.program;
191
193
  for (const id of ids) {
194
+ const labelLayout = next.materializationConfigs.labelLayouts?.[id];
195
+ if (
196
+ labelLayout?.leaderId !== undefined &&
197
+ next.graphicSpec.objects[labelLayout.leaderId] !== undefined
198
+ ) {
199
+ next = next.editGraphics({ target: labelLayout.leaderId, remove: true });
200
+ }
192
201
  if (findLayer(next, id) !== undefined) {
193
202
  next = next.editSemantic({ property: `layer[${id}]`, remove: true });
194
203
  }
@@ -196,6 +205,8 @@ export const removeMark = action(
196
205
  next = next.editGraphics({ target: id, remove: true });
197
206
  }
198
207
  next = next._withoutMaterializationConfig(["marks", id]);
208
+ next = next._withoutMaterializationConfig(["jitters", id]);
209
+ next = next._withoutMaterializationConfig(["labelLayouts", id]);
199
210
  }
200
211
 
201
212
  next = cleanupPositionGuides(next, positionScales);