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
@@ -104,7 +104,17 @@ const createRuleMark = action(
104
104
  length: 0,
105
105
  ...resolveMarkGraphicPlacement(created, { data, markType: "rule" })
106
106
  })
107
- ._withMarkConfig(id, DEFAULT_RULE_CONFIG);
107
+ ._withMarkConfig(id, {
108
+ ...DEFAULT_RULE_CONFIG,
109
+ ...(inherited === undefined
110
+ ? {}
111
+ : {
112
+ inheritedPosition: {
113
+ source: inherited.source,
114
+ channels: Object.keys(inherited.encoding)
115
+ }
116
+ })
117
+ });
108
118
  return materializeInheritedMark(created, id);
109
119
  }
110
120
  );
@@ -143,7 +153,7 @@ const rematerializeRuleMark = action(
143
153
  }
144
154
 
145
155
  const scaleIds = [...new Set(
146
- ["x", "y", "x2", "y2", "strokeDash", "opacity"]
156
+ ["x", "y", "x2", "y2", "strokeDash", "strokeWidth", "opacity"]
147
157
  .map(channel => layer.encoding?.[channel]?.scale)
148
158
  .filter(scale => scale !== undefined)
149
159
  )];
@@ -153,6 +163,11 @@ const rematerializeRuleMark = action(
153
163
  }
154
164
 
155
165
  const derived = deriveRuleValues(dataset.values, layer);
166
+ if (derived.values.strokeWidth?.some(value => value < 0)) {
167
+ throw new RangeError(
168
+ `Rule strokeWidth field "${layer.encoding.strokeWidth.field}" cannot contain negative values.`
169
+ );
170
+ }
156
171
  const mapped = {};
