-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathselection.ts
429 lines (395 loc) · 12.8 KB
/
selection.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
import {
Group,
Rect,
DisplayObject,
IDocument,
BaseStyleProps as BP,
Circle,
Path,
Text,
Ellipse,
Image,
Line,
Polygon,
Polyline,
HTML,
IAnimation as GAnimation,
} from '@antv/g';
import { group } from 'd3-array';
import { error } from './helper';
export type G2Element = DisplayObject & {
// Data for this element.
__data__?: any;
// An Array of data to be splitted to.
__toData__?: any[];
// An Array of elements to be merged from.
__fromElements__?: DisplayObject[];
// Whether to update parent if it in update selection.
__facet__?: boolean;
// Whether is removed in G2, but also exist in G dom.
__removed__?: boolean;
};
export function select<T = any>(node: DisplayObject) {
return new Selection<T>([node], null, node, node.ownerDocument);
}
/**
* A simple implementation of d3-selection for @antv/g.
* It has the core features of d3-selection and extended ability.
* Every methods of selection returns new selection if elements
* are mutated(e.g. append, remove), otherwise return the selection itself(e.g. attr, style).
* @see https://github.com/d3/d3-selection
* @see https://github.com/antvis/g
* @todo Nested selections.
* @todo More useful functor.
*/
export class Selection<T = any> {
static registry: Record<string, new () => G2Element> = {
g: Group,
rect: Rect,
circle: Circle,
path: Path,
text: Text,
ellipse: Ellipse,
image: Image,
line: Line,
polygon: Polygon,
polyline: Polyline,
html: HTML,
};
private _elements: G2Element[];
private _parent: G2Element;
private _data: T[] | [T, G2Element[]][];
private _enter: Selection;
private _exit: Selection;
private _update: Selection;
private _merge: Selection;
private _split: Selection;
private _document: IDocument;
private _transitions: (GAnimation | GAnimation[])[];
private _facetElements: G2Element[];
constructor(
elements: Iterable<G2Element> = null,
data: T[] | [T, G2Element[]][] = null,
parent: G2Element = null,
document: IDocument | null = null,
selections: [Selection, Selection, Selection, Selection, Selection] = [
null,
null,
null,
null,
null,
],
transitions: (GAnimation | GAnimation[])[] = [],
updateElements: G2Element[] = [],
) {
this._elements = Array.from(elements);
this._data = data;
this._parent = parent;
this._document = document;
this._enter = selections[0];
this._update = selections[1];
this._exit = selections[2];
this._merge = selections[3];
this._split = selections[4];
this._transitions = transitions;
this._facetElements = updateElements;
}
selectAll(selector: string | G2Element[]): Selection<T> {
const elements =
typeof selector === 'string'
? this._parent.querySelectorAll<G2Element>(selector)
: selector;
return new Selection<T>(elements, null, this._elements[0], this._document);
}
selectFacetAll(selector: string | G2Element[]): Selection<T> {
const elements =
typeof selector === 'string'
? this._parent.querySelectorAll<G2Element>(selector)
: selector;
return new Selection<T>(
this._elements,
null,
this._parent,
this._document,
undefined,
undefined,
elements,
);
}
/**
* @todo Replace with querySelector which has bug now.
*/
select(selector: string | G2Element): Selection<T> {
const element =
typeof selector === 'string'
? this._parent.querySelectorAll<G2Element>(selector)[0] || null
: selector;
return new Selection<T>([element], null, element, this._document);
}
append(node: string | ((data: T, i: number) => G2Element)): Selection<T> {
const callback =
typeof node === 'function' ? node : () => this.createElement(node);
const elements = [];
if (this._data !== null) {
// For empty selection, append new element to parent.
// Each element is bind with datum.
for (let i = 0; i < this._data.length; i++) {
const d = this._data[i];
const [datum, from] = Array.isArray(d) ? d : [d, null];
const newElement = callback(datum, i);
newElement.__data__ = datum;
if (from !== null) newElement.__fromElements__ = from;
this._parent.appendChild(newElement);
elements.push(newElement);
}
return new Selection(elements, null, this._parent, this._document);
} else {
// For non-empty selection, append new element to
// selected element and return new selection.
for (let i = 0; i < this._elements.length; i++) {
const element = this._elements[i];
const datum = element.__data__;
const newElement = callback(datum, i);
element.appendChild(newElement);
elements.push(newElement);
}
return new Selection(elements, null, elements[0], this._document);
}
}
maybeAppend(
id: string,
node: string | (() => G2Element),
className?: string,
) {
const element = this._elements[0];
const child = element.getElementById(id) as G2Element;
if (child) {
return new Selection([child], null, this._parent, this._document);
}
const newChild =
typeof node === 'string' ? this.createElement(node) : node();
newChild.id = id;
if (className) newChild.className = className;
element.appendChild(newChild);
return new Selection([newChild], null, this._parent, this._document);
}
/**
* Bind data to elements, and produce three selection:
* Enter: Selection with empty elements and data to be bind to elements.
* Update: Selection with elements to be updated.
* Exit: Selection with elements to be removed.
*/
data<T = any>(
data: T[],
id: (d: T, index?: number) => any = (d) => d,
groupId: (d: T, index?: number) => any = () => null,
): Selection<T> {
// An Array of new data.
const enter: T[] = [];
// An Array of elements to be updated.
const update: G2Element[] = [];
// A Set of elements to be removed.
const exit = new Set<G2Element>(this._elements);
// An Array of data to be merged into one element.
const merge: [T, G2Element[]][] = [];
// A Set of elements to be split into multiple datum.
const split = new Set<G2Element>();
// A Map from key to each element.
const keyElement = new Map<string, G2Element>(
this._elements.map((d, i) => [id(d.__data__, i), d]),
);
// A Map from key to exist element. The Update Selection
// can get element from this map, this is for diff among
// facets.
const keyUpdateElement = new Map<string, G2Element>(
this._facetElements.map((d, i) => [id(d.__data__, i), d]),
);
// A Map from groupKey to a group of elements.
const groupKeyElements = group(this._elements, (d) => groupId(d.__data__));
// Diff data with selection(elements with data).
// !!! Note
// The switch is strictly ordered, not not change the order of them.
for (let i = 0; i < data.length; i++) {
const datum = data[i];
const key = id(datum, i);
const groupKey = groupId(datum, i);
// Append element to update selection if incoming data has
// exactly the same key with elements.
if (keyElement.has(key)) {
const element = keyElement.get(key);
element.__data__ = datum;
element.__facet__ = false;
update.push(element);
exit.delete(element);
keyElement.delete(key);
// Append element to update selection if incoming data has
// exactly the same key with updateElements.
} else if (keyUpdateElement.has(key)) {
const element = keyUpdateElement.get(key);
element.__data__ = datum;
// Flag this element should update its parentNode.
element.__facet__ = true;
update.push(element);
keyUpdateElement.delete(key);
// Append datum to merge selection if existed elements has
// its key as groupKey.
} else if (groupKeyElements.has(key)) {
const group = groupKeyElements.get(key);
merge.push([datum, group]);
for (const element of group) exit.delete(element);
groupKeyElements.delete(key);
// Append element to split selection if incoming data has
// groupKey as its key, and bind to datum for it.
} else if (keyElement.has(groupKey)) {
const element = keyElement.get(groupKey);
if (element.__toData__) element.__toData__.push(datum);
else element.__toData__ = [datum];
split.add(element);
exit.delete(element);
} else {
// @todo Data with non-unique key.
enter.push(datum);
}
}
// Create new selection with enter, update and exit.
const S: [
Selection<T>,
Selection<T>,
Selection<T>,
Selection<T>,
Selection<T>,
] = [
new Selection<T>([], enter, this._parent, this._document),
new Selection<T>(update, null, this._parent, this._document),
new Selection<T>(exit, null, this._parent, this._document),
new Selection<T>([], merge, this._parent, this._document),
new Selection<T>(split, null, this._parent, this._document),
];
return new Selection<T>(
this._elements,
null,
this._parent,
this._document,
S,
);
}
merge(other: Selection<T>): Selection<T> {
const elements = [...this._elements, ...other._elements];
const transitions = [...this._transitions, ...other._transitions];
return new Selection<T>(
elements,
null,
this._parent,
this._document,
undefined,
transitions,
);
}
createElement(type: string): G2Element {
if (this._document) {
return this._document.createElement<G2Element, BP>(type, {});
}
const Ctor = Selection.registry[type];
if (Ctor) return new Ctor();
return error(`Unknown node type: ${type}`);
}
/**
* Apply callback for each selection(enter, update, exit)
* and merge them into one selection.
*/
join(
enter: (selection: Selection<T>) => any = (d) => d,
update: (selection: Selection<T>) => any = (d) => d,
exit: (selection: Selection<T>) => any = (d) => d.remove(),
merge: (selection: Selection<T>) => any = (d) => d,
split: (selection: Selection<T>) => any = (d) => d.remove(),
): Selection<T> {
const newEnter = enter(this._enter);
const newUpdate = update(this._update);
const newExit = exit(this._exit);
const newMerge = merge(this._merge);
const newSplit = split(this._split);
return newUpdate
.merge(newEnter)
.merge(newExit)
.merge(newMerge)
.merge(newSplit);
}
remove(): Selection<T> {
// Remove node immediately if there is no transition,
// otherwise wait until transition finished.
for (let i = 0; i < this._elements.length; i++) {
const transition = this._transitions[i];
if (transition) {
const T = Array.isArray(transition) ? transition : [transition];
Promise.all(T.map((d) => d.finished)).then(() => {
const element = this._elements[i];
element.remove();
});
} else {
const element = this._elements[i];
element.remove();
}
}
return new Selection<T>(
[],
null,
this._parent,
this._document,
undefined,
this._transitions,
);
}
each(callback: (datum: T, index: number, element) => any): Selection<T> {
for (let i = 0; i < this._elements.length; i++) {
const element = this._elements[i];
const datum = element.__data__;
callback(datum, i, element);
}
return this;
}
attr(key: string, value: any): Selection<T> {
const callback = typeof value !== 'function' ? () => value : value;
return this.each(function (d, i, element) {
if (value !== undefined) element[key] = callback(d, i, element);
});
}
style(key: string, value: any): Selection<T> {
const callback = typeof value !== 'function' ? () => value : value;
return this.each(function (d, i, element) {
if (value !== undefined) element.style[key] = callback(d, i, element);
});
}
transition(value: any): Selection<T> {
const callback = typeof value !== 'function' ? () => value : value;
const { _transitions: T } = this;
return this.each(function (d, i, element) {
T[i] = callback(d, i, element);
});
}
on(event: string, handler: any) {
this.each(function (d, i, element) {
element.addEventListener(event, handler);
});
return this;
}
call(
callback: (selection: Selection<T>, ...args: any[]) => any,
...args: any[]
): Selection<T> {
callback(this, ...args);
return this;
}
node(): G2Element {
return this._elements[0];
}
nodes(): G2Element[] {
return this._elements;
}
transitions(): (GAnimation | GAnimation[])[] {
return this._transitions;
}
parent(): DisplayObject {
return this._parent;
}
}