ggaction 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -0,0 +1,270 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { resolveTextBounds } from "../core/textMetrics.js";
3
+ import {
4
+ validateNonNegativeFinite,
5
+ validateOptionObject
6
+ } from "../core/validation.js";
7
+
8
+ const AXES = new Set(["x", "y", "both"]);
9
+ const BOUNDS = new Set(["plot", "canvas"]);
10
+ const POLICY_OPTIONS = Object.freeze([
11
+ "axis", "padding", "maxDisplacement", "bounds"
12
+ ]);
13
+
14
+ export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
15
+ axis: "both",
16
+ padding: 3,
17
+ maxDisplacement: 48,
18
+ bounds: "plot"
19
+ });
20
+
21
+ export function normalizeLabelLayoutGeometry(options = {}) {
22
+ validateOptionObject(options, POLICY_OPTIONS, "label layout");
23
+ const normalized = {
24
+ ...DEFAULT_LABEL_LAYOUT_GEOMETRY,
25
+ ...options
26
+ };
27
+ if (!AXES.has(normalized.axis)) {
28
+ throw new Error(`Unsupported label layout axis "${normalized.axis}".`);
29
+ }
30
+ if (!BOUNDS.has(normalized.bounds)) {
31
+ throw new Error(`Unsupported label layout bounds "${normalized.bounds}".`);
32
+ }
33
+ validateNonNegativeFinite(normalized.padding, "Label layout padding");
34
+ validateNonNegativeFinite(
35
+ normalized.maxDisplacement,
36
+ "Label layout maxDisplacement"
37
+ );
38
+ return cloneAndFreeze(normalized);
39
+ }
40
+
41
+ function validateBounds(bounds) {
42
+ if (
43
+ !isPlainObject(bounds) ||
44
+ ![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
45
+ bounds.right <= bounds.left ||
46
+ bounds.bottom <= bounds.top
47
+ ) {
48
+ throw new RangeError("Label layout requires ordered finite bounds.");
49
+ }
50
+ }
51
+
52
+ function validateItem(item, ids) {
53
+ if (!isPlainObject(item)) {
54
+ throw new TypeError("Label layout items must be plain objects.");
55
+ }
56
+ if (typeof item.id !== "string" || item.id.length === 0 || ids.has(item.id)) {
57
+ throw new TypeError("Label layout items require unique non-empty ids.");
58
+ }
59
+ if (
60
+ ![item.x, item.y, item.sourceX, item.sourceY, item.fontSize]
61
+ .every(Number.isFinite) ||
62
+ item.fontSize <= 0 ||
63
+ typeof item.text !== "string" ||
64
+ item.text.length === 0
65
+ ) {
66
+ throw new TypeError(
67
+ "Label layout items require text, positive fontSize, and finite geometry."
68
+ );
69
+ }
70
+ ids.add(item.id);
71
+ }
72
+
73
+ function expanded(bounds, padding) {
74
+ const half = padding / 2;
75
+ return {
76
+ left: bounds.left - half,
77
+ right: bounds.right + half,
78
+ top: bounds.top - half,
79
+ bottom: bounds.bottom + half
80
+ };
81
+ }
82
+
83
+ function intersectionArea(first, second) {
84
+ const width = Math.min(first.right, second.right) -
85
+ Math.max(first.left, second.left);
86
+ const height = Math.min(first.bottom, second.bottom) -
87
+ Math.max(first.top, second.top);
88
+ return width > 0 && height > 0 ? width * height : 0;
89
+ }
90
+
91
+ function overflow(bounds, boundary) {
92
+ return Math.max(0, boundary.left - bounds.left) +
93
+ Math.max(0, bounds.right - boundary.right) +
94
+ Math.max(0, boundary.top - bounds.top) +
95
+ Math.max(0, bounds.bottom - boundary.bottom);
96
+ }
97
+
98
+ export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
99
+ const policy = normalizeLabelLayoutGeometry({ axis, padding, maxDisplacement });
100
+ const step = Math.max(2, policy.padding);
101
+ const limit = Math.ceil(policy.maxDisplacement / step);
102
+ const candidates = [];
103
+ for (let x = -limit; x <= limit; x += 1) {
104
+ for (let y = -limit; y <= limit; y += 1) {
105
+ if (policy.axis === "x" && y !== 0) continue;
106
+ if (policy.axis === "y" && x !== 0) continue;
107
+ const offset = { x: x * step, y: y * step };
108
+ const distanceSquared = offset.x ** 2 + offset.y ** 2;
109
+ if (Math.sqrt(distanceSquared) > policy.maxDisplacement + 1e-9) continue;
110
+ candidates.push({ ...offset, distanceSquared });
111
+ }
112
+ }
113
+ candidates.sort((first, second) =>
114
+ first.distanceSquared - second.distanceSquared ||
115
+ Math.abs(first.x) - Math.abs(second.x) ||
116
+ first.y - second.y ||
117
+ second.x - first.x
118
+ );
119
+ return cloneAndFreeze(candidates);
120
+ }
121
+
122
+ function textBounds(item, offset = { x: 0, y: 0 }) {
123
+ return resolveTextBounds({
124
+ x: item.x + offset.x,
125
+ y: item.y + offset.y,
126
+ text: item.text,
127
+ fontSize: item.fontSize,
128
+ textAlign: item.textAlign,
129
+ textBaseline: item.textBaseline,
130
+ rotation: item.rotation
131
+ });
132
+ }
133
+
134
+ function candidateScore(bounds, boundary, placed, candidate, order) {
135
+ return {
136
+ outside: overflow(bounds, boundary),
137
+ overlap: placed.reduce(
138
+ (area, prior) => area + intersectionArea(bounds, prior),
139
+ 0
140
+ ),
141
+ distanceSquared: candidate.distanceSquared,
142
+ order
143
+ };
144
+ }
145
+
146
+ function compareScore(first, second) {
147
+ return Number(first.outside > 0) - Number(second.outside > 0) ||
148
+ first.outside - second.outside ||
149
+ first.overlap - second.overlap ||
150
+ first.distanceSquared - second.distanceSquared ||
151
+ first.order - second.order;
152
+ }
153
+
154
+ function overlapPairs(items) {
155
+ const pairs = [];
156
+ for (let first = 0; first < items.length; first += 1) {
157
+ for (let second = first + 1; second < items.length; second += 1) {
158
+ if (
159
+ intersectionArea(
160
+ items[first].collisionBounds,
161
+ items[second].collisionBounds
162
+ ) > 0
163
+ ) {
164
+ pairs.push([items[first].id, items[second].id]);
165
+ }
166
+ }
167
+ }
168
+ return pairs;
169
+ }
170
+
171
+ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
172
+ if (!Array.isArray(items)) {
173
+ throw new TypeError("Label layout requires an item array.");
174
+ }
175
+ validateBounds(bounds);
176
+ const policy = normalizeLabelLayoutGeometry(options);
177
+ const ids = new Set();
178
+ for (const item of items) validateItem(item, ids);
179
+ const candidates = enumerateLabelOffsets(policy);
180
+ const base = items.map(item => ({
181
+ id: item.id,
182
+ collisionBounds: expanded(textBounds(item), policy.padding)
183
+ }));
184
+ const placed = [];
185
+ const resolved = [];
186
+ for (const item of items) {
187
+ let best;
188
+ for (const [order, candidate] of candidates.entries()) {
189
+ const boundsAtCandidate = textBounds(item, candidate);
190
+ const collisionBounds = expanded(boundsAtCandidate, policy.padding);
191
+ const score = candidateScore(
192
+ collisionBounds,
193
+ bounds,
194
+ placed,
195
+ candidate,
196
+ order
197
+ );
198
+ if (best === undefined || compareScore(score, best.score) < 0) {
199
+ best = { candidate, bounds: boundsAtCandidate, collisionBounds, score };
200
+ }
201
+ if (score.outside === 0 && score.overlap === 0) break;
202
+ }
203
+ const resolvedItem = {
204
+ ...item,
205
+ baseX: item.x,
206
+ baseY: item.y,
207
+ x: item.x + best.candidate.x,
208
+ y: item.y + best.candidate.y,
209
+ dx: best.candidate.x,
210
+ dy: best.candidate.y,
211
+ distance: Math.sqrt(best.candidate.distanceSquared),
212
+ bounds: best.bounds,
213
+ collisionBounds: best.collisionBounds
214
+ };
215
+ placed.push(resolvedItem.collisionBounds);
216
+ resolved.push(resolvedItem);
217
+ }
218
+ const beforePairs = overlapPairs(base);
219
+ const afterPairs = overlapPairs(resolved);
220
+ const outside = resolved
221
+ .filter(item => overflow(item.collisionBounds, bounds) > 0)
222
+ .map(item => item.id);
223
+ const warnings = [];
224
+ if (afterPairs.length > 0) warnings.push({ code: "overlap", pairs: afterPairs });
225
+ if (outside.length > 0) warnings.push({ code: "bounds", items: outside });
226
+ return cloneAndFreeze({
227
+ items: resolved,
228
+ overlapBefore: beforePairs.length,
229
+ overlapAfter: afterPairs.length,
230
+ warnings
231
+ });
232
+ }
233
+
234
+ function contains(bounds, point) {
235
+ return point.x >= bounds.left && point.x <= bounds.right &&
236
+ point.y >= bounds.top && point.y <= bounds.bottom;
237
+ }
238
+
239
+ export function resolveLabelLeader(item) {
240
+ if (!isPlainObject(item) || ![item.dx, item.dy].every(Number.isFinite)) {
241
+ throw new TypeError("Label leader requires a resolved label item.");
242
+ }
243
+ if (item.dx === 0 && item.dy === 0) return undefined;
244
+ const source = { x: item.sourceX, y: item.sourceY };
245
+ if (contains(item.bounds, source)) return undefined;
246
+ const center = {
247
+ x: (item.bounds.left + item.bounds.right) / 2,
248
+ y: (item.bounds.top + item.bounds.bottom) / 2
249
+ };
250
+ const dx = center.x - source.x;
251
+ const dy = center.y - source.y;
252
+ const enterX = dx > 0
253
+ ? (item.bounds.left - source.x) / dx
254
+ : dx < 0
255
+ ? (item.bounds.right - source.x) / dx
256
+ : 0;
257
+ const enterY = dy > 0
258
+ ? (item.bounds.top - source.y) / dy
259
+ : dy < 0
260
+ ? (item.bounds.bottom - source.y) / dy
261
+ : 0;
262
+ const ratio = Math.max(0, Math.min(1, Math.max(enterX, enterY)));
263
+ return cloneAndFreeze({
264
+ id: item.id,
265
+ x1: source.x,
266
+ y1: source.y,
267
+ x2: source.x + dx * ratio,
268
+ y2: source.y + dy * ratio
269
+ });
270
+ }
@@ -1,6 +1,9 @@
1
1
  import { measureTextWidth } from "../core/textMetrics.js";
