ggaction 0.0.6 → 0.0.8
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 +43 -0
- package/README.md +77 -14
- package/package.json +19 -2
- package/src/BasicChartProgram.js +10 -0
- package/src/actions/basic.js +21 -0
- package/src/actions/canvas/actions.js +42 -0
- package/src/actions/canvas/index.js +5 -1
- package/src/actions/categoryOrder/index.js +111 -0
- package/src/actions/charts/basic.js +13 -0
- package/src/actions/data/basic.js +11 -0
- package/src/actions/data/basicBin2d.js +43 -0
- package/src/actions/data/bin2d.js +1 -26
- package/src/actions/data/bin2dMaterialize.js +29 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/timeUnit.js +48 -0
- package/src/actions/encodings/angle.js +77 -0
- package/src/actions/encodings/appearance.js +6 -2
- package/src/actions/encodings/basic.js +21 -0
- package/src/actions/encodings/color/index.js +17 -13
- package/src/actions/encodings/color/layout.js +2 -0
- package/src/actions/encodings/color/policy.js +3 -0
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/index.js +5 -1
- package/src/actions/encodings/position/policies/area.js +40 -2
- package/src/actions/encodings/position/policies/bar.js +6 -0
- package/src/actions/encodings/position/policies/index.js +2 -0
- package/src/actions/encodings/position/policies/tick.js +19 -0
- package/src/actions/encodings/ranged.js +6 -2
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/guides/axes/index.js +10 -2
- package/src/actions/guides/basic.js +19 -0
- package/src/actions/guides/grids/grid.js +7 -3
- package/src/actions/guides/grids/index.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +14 -2
- package/src/actions/guides/legends/continuous/common.js +17 -5
- package/src/actions/guides/legends/continuous/gradient.js +12 -8
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/opacity.js +70 -17
- package/src/actions/guides/legends/edit.js +13 -8
- package/src/actions/guides/legends/lane.js +468 -0
- package/src/actions/guides/legends/remove.js +3 -7
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +3 -1
- package/src/actions/marks/area/materialize.js +12 -3
- package/src/actions/marks/bar/index.js +5 -1
- package/src/actions/marks/basic.js +11 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/line/actions.js +5 -1
- package/src/actions/marks/line/index.js +4 -1
- package/src/actions/marks/point/index.js +5 -1
- package/src/actions/marks/point/materialize.js +17 -5
- package/src/actions/marks/rect/actions.js +5 -1
- package/src/actions/marks/rect/index.js +4 -1
- package/src/actions/marks/tick/actions.js +225 -0
- package/src/actions/marks/tick/index.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +2 -0
- package/src/actions/scales/consumers/index.js +8 -0
- package/src/actions/scales/consumers/seriesLayout.js +22 -2
- package/src/actions/scales/index.js +5 -1
- package/src/actions/scales/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- package/src/basic.js +2 -0
- package/src/core/vocabulary.js +7 -3
- package/src/grammar/areaSeries.js +112 -2
- package/src/grammar/categoryOrder.js +138 -0
- package/src/grammar/direction.js +46 -0
- package/src/grammar/facets/index.js +1 -1
- package/src/grammar/pointShapes.js +34 -6
- package/src/grammar/positionCompatibility.js +4 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/seriesLayout.js +10 -2
- package/src/grammar/timeUnit.js +108 -0
- package/src/grammar/transforms.js +6 -0
- package/src/grammar/window.js +73 -7
- package/src/layout/legendLane.js +338 -0
- package/src/materialization/marks/capabilities.js +9 -0
- package/src/materialization/marks/index.js +2 -1
- package/src/materialization/marks/policies.js +13 -0
- package/src/materialization/scales/policies/series.js +9 -0
- package/src/materialization/scales/resolve.js +25 -3
- package/src/materialization/selection/items/index.js +1 -0
- package/src/materialization/selection/items/path.js +4 -1
- package/src/materialization/selection/items/tick.js +42 -0
- package/src/materialization/selection/policies/index.js +2 -0
- package/src/materialization/selection/policies/tick.js +10 -0
- package/src/renderers/canvas/index.js +96 -60
- package/src/renderers/canvas/text.js +3 -14
- package/src/renderers/pdf.js +112 -0
- package/src/renderers/svg.js +559 -0
- package/src/renderers/text.js +11 -0
- package/types/basic.d.ts +107 -0
- package/types/index.d.ts +10 -0
- package/types/pdf.d.ts +26 -0
- package/types/program.d.ts +96 -3
- package/types/svg.d.ts +11 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
export const SIDE_LEGEND_BLOCK_GAP = 24;
|
|
2
|
+
export const HORIZONTAL_LEGEND_BLOCK_GAP = 40;
|
|
3
|
+
export const SIDE_LEGEND_SYMBOL_CENTER = 16;
|
|
4
|
+
export const SIDE_LEGEND_LABEL_START = 44;
|
|
5
|
+
export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
|
|
6
|
+
|
|
7
|
+
function unionBounds(bounds) {
|
|
8
|
+
return {
|
|
9
|
+
left: Math.min(...bounds.map(item => item.left)),
|
|
10
|
+
right: Math.max(...bounds.map(item => item.right)),
|
|
11
|
+
top: Math.min(...bounds.map(item => item.top)),
|
|
12
|
+
bottom: Math.max(...bounds.map(item => item.bottom))
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function decoration(border) {
|
|
17
|
+
return border === false
|
|
18
|
+
? 0
|
|
19
|
+
: border.padding + border.lineWidth / 2;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function horizontalExtent(blocks) {
|
|
23
|
+
const left = [0];
|
|
24
|
+
const right = [0];
|
|
25
|
+
for (const block of blocks) {
|
|
26
|
+
if (block.title !== undefined) right.push(block.title.width);
|
|
27
|
+
left.push(
|
|
28
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX
|
|
29
|
+
);
|
|
30
|
+
right.push(
|
|
31
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX
|
|
32
|
+
);
|
|
33
|
+
right.push(SIDE_LEGEND_LABEL_START + block.labels.width);
|
|
34
|
+
}
|
|
35
|
+
return { left: Math.min(...left), right: Math.max(...right) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function placeBlock(block, cursor) {
|
|
39
|
+
let dy;
|
|
40
|
+
if (block.title === undefined) {
|
|
41
|
+
dy = cursor - block.bounds.top;
|
|
42
|
+
} else {
|
|
43
|
+
const desiredTitleY = Math.ceil(cursor + block.title.fontSize / 2);
|
|
44
|
+
dy = desiredTitleY - block.title.y;
|
|
45
|
+
const futureTop = block.bounds.top + dy;
|
|
46
|
+
if (futureTop < cursor) dy += cursor - futureTop;
|
|
47
|
+
}
|
|
48
|
+
const bounds = {
|
|
49
|
+
left: block.bounds.left,
|
|
50
|
+
right: block.bounds.right,
|
|
51
|
+
top: block.bounds.top + dy,
|
|
52
|
+
bottom: block.bounds.bottom + dy
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
id: block.id,
|
|
56
|
+
dy,
|
|
57
|
+
titleDx: block.title === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: -block.title.x,
|
|
60
|
+
symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
|
|
61
|
+
labelDx: SIDE_LEGEND_LABEL_START - block.labels.x,
|
|
62
|
+
bounds
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function overlap(a, b) {
|
|
67
|
+
return a.left < b.right && a.right > b.left &&
|
|
68
|
+
a.top < b.bottom && a.bottom > b.top;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveSideLegendLane({
|
|
72
|
+
side,
|
|
73
|
+
plot,
|
|
74
|
+
canvas,
|
|
75
|
+
groups,
|
|
76
|
+
axisBounds
|
|
77
|
+
}) {
|
|
78
|
+
if (!["right", "left"].includes(side)) {
|
|
79
|
+
throw new Error(`Unsupported side legend lane "${side}".`);
|
|
80
|
+
}
|
|
81
|
+
const blocks = groups.flatMap(group => group.blocks);
|
|
82
|
+
if (blocks.length < 2) return undefined;
|
|
83
|
+
const laneExtent = horizontalExtent(blocks);
|
|
84
|
+
const offset = Math.max(...blocks.map(block => block.offset));
|
|
85
|
+
const titleStartX = side === "right"
|
|
86
|
+
? plot.x + plot.width + offset
|
|
87
|
+
: plot.x - offset - laneExtent.right;
|
|
88
|
+
const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
|
|
89
|
+
const labelStartX = titleStartX + SIDE_LEGEND_LABEL_START;
|
|
90
|
+
const first = blocks[0];
|
|
91
|
+
let cursor = first.title === undefined
|
|
92
|
+
? plot.y + 12
|
|
93
|
+
: plot.y + 20 - first.title.fontSize / 2;
|
|
94
|
+
const placements = [];
|
|
95
|
+
const backgrounds = [];
|
|
96
|
+
const occupiedGroups = [];
|
|
97
|
+
|
|
98
|
+
for (const group of groups) {
|
|
99
|
+
const inset = decoration(group.border);
|
|
100
|
+
if (occupiedGroups.length > 0) cursor += inset;
|
|
101
|
+
const groupPlacements = [];
|
|
102
|
+
for (const block of group.blocks) {
|
|
103
|
+
const placement = placeBlock(block, cursor);
|
|
104
|
+
groupPlacements.push(placement);
|
|
105
|
+
placements.push({
|
|
106
|
+
...placement,
|
|
107
|
+
titleDx: placement.titleDx === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: placement.titleDx + titleStartX,
|
|
110
|
+
symbolDx: placement.symbolDx + titleStartX,
|
|
111
|
+
labelDx: placement.labelDx + titleStartX
|
|
112
|
+
});
|
|
113
|
+
cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
114
|
+
}
|
|
115
|
+
const vertical = unionBounds(groupPlacements.map(item => item.bounds));
|
|
116
|
+
const groupExtent = horizontalExtent(group.blocks);
|
|
117
|
+
const foreground = {
|
|
118
|
+
left: titleStartX + groupExtent.left,
|
|
119
|
+
right: titleStartX + groupExtent.right,
|
|
120
|
+
top: vertical.top,
|
|
121
|
+
bottom: vertical.bottom
|
|
122
|
+
};
|
|
123
|
+
const occupied = {
|
|
124
|
+
left: foreground.left - inset,
|
|
125
|
+
right: foreground.right + inset,
|
|
126
|
+
top: foreground.top - inset,
|
|
127
|
+
bottom: foreground.bottom + inset
|
|
128
|
+
};
|
|
129
|
+
occupiedGroups.push(occupied);
|
|
130
|
+
if (group.backgroundId !== undefined) {
|
|
131
|
+
backgrounds.push({
|
|
132
|
+
id: group.backgroundId,
|
|
133
|
+
x: foreground.left - group.border.padding,
|
|
134
|
+
y: foreground.top - group.border.padding,
|
|
135
|
+
width: foreground.right - foreground.left + group.border.padding * 2,
|
|
136
|
+
height: foreground.bottom - foreground.top + group.border.padding * 2
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
cursor = occupied.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const occupied = unionBounds(occupiedGroups);
|
|
143
|
+
if (
|
|
144
|
+
occupied.left < 0 || occupied.right > canvas.width ||
|
|
145
|
+
occupied.top < 0 || occupied.bottom > canvas.height
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(`Legend lane requires more ${side}-margin or vertical Canvas space.`);
|
|
148
|
+
}
|
|
149
|
+
if (axisBounds !== undefined && overlap(occupied, axisBounds)) {
|
|
150
|
+
throw new Error(`${side[0].toUpperCase()}${side.slice(1)} legend lane and y-axis guides require more margin space.`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
side,
|
|
154
|
+
titleStartX,
|
|
155
|
+
symbolCenterX,
|
|
156
|
+
labelStartX,
|
|
157
|
+
placements,
|
|
158
|
+
backgrounds,
|
|
159
|
+
occupied
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function translateBounds(bounds, dy) {
|
|
164
|
+
return {
|
|
165
|
+
left: bounds.left,
|
|
166
|
+
right: bounds.right,
|
|
167
|
+
top: bounds.top + dy,
|
|
168
|
+
bottom: bounds.bottom + dy
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function translateBoundsX(bounds, dx) {
|
|
173
|
+
return {
|
|
174
|
+
left: bounds.left + dx,
|
|
175
|
+
right: bounds.right + dx,
|
|
176
|
+
top: bounds.top,
|
|
177
|
+
bottom: bounds.bottom
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function expandBounds(bounds, inset) {
|
|
182
|
+
return {
|
|
183
|
+
left: bounds.left - inset,
|
|
184
|
+
right: bounds.right + inset,
|
|
185
|
+
top: bounds.top - inset,
|
|
186
|
+
bottom: bounds.bottom + inset
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function packHorizontalRows(groups, plot) {
|
|
191
|
+
const rows = [];
|
|
192
|
+
let row = [];
|
|
193
|
+
let cursor = plot.x;
|
|
194
|
+
for (const group of groups) {
|
|
195
|
+
const interval = expandBounds(group.horizontal, group.inset);
|
|
196
|
+
const width = interval.right - interval.left;
|
|
197
|
+
if (width > plot.width) {
|
|
198
|
+
throw new Error("Horizontal legend block requires more plot width.");
|
|
199
|
+
}
|
|
200
|
+
let dx = cursor - interval.left;
|
|
201
|
+
let placed = translateBoundsX(interval, dx);
|
|
202
|
+
if (row.length > 0 && placed.right > plot.x + plot.width) {
|
|
203
|
+
rows.push(row);
|
|
204
|
+
row = [];
|
|
205
|
+
cursor = plot.x;
|
|
206
|
+
dx = cursor - interval.left;
|
|
207
|
+
placed = translateBoundsX(interval, dx);
|
|
208
|
+
}
|
|
209
|
+
row.push({ group, interval: placed, dx });
|
|
210
|
+
cursor = placed.right + HORIZONTAL_LEGEND_BLOCK_GAP;
|
|
211
|
+
}
|
|
212
|
+
if (row.length > 0) rows.push(row);
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeHorizontalRow(entries) {
|
|
217
|
+
const stacked = entries.filter(entry =>
|
|
218
|
+
entry.group.title !== undefined && entry.group.inline !== true
|
|
219
|
+
);
|
|
220
|
+
const commonTitleY = stacked[0]?.group.title.y;
|
|
221
|
+
const titleDescent = stacked.length === 0
|
|
222
|
+
? 0
|
|
223
|
+
: Math.max(...stacked.map(
|
|
224
|
+
entry => entry.group.title.bounds.bottom - entry.group.title.y
|
|
225
|
+
));
|
|
226
|
+
const elementAnchor = commonTitleY === undefined
|
|
227
|
+
? (entries[0].group.element.top + entries[0].group.element.bottom) / 2
|
|
228
|
+
: commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
|
|
229
|
+
return entries.map(({ group, dx }) => {
|
|
230
|
+
const contentDy = commonTitleY === undefined
|
|
231
|
+
? elementAnchor - (group.element.top + group.element.bottom) / 2
|
|
232
|
+
: elementAnchor - group.element.top;
|
|
233
|
+
const titleDy = group.title === undefined
|
|
234
|
+
? 0
|
|
235
|
+
: group.inline === true
|
|
236
|
+
? contentDy
|
|
237
|
+
: commonTitleY - group.title.y;
|
|
238
|
+
const foreground = unionBounds([
|
|
239
|
+
...(group.title === undefined
|
|
240
|
+
? []
|
|
241
|
+
: [translateBoundsX(translateBounds(group.title.bounds, titleDy), dx)]),
|
|
242
|
+
translateBoundsX(translateBounds(group.content, contentDy), dx)
|
|
243
|
+
]);
|
|
244
|
+
return {
|
|
245
|
+
id: group.id,
|
|
246
|
+
dx,
|
|
247
|
+
titleDy,
|
|
248
|
+
contentDy,
|
|
249
|
+
foreground,
|
|
250
|
+
occupied: expandBounds(foreground, group.inset),
|
|
251
|
+
padding: group.padding,
|
|
252
|
+
backgroundId: group.backgroundId
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function translateHorizontalPlacement(placement, dy) {
|
|
258
|
+
return {
|
|
259
|
+
...placement,
|
|
260
|
+
titleDy: placement.titleDy + dy,
|
|
261
|
+
contentDy: placement.contentDy + dy,
|
|
262
|
+
foreground: translateBounds(placement.foreground, dy),
|
|
263
|
+
occupied: translateBounds(placement.occupied, dy)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resolveHorizontalLegendLane({
|
|
268
|
+
edge,
|
|
269
|
+
plot,
|
|
270
|
+
canvas,
|
|
271
|
+
groups,
|
|
272
|
+
collisionBounds = []
|
|
273
|
+
}) {
|
|
274
|
+
if (!["top", "bottom"].includes(edge)) {
|
|
275
|
+
throw new Error(`Unsupported horizontal legend lane "${edge}".`);
|
|
276
|
+
}
|
|
277
|
+
if (groups.length < 2) return undefined;
|
|
278
|
+
const packed = packHorizontalRows(groups, plot);
|
|
279
|
+
const normalized = packed.map(normalizeHorizontalRow);
|
|
280
|
+
const placements = [];
|
|
281
|
+
let previousRowBounds;
|
|
282
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
283
|
+
const row = normalized[index];
|
|
284
|
+
const rowBounds = unionBounds(row.map(item => item.occupied));
|
|
285
|
+
let dy;
|
|
286
|
+
if (index === 0) {
|
|
287
|
+
const anchor = edge === "top"
|
|
288
|
+
? Math.max(...packed[index].map(item => item.group.horizontal.bottom))
|
|
289
|
+
: Math.min(...packed[index].map(item => item.group.horizontal.top));
|
|
290
|
+
dy = edge === "top"
|
|
291
|
+
? anchor - rowBounds.bottom
|
|
292
|
+
: anchor - rowBounds.top;
|
|
293
|
+
} else {
|
|
294
|
+
dy = edge === "bottom"
|
|
295
|
+
? previousRowBounds.bottom + HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.top
|
|
296
|
+
: previousRowBounds.top - HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.bottom;
|
|
297
|
+
}
|
|
298
|
+
const translated = row.map(item => translateHorizontalPlacement(item, dy));
|
|
299
|
+
placements.push(...translated);
|
|
300
|
+
previousRowBounds = unionBounds(translated.map(item => item.occupied));
|
|
301
|
+
}
|
|
302
|
+
const occupied = unionBounds(placements.map(item => item.occupied));
|
|
303
|
+
if (placements.some(placement =>
|
|
304
|
+
placement.occupied.left < 0 || placement.occupied.right > canvas.width ||
|
|
305
|
+
placement.occupied.top < 0 || placement.occupied.bottom > canvas.height
|
|
306
|
+
)) {
|
|
307
|
+
throw new Error(`Legend lane requires more ${edge}-margin or Canvas space.`);
|
|
308
|
+
}
|
|
309
|
+
if (placements.some(placement =>
|
|
310
|
+
collisionBounds.some(bounds => overlap(placement.occupied, bounds))
|
|
311
|
+
)) {
|
|
312
|
+
const owner = edge === "top" ? "chart titles" : "x-axis guides";
|
|
313
|
+
throw new Error(
|
|
314
|
+
`${edge[0].toUpperCase()}${edge.slice(1)} legend lane and ${owner} require more margin space.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
edge,
|
|
319
|
+
placements: placements.map(placement => ({
|
|
320
|
+
...placement,
|
|
321
|
+
...(placement.backgroundId === undefined
|
|
322
|
+
? {}
|
|
323
|
+
: {
|
|
324
|
+
background: {
|
|
325
|
+
id: placement.backgroundId,
|
|
326
|
+
x: placement.foreground.left - placement.padding,
|
|
327
|
+
y: placement.foreground.top - placement.padding,
|
|
328
|
+
width: placement.foreground.right - placement.foreground.left +
|
|
329
|
+
placement.padding * 2,
|
|
330
|
+
height: placement.foreground.bottom - placement.foreground.top +
|
|
331
|
+
placement.padding * 2
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
})),
|
|
335
|
+
occupied,
|
|
336
|
+
rowCount: packed.length
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -32,6 +32,10 @@ export function canMaterializePoint(_program, layer) {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function canMaterializeTick(_program, layer) {
|
|
36
|
+
return layer.mark?.type === "tick" && hasCartesianPositionScales(layer);
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
export function canMaterializeLine(program, layer) {
|
|
36
40
|
const parallel = layer.encoding?.parallel;
|
|
37
41
|
if (parallel !== undefined) {
|
|
@@ -76,8 +80,13 @@ export function canMaterializeArea(program, layer) {
|
|
|
76
80
|
densityTransform !== undefined &&
|
|
77
81
|
(densityTransform.groupBy === undefined ||
|
|
78
82
|
layer.encoding?.group?.field === densityTransform.groupBy);
|
|
83
|
+
const completeCenter =
|
|
84
|
+
densityTransform === undefined &&
|
|
85
|
+
layer.encoding?.y?.stack === "center" &&
|
|
86
|
+
layer.encoding?.group?.fieldType === "nominal";
|
|
79
87
|
return (
|
|
80
88
|
completeDensity ||
|
|
89
|
+
completeCenter ||
|
|
81
90
|
layer.encoding?.y2?.scale === layer.encoding.y.scale ||
|
|
82
91
|
layer.encoding?.x2?.scale === layer.encoding.x.scale
|
|
83
92
|
);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
canMaterializeRect,
|
|
8
8
|
canMaterializeRule,
|
|
9
9
|
canMaterializeText,
|
|
10
|
+
canMaterializeTick,
|
|
10
11
|
isIntentionallyEmptyArea
|
|
11
12
|
} from "./capabilities.js";
|
|
12
13
|
|
|
@@ -24,6 +25,18 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
|
|
|
24
25
|
}),
|
|
25
26
|
rematerializeIncompleteExisting: true
|
|
26
27
|
}),
|
|
28
|
+
tick: Object.freeze({
|
|
29
|
+
canMaterialize: canMaterializeTick,
|
|
30
|
+
op: "rematerializeTickMark",
|
|
31
|
+
positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
|
|
32
|
+
encoding: Object.freeze({ scaleFirst: true }),
|
|
33
|
+
scaleApplication: Object.freeze({
|
|
34
|
+
deferWithMark: true,
|
|
35
|
+
position: "rematerialize",
|
|
36
|
+
default: "defer"
|
|
37
|
+
}),
|
|
38
|
+
rematerializeIncompleteExisting: true
|
|
39
|
+
}),
|
|
27
40
|
line: Object.freeze({
|
|
28
41
|
canMaterialize: canMaterializeLine,
|
|
29
42
|
op: "rematerializeLineMark",
|
|
@@ -40,6 +40,15 @@ export function resolveSeriesLayoutDomain({
|
|
|
40
40
|
...activeLayouts.flatMap(item => item.values),
|
|
41
41
|
...directConsumers.flatMap(item => item.values)
|
|
42
42
|
];
|
|
43
|
+
if (layout === "center" && scale.domain !== "auto") {
|
|
44
|
+
const minimum = Math.min(...values);
|
|
45
|
+
const maximum = Math.max(...values);
|
|
46
|
+
if (Math.min(...scale.domain) > minimum || Math.max(...scale.domain) < maximum) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Center layout scale "${id}" explicit domain must contain every centered bound.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
43
52
|
if (
|
|
44
53
|
["group", "overlay"].includes(layout) &&
|
|
45
54
|
scale.domain !== "auto" &&
|
|
@@ -72,6 +72,20 @@ function resolveDefaultDomain({
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function resolveCategoryOrderDomain(valuesByConsumer) {
|
|
76
|
+
const orders = valuesByConsumer
|
|
77
|
+
.map(item => item.categoryOrder)
|
|
78
|
+
.filter(order => order !== undefined);
|
|
79
|
+
if (orders.length === 0) return undefined;
|
|
80
|
+
const [first, ...rest] = orders;
|
|
81
|
+
if (rest.some(order =>
|
|
82
|
+
order.length !== first.length ||
|
|
83
|
+
order.some((value, index) => !Object.is(value, first[index])))) {
|
|
84
|
+
throw new Error("Shared scale category order assignments must resolve identically.");
|
|
85
|
+
}
|
|
86
|
+
return first;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
function resolveRange({
|
|
76
90
|
scale,
|
|
77
91
|
channel,
|
|
@@ -225,9 +239,17 @@ export function resolveScaleMaterialization({
|
|
|
225
239
|
valuesByConsumer,
|
|
226
240
|
seriesLayouts
|
|
227
241
|
});
|
|
242
|
+
const categoryOrderDomain = isDiscretizedColor
|
|
243
|
+
? undefined
|
|
244
|
+
: resolveCategoryOrderDomain(valuesByConsumer);
|
|
245
|
+
if (categoryOrderDomain !== undefined && scale.domain !== "auto") {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Scale "${id}" cannot combine an explicit domain with category order.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
228
250
|
const domain = isDiscretizedColor
|
|
229
251
|
? discretizedScale.domain
|
|
230
|
-
: binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
|
|
252
|
+
: categoryOrderDomain ?? binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
|
|
231
253
|
scale,
|
|
232
254
|
allValues,
|
|
233
255
|
isOrdinalAppearance,
|
|
@@ -272,7 +294,7 @@ export function resolveScaleMaterialization({
|
|
|
272
294
|
: isDiscretePosition
|
|
273
295
|
? resolveDiscretePositionScale({
|
|
274
296
|
type: scale.type,
|
|
275
|
-
domain: scale.domain,
|
|
297
|
+
domain: categoryOrderDomain ?? scale.domain,
|
|
276
298
|
values: allValues,
|
|
277
299
|
range: channel === "theta" && consumers.every(
|
|
278
300
|
consumer => consumer.layer.mark?.type === "arc"
|
|
@@ -290,7 +312,7 @@ export function resolveScaleMaterialization({
|
|
|
290
312
|
})
|
|
291
313
|
: isOrdinalPosition
|
|
292
314
|
? resolveOrdinalPositionScale({
|
|
293
|
-
domain: scale.domain,
|
|
315
|
+
domain: categoryOrderDomain ?? scale.domain,
|
|
294
316
|
values: allValues,
|
|
295
317
|
range: scale.range,
|
|
296
318
|
channel,
|
|
@@ -3,4 +3,5 @@ export { resolveArcItems } from "./arc.js";
|
|
|
3
3
|
export { resolveBarItems } from "./bar.js";
|
|
4
4
|
export { resolvePointItems } from "./point.js";
|
|
5
5
|
export { resolveRuleItems } from "./rule.js";
|
|
6
|
+
export { resolveTickItems } from "./tick.js";
|
|
6
7
|
export { resolveRectItems } from "./rect.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
deriveCenteredAreaSeries,
|
|
2
3
|
deriveAreaSeries,
|
|
3
4
|
deriveDensityAreaSeries
|
|
4
5
|
} from "../../../grammar/areaSeries.js";
|
|
@@ -73,7 +74,9 @@ export function resolveLineItems(program, layer, dataset) {
|
|
|
73
74
|
export function resolveAreaItems(program, layer, dataset) {
|
|
74
75
|
const transform = findUpstreamTransform(program, dataset, "density");
|
|
75
76
|
const derived = transform === undefined
|
|
76
|
-
?
|
|
77
|
+
? layer.encoding?.y?.stack === "center"
|
|
78
|
+
? deriveCenteredAreaSeries(dataset.values, layer)
|
|
79
|
+
: deriveAreaSeries(dataset.values, layer)
|
|
77
80
|
: deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
78
81
|
return finalizeItems(
|
|
79
82
|
program,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { selectMarkItemKeys } from "../../../grammar/markSelection.js";
|
|
2
|
+
import {
|
|
3
|
+
channelMapFromRow,
|
|
4
|
+
concreteProperties,
|
|
5
|
+
finalizeItems,
|
|
6
|
+
itemKey,
|
|
7
|
+
ownFields
|
|
8
|
+
} from "./common.js";
|
|
9
|
+
|
|
10
|
+
export function resolveTickItems(program, layer, dataset) {
|
|
11
|
+
const graphic = program.graphicSpec.objects[layer.id];
|
|
12
|
+
const completePosition =
|
|
13
|
+
layer.encoding?.x?.scale !== undefined &&
|
|
14
|
+
layer.encoding?.y?.scale !== undefined;
|
|
15
|
+
if (!Array.isArray(graphic?.items) || !completePosition) {
|
|
16
|
+
throw new Error(`Tick mark "${layer.id}" is incomplete for selection.`);
|
|
17
|
+
}
|
|
18
|
+
let definitions = dataset.values.map((row, index) => ({
|
|
19
|
+
key: itemKey(layer, "tick", index),
|
|
20
|
+
fields: ownFields(row),
|
|
21
|
+
channels: channelMapFromRow(row, layer),
|
|
22
|
+
properties: concreteProperties(graphic.items[index]?.properties),
|
|
23
|
+
members: [row]
|
|
24
|
+
}));
|
|
25
|
+
for (const config of Object.values(
|
|
26
|
+
program.materializationConfigs.highlights ?? {}
|
|
27
|
+
)) {
|
|
28
|
+
if (config.target !== layer.id || config.bringToFront !== true) continue;
|
|
29
|
+
const selection = program.materializationConfigs.selections?.[config.selection];
|
|
30
|
+
if (selection?.target !== layer.id) continue;
|
|
31
|
+
const selected = new Set(selectMarkItemKeys(definitions, selection.selector));
|
|
32
|
+
definitions = [
|
|
33
|
+
...definitions.filter(definition => !selected.has(definition.key)),
|
|
34
|
+
...definitions.filter(definition => selected.has(definition.key))
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
definitions = definitions.map((definition, index) => ({
|
|
38
|
+
...definition,
|
|
39
|
+
properties: concreteProperties(graphic.items[index]?.properties)
|
|
40
|
+
}));
|
|
41
|
+
return finalizeItems(program, layer, "tick", definitions, "line");
|
|
42
|
+
}
|
|
@@ -6,6 +6,7 @@ import { barSelectionPolicy } from "./bar.js";
|
|
|
6
6
|
import { lineSelectionPolicy } from "./line.js";
|
|
7
7
|
import { pointSelectionPolicy } from "./point.js";
|
|
8
8
|
import { ruleSelectionPolicy } from "./rule.js";
|
|
9
|
+
import { tickSelectionPolicy } from "./tick.js";
|
|
9
10
|
import { rectSelectionPolicy } from "./rect.js";
|
|
10
11
|
|
|
11
12
|
const POLICIES = Object.freeze({
|
|
@@ -15,6 +16,7 @@ const POLICIES = Object.freeze({
|
|
|
15
16
|
line: lineSelectionPolicy,
|
|
16
17
|
point: pointSelectionPolicy,
|
|
17
18
|
rule: ruleSelectionPolicy,
|
|
19
|
+
tick: tickSelectionPolicy,
|
|
18
20
|
rect: rectSelectionPolicy
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveTickItems } from "../items/index.js";
|
|
2
|
+
import { normalizeStrokeHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const tickSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item"]),
|
|
6
|
+
resolveItems: resolveTickItems,
|
|
7
|
+
normalizeHighlightStyle: args => normalizeStrokeHighlightStyle(args, "Tick"),
|
|
8
|
+
applyHighlightOp: "applyRuleHighlight",
|
|
9
|
+
rematerializeOp: "rematerializeTickMark"
|
|
10
|
+
});
|