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.
- package/CHANGELOG.md +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- 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
|
+
}
|
package/src/layout/text.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { measureTextWidth } from "../core/textMetrics.js";
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
measureTextWidth,
|
|
5
|
+
resolveTextBounds
|
|
6
|
+
} from "../core/textMetrics.js";
|
|
4
7
|
|
|
5
8
|
function wrapCodePoints(text, maxWidth, style) {
|
|
6
9
|
const lines = [];
|
package/src/layout/title.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 {
|
|
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 !==
|
|
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
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
9
|
-
series: Object.freeze(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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);
|