UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

430 lines (395 loc) 13.1 kB
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; } }