2
2
 
3
- export { measureTextWidth } from "../core/textMetrics.js";
3
+ export {
4
+ measureTextWidth,
5
+ resolveTextBounds
6
+ } from "../core/textMetrics.js";
4
7
 
5
8
  function wrapCodePoints(text, maxWidth, style) {
6
9
  const lines = [];
@@ -1,4 +1,4 @@
1
- import { measureTextWidth, wrapText } from "./text.js";
1
+ import { measureTextWidth, resolveTextBounds, wrapText } from "./text.js";
2
2
 
3
3
  function lineCenters(lines, style, lineHeight, start) {
4
4
  return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
@@ -62,25 +62,15 @@ export function alignedTextAnchor(start, length, align) {
62
62
  }
63
63
 
64
64
  function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
65
- const width = measureTextWidth(text, style);
66
- const left = align === "left" ? 0 : align === "center" ? -width / 2 : -width;
67
- const right = left + width;
68
- const top = -style.fontSize / 2;
69
- const bottom = style.fontSize / 2;
70
- const cosine = Math.cos(rotation);
71
- const sine = Math.sin(rotation);
72
- const corners = [
73
- [left, top], [right, top], [right, bottom], [left, bottom]
74
- ].map(([localX, localY]) => ({
75
- x: x + localX * cosine - localY * sine,
76
- y: y + localX * sine + localY * cosine
77
- }));
78
- return {
79
- left: Math.min(...corners.map(point => point.x)),
80
- right: Math.max(...corners.map(point => point.x)),
81
- top: Math.min(...corners.map(point => point.y)),
82
- bottom: Math.max(...corners.map(point => point.y))
83
- };
65
+ return resolveTextBounds({
66
+ x,
67
+ y,
68
+ text,
69
+ ...style,
70
+ textAlign: align,
71
+ textBaseline: "middle",
72
+ rotation
73
+ });
84
74
  }
85
75
 
86
76
  function unionBounds(bounds) {
@@ -1,4 +1,56 @@
1
- import { findDataset } from "../selectors/datasets.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validateUserId } from "../core/identifiers.js";
3
+ import { findTransformPolicy } from "../grammar/transforms.js";
4
+ import { findDataset, hasDataset } from "../selectors/datasets.js";
5
+ import { requireLayer } from "../selectors/layers.js";
6
+
7
+ function validateRevisionRole(value) {
8
+ if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {
9
+ throw new TypeError(
10
+ "Derived dataset revision role must be an alphanumeric identifier."
11
+ );
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function nextRevisionId(program, owner, role) {
17
+ let revision = 1;
18
+ const prefix = `${owner}${role}Revision`;
19
+ while (hasDataset(program, `${prefix}${revision}`)) revision += 1;
20
+ return `${prefix}${revision}`;
21
+ }
22
+
23
+ export function planDerivedDataRevision(program, options = {}) {
24
+ if (!isPlainObject(options)) {
25
+ throw new TypeError("Derived dataset revision options must be a plain object.");
26
+ }
27
+ const owner = validateUserId(options.owner, "Derived dataset revision owner");
28
+ const role = validateRevisionRole(options.role);
29
+ const previous = options.previous === undefined
30
+ ? undefined
31
+ : validateUserId(options.previous, "Previous derived dataset id");
32
+ if (previous !== undefined && !hasDataset(program, previous)) {
33
+ throw new Error(`Unknown previous derived dataset "${previous}".`);
34
+ }
35
+ if (!Array.isArray(options.consumers)) {
36
+ throw new TypeError("Derived dataset revision consumers must be an array.");
37
+ }
38
+ const consumers = options.consumers.map(id => {
39
+ const validated = validateUserId(id, "Derived dataset consumer id");
40
+ requireLayer(program, validated);
41
+ return validated;
42
+ });
43
+ if (new Set(consumers).size !== consumers.length) {
44
+ throw new Error("Derived dataset revision consumers must be unique.");
45
+ }
46
+ const id = nextRevisionId(program, owner, role);
47
+ return cloneAndFreeze({
48
+ id,
49
+ ...(previous === undefined ? {} : { previous }),
50
+ rebinds: consumers.map(consumer => ({ id: consumer, data: id })),
51
+ ...(previous === undefined ? {} : { release: { id: previous } })
52
+ });
53
+ }
2
54
 
3
55
  export function findUpstreamTransform(program, dataset, type) {
4
56
  const visited = new Set();
@@ -8,7 +60,9 @@ export function findUpstreamTransform(program, dataset, type) {
8
60
  if (current.transform?.length !== 1) return undefined;
9
61
  const transform = current.transform[0];
10
62
  if (transform.type === type) return transform;
11
- if (transform.type !== "markFilter") return undefined;
63
+ if (findTransformPolicy(transform.type)?.provenanceTransparent !== true) {
64
+ return undefined;
65
+ }
12
66
  current = findDataset(program, current.source);
13
67
  }
14
68
  return undefined;
@@ -14,12 +14,13 @@ import {
14
14
  needsCanvasScaleRematerialization,
15
15
  planScaleGuideRematerialization
16
16
  } from "./scaleGuideDependencies.js";
17
+ import { planLayoutRematerialization } from "./layout.js";
17
18
 
18
- function hasTitle(program) {
19
- return (
20
- program.semanticSpec.title.text !== undefined &&
21
- program.titleConfig !== undefined
22
- );
19
+ function layerScaleIds(layer) {
20
+ return [
21
+ ...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
22
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
23
+ ].filter(scale => scale !== undefined);
23
24
  }
24
25
 
25
26
  export function planCanvasRematerialization(program) {
@@ -37,9 +38,7 @@ export function planCanvasRematerialization(program) {
37
38
  if (needsCanvasScaleRematerialization(program, scale)) {
38
39
  const deferredConsumers = program.semanticSpec.layers.filter(layer =>
39
40
  canDeferScaleConsumerApplication(layer) &&
40
- Object.values(layer.encoding ?? {}).some(
41
- encoding => encoding?.scale === scale.id
42
- )
41
+ layerScaleIds(layer).includes(scale.id)
43
42
  );
44
43
  const canDeferMarks = deferredConsumers.length === 0 ||
45
44
  deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
@@ -61,17 +60,13 @@ export function planCanvasRematerialization(program) {
61
60
  if (hasMaterializedLegend(program)) {
62
61
  guides.push({ op: "rematerializeLegend" });
63
62
  }
64
- const layout = hasTitle(program) ? [{ op: "rematerializeTitle" }] : [];
63
+ const layout = planLayoutRematerialization(program);
65
64
  return buildMaterializationPlan({ scales, marks, guides, layout });
66
65
  }
67
66
 
68
67
  export function planLayerDataRematerialization(program, id) {
69
68
  const layer = requireLayer(program, id);
70
- const scaleIds = [...new Set(
71
- Object.values(layer.encoding ?? {})
72
- .map(encoding => encoding?.scale)
73
- .filter(scale => scale !== undefined)
74
- )];
69
+ const scaleIds = [...new Set(layerScaleIds(layer))];
75
70
  const markStep = getMarkMaterializationStep(program, layer);
76
71
  const scales = scaleIds.map(scale => ({
77
72
  op: "rematerializeScale",
@@ -5,27 +5,66 @@ const CATEGORICAL_COMPONENTS = Object.freeze([
5
5
  "Labels", "Title", "Background"
6
6
  ]);
7
7
 
8
- const LEGEND_GRAPHICS = Object.freeze({
9
- series: Object.freeze(CATEGORICAL_COMPONENTS.map(
10
- component => `seriesLegend${component}`
11
- )),
12
- color: Object.freeze(CATEGORICAL_COMPONENTS.map(
13
- component => `colorLegend${component}`
14
- )),
15
- size: Object.freeze([
16
- "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
17
- ]),
18
- gradient: Object.freeze([
19
- "colorGradientBackground", "colorGradientStrips", "colorGradientTicks",
20
- "colorGradientLabels", "colorGradientTitle"
21
- ]),
22
- interval: Object.freeze([
23
- "colorLegendSymbols", "colorLegendLabels", "colorLegendTitle"
24
- ]),
25
- opacity: Object.freeze([
26
- "opacityLegendBackground", "opacityLegendSymbols", "opacityLegendLabels",
27
- "opacityLegendTitle"
28
- ])
8
+ const LEGEND_RESOURCE_POLICIES = Object.freeze({
9
+ series: Object.freeze({
10
+ semanticKind: "series",
11
+ family: "categorical",
12
+ rematerializeOp: undefined,
13
+ graphicIds: Object.freeze(CATEGORICAL_COMPONENTS.map(
14
+ component => `seriesLegend${component}`
15
+ ))
16
+ }),
17
+ color: Object.freeze({
18
+ semanticKind: "color",
19
+ family: "categorical",
20
+ rematerializeOp: undefined,
21
+ graphicIds: Object.freeze(CATEGORICAL_COMPONENTS.map(
22
+ component => `colorLegend${component}`
23
+ ))
24
+ }),
25
+ size: Object.freeze({
26
+ semanticKind: "size",
27
+ family: "size",
28
+ rematerializeOp: "rematerializeSizeLegend",
29
+ graphicIds: Object.freeze([
30
+ "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
31
+ ])
32
+ }),
33
+ gradient: Object.freeze({
34
+ semanticKind: "color",
35
+ family: "continuous",
36
+ rematerializeOp: "rematerializeGradientLegend",
37
+ graphicIds: Object.freeze([
38
+ "colorGradientBackground", "colorGradientStrips", "colorGradientTicks",
39
+ "colorGradientLabels", "colorGradientTitle"
40
+ ])
41
+ }),
42
+ interval: Object.freeze({
43
+ semanticKind: "color",
44
+ family: "interval",
45
+ rematerializeOp: "rematerializeIntervalLegend",
46
+ graphicIds: Object.freeze([
47
+ "colorLegendSymbols", "colorLegendLabels", "colorLegendTitle"
48
+ ])
49
+ }),
50
+ opacity: Object.freeze({
51
+ semanticKind: "opacity",
52
+ family: "continuous",
53
+ rematerializeOp: "rematerializeOpacityLegend",
54
+ graphicIds: Object.freeze([
55
+ "opacityLegendBackground", "opacityLegendSymbols", "opacityLegendLabels",
56
+ "opacityLegendTitle"
57
+ ])
58
+ }),
59
+ strokeWidth: Object.freeze({
60
+ semanticKind: "strokeWidth",
61
+ family: "strokeWidth",
62
+ rematerializeOp: "rematerializeStrokeWidthLegend",
63
+ graphicIds: Object.freeze([
64
+ "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
65
+ "strokeWidthLegendTitle"
66
+ ])
67
+ })
29
68
  });
30
69
 
31
70
  export function axisGraphicIds(channel) {
@@ -34,11 +73,22 @@ export function axisGraphicIds(channel) {
34
73
  }
35
74
 
36
75
  export function legendGraphicIds(kind) {
37
- const ids = LEGEND_GRAPHICS[kind];
38
- if (ids === undefined) {
76
+ return legendResourcePolicy(kind).graphicIds;
77
+ }
78
+
79
+ export function legendResourcePolicy(kind) {
80
+ const policy = LEGEND_RESOURCE_POLICIES[kind];
81
+ if (policy === undefined) {
39
82
  throw new Error(`Unknown legend graphic kind "${kind}".`);
40
83
  }
41
- return ids;
84
+ return policy;
85
+ }
86
+
87
+ export function legendResourcePolicies() {
88
+ return Object.entries(LEGEND_RESOURCE_POLICIES).map(([kind, policy]) => ({
89
+ kind,
90
+ ...policy
91
+ }));
42
92
  }
43
93
 
44
94
  export function allLegendGraphicIds(kinds) {
@@ -0,0 +1,29 @@
1
+ import { getMarkMaterializationStep } from "./marks/index.js";
2
+ import { buildMaterializationPlan } from "./planner.js";
3
+ import { requireLayer } from "../selectors/layers.js";
4
+
5
+ export function planHorizonRematerialization(program, target) {
6
+ const layer = requireLayer(
7
+ program,
8
+ target,
9
+ `Unknown Horizon materialization target "${target}"`
10
+ );
11
+ const scaleIds = new Set([
12
+ layer.encoding?.x?.scale,
13
+ layer.encoding?.y?.scale,
14
+ layer.encoding?.color?.scale
15
+ ].filter(Boolean));
16
+ const ordered = [
17
+ layer,
18
+ ...program.semanticSpec.layers.filter(candidate => candidate.id !== target)
19
+ ];
20
+ const marks = ordered.flatMap(candidate => {
21
+ const consumesScale = Object.values(candidate.encoding ?? {}).some(
22
+ encoding => scaleIds.has(encoding?.scale)
23
+ );
24
+ if (!consumesScale) return [];
25
+ const step = getMarkMaterializationStep(program, candidate);
26
+ return step === undefined ? [] : [step];
27
+ });
28
+ return buildMaterializationPlan({ marks });
29
+ }
@@ -0,0 +1,15 @@
1
+ const LAYOUT_CONSUMER_POLICIES = Object.freeze([
2
+ Object.freeze({
3
+ applies(program) {
4
+ return program.semanticSpec.title.text !== undefined &&
5
+ program.titleConfig !== undefined;
6
+ },
7
+ step: Object.freeze({ op: "rematerializeTitle" })
8
+ })
9
+ ]);
10
+
11
+ export function planLayoutRematerialization(program) {
12
+ return LAYOUT_CONSUMER_POLICIES.flatMap(policy =>
13
+ policy.applies(program) ? [policy.step] : []
14
+ );
15
+ }
@@ -1,5 +1,6 @@
1
1
  import { findDataset } from "../../selectors/datasets.js";
2
2
  import { findLayer } from "../../selectors/layers.js";
3
+ import { findCoordinate } from "../../selectors/coordinates.js";
3
4
  import { isAggregate } from "../../grammar/aggregate.js";
4
5
  import {
5
6
  BAR_GRAINS,
@@ -32,6 +33,14 @@ export function canMaterializePoint(_program, layer) {
32
33
  }
33
34
 
34
35
  export function canMaterializeLine(program, layer) {
36
+ const parallel = layer.encoding?.parallel;
37
+ if (parallel !== undefined) {
38
+ const coordinate = findCoordinate(program, layer.coordinate);
39
+ return layer.mark?.type === "line" &&
40
+ coordinate?.type === "parallel" &&
41
+ parallel.dimensions?.length >= 2 &&
42
+ parallel.dimensions.every(dimension => dimension.scale !== undefined);
43
+ }
35
44
  const dataset = findDataset(program, layer.data);
36
45
  const interval = dataset?.transform?.some(item => item.type === "interval");
37
46
  if (hasPolarPositionScales(layer)) {
@@ -62,6 +71,7 @@ export function canMaterializeArea(program, layer) {
62
71
  }
63
72
  const dataset = findDataset(program, layer.data);
64
73
  const densityTransform = findUpstreamTransform(program, dataset, "density");
74
+ if (isIntentionallyEmptyArea(program, layer)) return false;
65
75
  const completeDensity =
66
76
  densityTransform !== undefined &&
67
77
  (densityTransform.groupBy === undefined ||
@@ -73,6 +83,16 @@ export function canMaterializeArea(program, layer) {
73
83
  );
74
84
  }
75
85
 
86
+ export function isIntentionallyEmptyArea(program, layer) {
87
+ if (
88
+ layer.mark?.type !== "area" ||
89
+ !Array.isArray(program.semanticSpec?.datasets)
90
+ ) return false;
91
+ const dataset = findDataset(program, layer.data);
92
+ return dataset?.values?.length === 0 &&
93
+ findUpstreamTransform(program, dataset, "horizon") !== undefined;
94
+ }
95
+
76
96
  export function canMaterializeArc(_program, layer) {
77
97
  if (
78
98
  layer.mark?.type !== "arc" ||
@@ -81,7 +101,7 @@ export function canMaterializeArc(_program, layer) {
81
101
  ) {
82
102
  return false;
83
103
  }
84
- if (layer.encoding.theta.aggregate === "count") {
104
+ if (["count", "sum"].includes(layer.encoding.theta.aggregate)) {
85
105
  return layer.encoding?.radius === undefined;
86
106
  }
87
107
  return (
@@ -96,6 +96,9 @@ export function getEncodingMaterializationStages(program, layer, channel, scale)
96
96
  )
97
97
  : [layer];
98
98
  const marks = candidates.flatMap(candidate => {
99
+ if (policy.encoding?.skipRematerialization?.(program, candidate) === true) {
100
+ return [];
101
+ }
99
102
  const step = policy.encoding?.completeOnly === true
100
103
  ? getMarkMaterializationStep(program, candidate)
101
104
  : getMarkRematerializationStep(candidate);