157
172
  for (const channel of ["x", "y", "x2", "y2"]) {
158
173
  const encoding = layer.encoding?.[channel];
@@ -226,6 +241,7 @@ const rematerializeRuleMark = action(
226
241
  const resolvedConfig = resolved.markConfigs[id] ?? DEFAULT_RULE_CONFIG;
227
242
  const dashEncoding = layer.encoding?.strokeDash;
228
243
  const opacityEncoding = layer.encoding?.opacity;
244
+ const strokeWidthEncoding = layer.encoding?.strokeWidth;
229
245
  const strokeDash = dashEncoding?.scale === undefined
230
246
  ? Array.from(
231
247
  { length: derived.length },
@@ -244,6 +260,12 @@ const rematerializeRuleMark = action(
244
260
  derived.values.opacity,
245
261
  resolved.resolvedScales[opacityEncoding.scale]
246
262
  );
263
+ const strokeWidth = strokeWidthEncoding?.scale === undefined
264
+ ? resolvedConfig.strokeWidth
265
+ : mapContinuousScaleValues(
266
+ derived.values.strokeWidth,
267
+ resolved.resolvedScales[strokeWidthEncoding.scale]
268
+ );
247
269
 
248
270
  return resolved
249
271
  .editGraphics({ target: id, property: "length", value: derived.length })
@@ -255,7 +277,7 @@ const rematerializeRuleMark = action(
255
277
  .editGraphics({
256
278
  target: id,
257
279
  property: "strokeWidth",
258
- value: resolvedConfig.strokeWidth
280
+ value: strokeWidth
259
281
  })
260
282
  .editGraphics({ target: id, property: "strokeDash", value: strokeDash })
261
283
  .editGraphics({ target: id, property: "opacity", value: opacity });
@@ -24,14 +24,19 @@ const STYLE_OPTIONS = Object.freeze([
24
24
  ]);
25
25
  const CREATE_OPTIONS = Object.freeze(["id", "data", "text", ...STYLE_OPTIONS]);
26
26
  const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
27
- const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
27
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id", "replayLayout"]);
28
28
  const SOURCE_TYPES = new Set(["point", "bar", "rule", "rect"]);
29
29
 
30
+ function sourceMatchesData(program, layer, requestedData) {
31
+ if (requestedData === undefined || layer?.data === requestedData) return true;
32
+ return program.markConfigs[layer.id]?.gradientPlot?.source === requestedData;
33
+ }
34
+
30
35
  function eligibleSource(program, layer, requestedData) {
31
36
  if (
32
37
  !SOURCE_TYPES.has(layer?.mark?.type) ||
33
38
  layer.data === undefined ||
34
- (requestedData !== undefined && layer.data !== requestedData)
39
+ !sourceMatchesData(program, layer, requestedData)
35
40
  ) return false;
36
41
  const encodings = resolveCompatibleEncodings(program, layer, "text");
37
42
  return encodings.x !== undefined && encodings.y !== undefined;
@@ -133,6 +138,10 @@ const rematerializeTextMark = action(
133
138
  function (args = {}) {
134
139
  validateMarkOptions(args, REMATERIALIZE_OPTIONS, "rematerializeTextMark");
135
140
  const id = validateUserId(args.id, "Text mark id");
141
+ const replayLayout = args.replayLayout ?? true;
142
+ if (typeof replayLayout !== "boolean") {
143
+ throw new TypeError("rematerializeTextMark replayLayout must be a boolean.");
144
+ }
136
145
  const layer = findLayer(this, id);
137
146
  const graphic = this.graphicSpec.objects[id];
138
147
  if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
@@ -140,11 +149,14 @@ const rematerializeTextMark = action(
140
149
  throw new Error(`Text mark "${id}" requires text collection graphics.`);
141
150
  }
142
151
  if (!canMaterializeText(this, layer)) {
143
- return graphic.items.length === 0
152
+ const next = graphic.items.length === 0
144
153
  ? this
145
154
  : this.editGraphics({ target: id, property: "length", value: 0 });
155
+ return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
156
+ ? next.materializeLabelLayout({ id, rematerializeBase: false })
157
+ : next;
146
158
  }
147
- return this.editGraphics({
159
+ const next = this.editGraphics({
148
160
  target: id,
149
161
  property: "items",
150
162
  value: resolveTextGraphicItems(
@@ -153,6 +165,9 @@ const rematerializeTextMark = action(
153
165
  this.markConfigs[id] ?? DEFAULT_TEXT_MARK
154
166
  )
155
167
  });
168
+ return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
169
+ ? next.materializeLabelLayout({ id, rematerializeBase: false })
170
+ : next;
156
171
  }
157
172
  );
158
173
 
@@ -1 +1,7 @@
1
- export { registerTextMarkActions } from "./actions.js";
1
+ import { registerTextMarkActions as registerTextCoreActions } from "./actions.js";
2
+ import { registerTextLabelLayoutActions } from "./layout.js";
3
+
4
+ export function registerTextMarkActions(ProgramClass) {
5
+ registerTextCoreActions(ProgramClass);
6
+ registerTextLabelLayoutActions(ProgramClass);
7
+ }
@@ -0,0 +1,321 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../../core/validation.js";
4
+ import { DEFAULT_TEXT_MARK } from "../../../grammar/text.js";
5
+ import { findGraphicParent } from "../../../grammar/schemas/graphicTree.js";
6
+ import { validateConcreteGraphicValue } from
7
+ "../../../grammar/schemas/concreteGraphic.js";
8
+ import {
9
+ normalizeLabelLayoutGeometry,
10
+ resolveLabelLayout,
11
+ resolveLabelLeader
12
+ } from "../../../layout/labels.js";
13
+ import { resolveGraphicBounds } from "../../../layout/canvas.js";
14
+ import { canMaterializeText } from "../../../materialization/marks/index.js";
15
+ import { resolveTextGraphicEntries } from "../../../materialization/text.js";
16
+ import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
17
+
18
+ const LAYOUT_OPTIONS = Object.freeze([
19
+ "target", "axis", "padding", "maxDisplacement", "bounds", "leader"
20
+ ]);
21
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
22
+ const MATERIALIZE_OPTIONS = Object.freeze(["id", "rematerializeBase"]);
23
+ const LEADER_OPTIONS = Object.freeze([
24
+ "stroke", "strokeWidth", "strokeDash", "opacity"
25
+ ]);
26
+
27
+ function normalizeLeader(value) {
28
+ if (value === false || value === undefined) return false;
29
+ validateOptionObject(value, LEADER_OPTIONS, "label leader");
30
+ const normalized = {
31
+ stroke: "#94a3b8",
32
+ strokeWidth: 1,
33
+ strokeDash: [],
34
+ opacity: 1,
35
+ ...value
36
+ };
37
+ for (const [property, propertyValue] of Object.entries(normalized)) {
38
+ validateConcreteGraphicValue("line", property, propertyValue);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function normalizePolicy(args) {
44
+ const geometry = normalizeLabelLayoutGeometry(Object.fromEntries(
45
+ Object.entries(args).filter(
46
+ ([key]) => key !== "target" && key !== "leader"
47
+ )
48
+ ));
49
+ return { ...geometry, leader: normalizeLeader(args.leader) };
50
+ }
51
+
52
+ function leaderId(target) {
53
+ return `${target}-label-leaders`;
54
+ }
55
+
56
+ function requireCompleteText(program, requested, operation) {
57
+ const target = requested === undefined
58
+ ? undefined
59
+ : validateUserId(requested, "Text mark id");
60
+ if (target !== undefined) {
61
+ const layer = findLayer(program, target);
62
+ if (layer?.mark?.type !== "text") {
63
+ throw new Error(`Unknown ${operation} text mark target "${target}".`);
64
+ }
65
+ if (!canMaterializeText(program, layer)) {
66
+ throw new Error(`${operation} requires a complete text mark.`);
67
+ }
68
+ return layer;
69
+ }
70
+ return resolveEligibleLayer(program, {
71
+ predicate: layer =>
72
+ layer.mark?.type === "text" && canMaterializeText(program, layer),
73
+ label: `${operation} text mark`
74
+ });
75
+ }
76
+
77
+ function requireConfiguredText(program, requested) {
78
+ const target = requested === undefined
79
+ ? undefined
80
+ : validateUserId(requested, "Text mark id");
81
+ return resolveEligibleLayer(program, {
82
+ target,
83
+ predicate: layer =>
84
+ layer.mark?.type === "text" &&
85
+ program.materializationConfigs.labelLayouts?.[layer.id] !== undefined,
86
+ label: "removeLabelLayout text mark"
87
+ });
88
+ }
89
+
90
+ function concreteBounds(program, kind) {
91
+ if (kind === "plot") {
92
+ const plot = resolveGraphicBounds(program);
93
+ return {
94
+ left: plot.x,
95
+ right: plot.x + plot.width,
96
+ top: plot.y,
97
+ bottom: plot.y + plot.height
98
+ };
99
+ }
100
+ const canvas = program.graphicSpec.objects.canvas;
101
+ if (
102
+ canvas?.type !== "canvas" ||
103
+ !Number.isFinite(canvas.properties.width) ||
104
+ !Number.isFinite(canvas.properties.height)
105
+ ) {
106
+ throw new Error("Canvas label bounds require concrete Canvas dimensions.");
107
+ }
108
+ const x = canvas.properties.x ?? 0;
109
+ const y = canvas.properties.y ?? 0;
110
+ return {
111
+ left: x,
112
+ right: x + canvas.properties.width,
113
+ top: y,
114
+ bottom: y + canvas.properties.height
115
+ };
116
+ }
117
+
118
+ function layoutInputs(program, layer) {
119
+ const entries = resolveTextGraphicEntries(
120
+ program,
121
+ layer,
122
+ program.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
123
+ );
124
+ const graphic = program.graphicSpec.objects[layer.id];
125
+ if (graphic?.type !== "text" || !Array.isArray(graphic.items)) {
126
+ throw new Error(`Text mark "${layer.id}" requires text collection graphics.`);
127
+ }
128
+ if (entries.length !== graphic.items.length) {
129
+ throw new Error(`Text mark "${layer.id}" base materialization is inconsistent.`);
130
+ }
131
+ return entries.map((entry, index) => ({
132
+ id: graphic.items[index].id,
133
+ x: entry.graphic.properties.x,
134
+ y: entry.graphic.properties.y,
135
+ sourceX: entry.anchor.x,
136
+ sourceY: entry.anchor.y,
137
+ text: entry.graphic.properties.text,
138
+ fontSize: entry.graphic.properties.fontSize,
139
+ textAlign: entry.graphic.properties.textAlign,
140
+ textBaseline: entry.graphic.properties.textBaseline,
141
+ rotation: entry.graphic.properties.rotation
142
+ }));
143
+ }
144
+
145
+ function leaderPlacement(program, layer) {
146
+ const targetParent = findGraphicParent(program.graphicSpec, layer.id);
147
+ if (targetParent?.kind !== "parent") {
148
+ throw new Error("Label leaders require the text graphic to have a parent container.");
149
+ }
150
+ const source = layer.source === undefined
151
+ ? undefined
152
+ : program.graphicSpec.objects[layer.source];
153
+ const sourceParent = source === undefined
154
+ ? undefined
155
+ : findGraphicParent(program.graphicSpec, layer.source);
156
+ const before = sourceParent?.kind === "parent" && sourceParent.id === targetParent.id
157
+ ? layer.source
158
+ : layer.id;
159
+ return { parent: targetParent.id, before };
160
+ }
161
+
162
+ function removeLeaderGraphic(program, id) {
163
+ return program.graphicSpec.objects[id] === undefined
164
+ ? program
165
+ : program.editGraphics({ target: id, remove: true });
166
+ }
167
+
168
+ function materializeLeaders(program, layer, config, resolved) {
169
+ let next = removeLeaderGraphic(program, config.leaderId);
170
+ if (config.leader === false) return { program: next, count: 0 };
171
+ const leaders = resolved.items.map(resolveLabelLeader).filter(Boolean);
172
+ if (leaders.length === 0) return { program: next, count: 0 };
173
+ next = next
174
+ .createGraphics({
175
+ id: config.leaderId,
176
+ type: "line",
177
+ length: leaders.length,
178
+ ...leaderPlacement(next, layer)
179
+ })
180
+ .editGraphics({
181
+ target: config.leaderId,
182
+ property: "x1",
183
+ value: leaders.map(line => line.x1)
184
+ })
185
+ .editGraphics({
186
+ target: config.leaderId,
187
+ property: "y1",
188
+ value: leaders.map(line => line.y1)
189
+ })
190
+ .editGraphics({
191
+ target: config.leaderId,
192
+ property: "x2",
193
+ value: leaders.map(line => line.x2)
194
+ })
195
+ .editGraphics({
196
+ target: config.leaderId,
197
+ property: "y2",
198
+ value: leaders.map(line => line.y2)
199
+ });
200
+ for (const property of ["stroke", "strokeWidth", "strokeDash", "opacity"]) {
201
+ next = next.editGraphics({
202
+ target: config.leaderId,
203
+ property,
204
+ value: property === "strokeDash"
205
+ ? leaders.map(() => config.leader.strokeDash)
206
+ : config.leader[property]
207
+ });
208
+ }
209
+ return { program: next, count: leaders.length };
210
+ }
211
+
212
+ const materializeLabelLayout = action(
213
+ {
214
+ op: "materializeLabelLayout",
215
+ description: "Resolve concrete label positions and optional leader lines."
216
+ },
217
+ function (args = {}) {
218
+ validateOptionObject(args, MATERIALIZE_OPTIONS, "materializeLabelLayout");
219
+ const id = validateUserId(args.id, "Text mark id");
220
+ const rematerializeBase = args.rematerializeBase ?? true;
221
+ if (typeof rematerializeBase !== "boolean") {
222
+ throw new TypeError("materializeLabelLayout rematerializeBase must be a boolean.");
223
+ }
224
+ const layer = findLayer(this, id);
225
+ if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
226
+ const config = this.materializationConfigs.labelLayouts?.[id];
227
+ if (config === undefined) {
228
+ throw new Error(`Text mark "${id}" has no label layout policy.`);
229
+ }
230
+ let next = rematerializeBase
231
+ ? this.rematerializeTextMark({ id, replayLayout: false })
232
+ : this;
233
+ const resolved = resolveLabelLayout({
234
+ items: canMaterializeText(next, layer) ? layoutInputs(next, layer) : [],
235
+ bounds: concreteBounds(next, config.bounds),
236
+ axis: config.axis,
237
+ padding: config.padding,
238
+ maxDisplacement: config.maxDisplacement
239
+ });
240
+ next = next
241
+ .editGraphics({
242
+ target: id,
243
+ property: "x",
244
+ value: resolved.items.map(item => item.x)
245
+ })
246
+ .editGraphics({
247
+ target: id,
248
+ property: "y",
249
+ value: resolved.items.map(item => item.y)
250
+ });
251
+ const leaders = materializeLeaders(next, layer, config, resolved);
252
+ next = leaders.program;
253
+ return next._withMaterializationConfig(["labelLayouts", id], {
254
+ axis: config.axis,
255
+ padding: config.padding,
256
+ maxDisplacement: config.maxDisplacement,
257
+ bounds: config.bounds,
258
+ leader: config.leader,
259
+ leaderId: config.leaderId,
260
+ resolution: {
261
+ overlapBefore: resolved.overlapBefore,
262
+ overlapAfter: resolved.overlapAfter,
263
+ displaced: resolved.items.filter(item => item.distance > 0).length,
264
+ leaders: leaders.count,
265
+ maximumDisplacement: resolved.items.reduce(
266
+ (maximum, item) => Math.max(maximum, item.distance),
267
+ 0
268
+ ),
269
+ warnings: resolved.warnings
270
+ }
271
+ });
272
+ }
273
+ );
274
+
275
+ const layoutLabels = action(
276
+ {
277
+ op: "layoutLabels",
278
+ description: "Assign deterministic collision-aware layout to a text mark."
279
+ },
280
+ function (args = {}) {
281
+ validateOptionObject(args, LAYOUT_OPTIONS, "layoutLabels");
282
+ const layer = requireCompleteText(this, args.target, "layoutLabels");
283
+ const policy = normalizePolicy(args);
284
+ const generatedId = leaderId(layer.id);
285
+ const existingConfig = this.materializationConfigs.labelLayouts?.[layer.id];
286
+ if (
287
+ this.graphicSpec.objects[generatedId] !== undefined &&
288
+ existingConfig?.leaderId !== generatedId
289
+ ) {
290
+ throw new Error(`Label layout leader graphic "${generatedId}" already exists.`);
291
+ }
292
+ return this
293
+ ._withMaterializationConfig(["labelLayouts", layer.id], {
294
+ ...policy,
295
+ leaderId: generatedId
296
+ })
297
+ .materializeLabelLayout({ id: layer.id });
298
+ }
299
+ );
300
+
301
+ const removeLabelLayout = action(
302
+ {
303
+ op: "removeLabelLayout",
304
+ description: "Remove label layout and restore semantic base text positions."
305
+ },
306
+ function (args = {}) {
307
+ validateOptionObject(args, REMOVE_OPTIONS, "removeLabelLayout");
308
+ const layer = requireConfiguredText(this, args.target);
309
+ const config = this.materializationConfigs.labelLayouts[layer.id];
310
+ return removeLeaderGraphic(
311
+ this._withoutMaterializationConfig(["labelLayouts", layer.id]),
312
+ config.leaderId
313
+ ).rematerializeTextMark({ id: layer.id });
314
+ }
315
+ );
316
+
317
+ export function registerTextLabelLayoutActions(ProgramClass) {
318
+ ProgramClass.prototype.layoutLabels = layoutLabels;
319
+ ProgramClass.prototype.removeLabelLayout = removeLabelLayout;
320
+ ProgramClass.prototype.materializeLabelLayout = materializeLabelLayout;
321
+ }
@@ -0,0 +1,22 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import { isPlainObject } from "../../../core/immutable.js";
3
+ import { validateDatasetTransforms } from "../../../grammar/transforms.js";
4
+ import { hasDataset } from "../../../selectors/datasets.js";
5
+
6
+ export function validateDatasetSemanticValue(program, parsed, value) {
7
+ const property = parsed.path[0];
8
+ if (property === "values") {
9
+ if (!Array.isArray(value) || !value.every(isPlainObject)) {
10
+ throw new TypeError("Dataset values must be an array of plain row objects.");
11
+ }
12
+ return;
13
+ }
14
+ if (property === "source") {
15
+ validateUserId(value, "Dataset source id");
16
+ if (!hasDataset(program, value)) {
17
+ throw new Error(`Unknown source dataset "${value}".`);
18
+ }
19
+ return;
20
+ }
21
+ if (property === "transform") validateDatasetTransforms(value);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import { CATEGORICAL_LEGEND_CHANNELS } from "../../../core/vocabulary.js";
3
+ import { validateNonEmptySemanticString } from "./shared.js";
4
+
5
+ function validateLegend(property, value) {
6
+ if (property === "title") {
7
+ validateNonEmptySemanticString(value, "Legend title");
8
+ return;
9
+ }
10
+ if (!Array.isArray(value) || value.length === 0) {
11
+ throw new TypeError(`Legend ${property} must be a non-empty array.`);
12
+ }
13
+ if (new Set(value).size !== value.length) {
14
+ throw new Error(`Legend ${property} must not contain duplicates.`);
15
+ }
16
+ if (property === "channels") {
17
+ if (!value.every(channel => CATEGORICAL_LEGEND_CHANNELS.includes(channel))) {
18
+ throw new Error("Legend channels support only color, strokeDash, and shape.");
19
+ }
20
+ return;
21
+ }
22
+ for (const id of value) validateUserId(id, "Legend scale id");
23
+ }
24
+
25
+ export function validateGuideSemanticValue(_program, parsed, value) {
26
+ const property = parsed.path.at(-1);
27
+ if (parsed.id === "legend.series") {
28
+ validateLegend(property, value);
29
+ return;
30
+ }
31
+ if (!parsed.id.startsWith("grid.") && !parsed.id.startsWith("axis.")) return;
32
+ if (parsed.id === "axis.parallel" && property === "scales") {
33
+ if (!Array.isArray(value) || value.length < 2) {
34
+ throw new TypeError("Parallel axis scales must contain at least two ids.");
35
+ }
36
+ value.forEach(id => validateUserId(id, "Parallel axis scale id"));
37
+ } else if (property === "title") {
38
+ validateNonEmptySemanticString(value, "Axis title");
39
+ } else {
40
+ const guideKind = parsed.id.startsWith("grid.") ? "Grid" : "Axis";
41
+ validateUserId(value, `${guideKind} ${property} id`);
42
+ }
43
+ }
@@ -0,0 +1,23 @@
1
+ import { validateCoordinateType } from "../../../grammar/coordinates.js";
2
+ import { validateDatasetSemanticValue } from "./dataset.js";
3
+ import { validateGuideSemanticValue } from "./guide.js";
4
+ import { validateLayerSemanticValue } from "./layer.js";
5
+ import { validateScaleSemanticValue } from "./scale.js";
6
+ import { validateNonEmptySemanticString } from "./shared.js";
7
+
8
+ const VALIDATORS = Object.freeze({
9
+ dataset: validateDatasetSemanticValue,
10
+ layer: validateLayerSemanticValue,
11
+ scale: validateScaleSemanticValue,
12
+ coordinate(_program, parsed, value) {
13
+ if (parsed.path[0] === "type") validateCoordinateType(value);
14
+ },
15
+ guide: validateGuideSemanticValue,
16
+ title(_program, parsed, value) {
17
+ validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
18
+ }
19
+ });
20
+
21
+ export function validateSemanticValue(program, parsed, value) {
22
+ VALIDATORS[parsed.kind]?.(program, parsed, value);
23
+ }
@@ -0,0 +1,74 @@
1
+ import { validateUserId } from "../../../core/identifiers.js";
2
+ import {
3
+ COLOR_LAYOUTS,
4
+ MARK_TYPES,
5
+ STACK_MODES
6
+ } from "../../../core/vocabulary.js";
7
+ import { validateAggregate } from "../../../grammar/aggregate.js";
8
+ import {
9
+ normalizeHistogramBin,
10
+ validateHistogramBinBoundaries,
11
+ validateHistogramBinStep
12
+ } from "../../../grammar/histogram.js";
13
+ import {
14
+ validateParallelDimensions,
15
+ validateParallelKeyField,
16
+ validateParallelMissingPolicy
17
+ } from "../../../grammar/parallelCoordinates.js";
18
+ import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
19
+ import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
20
+ import { findLayer } from "../../../selectors/layers.js";
21
+ import { validateNonEmptySemanticString } from "./shared.js";
22
+
23
+ function validateLayerSource(program, parsed, value) {
24
+ validateUserId(value, "Layer source id");
25
+ if (value === parsed.id) {
26
+ throw new Error("A layer cannot use itself as its source.");
27
+ }
28
+ const source = findLayer(program, value);
29
+ if (source === undefined) {
30
+ throw new Error(`Unknown source layer "${value}".`);
31
+ }
32
+ if (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
33
+ throw new Error(
34
+ `Layer source "${value}" must be a point, bar, rule, or rect mark.`
35
+ );
36
+ }
37
+ }
38
+
39
+ export function validateLayerSemanticValue(program, parsed, value) {
40
+ const property = parsed.path.join(".");
41
+ if (property === "mark.type" && !MARK_TYPES.includes(value)) {
42
+ throw new Error(`Unknown mark type "${value}".`);
43
+ }
44
+ if (property === "source") validateLayerSource(program, parsed, value);
45
+ if (property.endsWith(".title")) {
46
+ validateNonEmptySemanticString(value, "Encoding title");
47
+ }
48
+ if (property === "encoding.pathOrder.fieldType" && value !== "quantitative") {
49
+ throw new Error("Path order field type must be quantitative.");
50
+ }
51
+ if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
52
+ if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
53
+ if (property === "encoding.parallel.dimensions") {
54
+ validateParallelDimensions(value, { normalized: true });
55
+ }
56
+ if (property === "encoding.parallel.key") validateParallelKeyField(value);
57
+ if (property === "encoding.parallel.missing") {
58
+ validateParallelMissingPolicy(value);
59
+ }
60
+ if (property.endsWith(".aggregate")) validateAggregate(value);
61
+ if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
62
+ if (property.endsWith(".bin.step")) validateHistogramBinStep(value);
63
+ if (property.endsWith(".bin.boundaries")) validateHistogramBinBoundaries(value);
64
+ if (
65
+ property.endsWith(".stack") &&
66
+ value !== null &&
67
+ !STACK_MODES.includes(value)
68
+ ) {
69
+ throw new Error(`Unsupported stack "${value}".`);
70
+ }
71
+ if (property === "encoding.color.layout" && !COLOR_LAYOUTS.includes(value)) {
72
+ throw new Error(`Unsupported color layout "${value}".`);
73
+ }
74
+ }