UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

305 lines (282 loc) 8.25 kB
import { isNumber } from '@antv/util'; import { G2ViewTree } from '../runtime'; import { getContainerSize } from '../utils/size'; import { deepAssign } from '../utils/helper'; import { Node } from './node'; // Keys can specified by new Chart({...}). // Keys can bubble form mark-level options to view-level options. export const VIEW_KEYS = [ 'width', 'height', 'depth', 'padding', 'paddingLeft', 'paddingRight', 'paddingBottom', 'paddingTop', 'inset', 'insetLeft', 'insetRight', 'insetTop', 'insetBottom', 'margin', 'marginLeft', 'marginRight', 'marginTop', 'marginBottom', 'autoFit', 'theme', 'title', ]; export const REMOVE_FLAG = '__remove__'; export const CALLBACK_NODE = '__callback__'; /** Minimum chart width */ export const MIN_CHART_WIDTH = 1; /** Minimum chart height */ export const MIN_CHART_HEIGHT = 1; export function normalizeContainer( container: string | HTMLElement, ): HTMLElement { if (container === undefined) { const container = document.createElement('div'); container[REMOVE_FLAG] = true; return container; } if (typeof container === 'string') { const node = document.getElementById(container); return node; } return container; } export function removeContainer(container: HTMLElement) { const parent = container.parentNode; if (parent) { parent.removeChild(container); } } export function normalizeRoot(node: Node) { if (node.type !== null) return node; const root = node.children[node.children.length - 1]; for (const key of VIEW_KEYS) root.attr(key, node.attr(key)); return root; } export function valueOf(node: Node): Record<string, any> { return { ...node.value, type: node.type, }; } export function sizeOf(options, container) { const { width, height, autoFit, depth = 0 } = options; let effectiveWidth = 640; let effectiveHeight = 480; if (autoFit) { const { width: containerWidth, height: containerHeight } = getContainerSize(container); effectiveWidth = containerWidth || effectiveWidth; effectiveHeight = containerHeight || effectiveHeight; } effectiveWidth = width || effectiveWidth; effectiveHeight = height || effectiveHeight; return { width: Math.max( isNumber(effectiveWidth) ? effectiveWidth : MIN_CHART_WIDTH, MIN_CHART_WIDTH, ), height: Math.max( isNumber(effectiveHeight) ? effectiveHeight : MIN_CHART_HEIGHT, MIN_CHART_HEIGHT, ), depth, }; } export function optionsOf(node: Node): Record<string, any> { const root = normalizeRoot(node); const discovered: Node[] = [root]; const nodeValue = new Map<Node, Record<string, any>>(); nodeValue.set(root, valueOf(root)); while (discovered.length) { const node = discovered.pop(); const value = nodeValue.get(node); const { children = [] } = node; for (const child of children) { if (child.type === CALLBACK_NODE) { value.children = child.value; } else { const childValue = valueOf(child); const { children = [] } = value; children.push(childValue); discovered.push(child); nodeValue.set(child, childValue); value.children = children; } } } return nodeValue.get(root); } function isMark( type: string | ((...args: any[]) => any), mark: Record<string, new () => Node>, ): boolean { if (typeof type === 'function') return true; return new Set(Object.keys(mark)).has(type); } function isComposition( type: string | ((...args: any[]) => any), composition: Record<string, new () => Node>, ): boolean { return ( typeof type !== 'function' && new Set(Object.keys(composition)).has(type) ); } function normalizeRootOptions( node: Node, options: G2ViewTree, previousType: string, marks: Record<string, new () => Node>, composition: Record<string, new () => Node>, ) { const { type: oldType } = node; const { type = previousType || oldType } = options; if (isComposition(type, composition)) { for (const key of VIEW_KEYS) { if (node.attr(key) !== undefined && options[key] === undefined) { options[key] = node.attr(key); } } return options; } if (isMark(type, marks)) { const view = { type: 'view' }; const mark = { ...options }; for (const key of VIEW_KEYS) { if (mark[key] !== undefined) { view[key] = mark[key]; delete mark[key]; } } return { ...view, children: [mark] }; } return options; } function typeCtor( type: string | ((...args: any[]) => any), mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ): new () => Node { if (typeof type === 'function') return mark.mark; const node = { ...mark, ...composition }; const ctor = node[type]; if (!ctor) throw new Error(`Unknown mark: ${type}.`); return ctor; } // Create node from options. function createNode( options: G2ViewTree, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ): Node { if (typeof options === 'function') { const node = new Node(); node.value = options; node.type = CALLBACK_NODE; return node; } const { type, children, ...value } = options; const Ctor = typeCtor(type, mark, composition); const node = new Ctor(); node.value = value; // @ts-ignore node.type = type; return node; } // Update node by options. function updateNode(node: Node, newOptions: G2ViewTree) { const { type, children, ...value } = newOptions; if (node.type === type || type === undefined) { // Update node. deepAssign(node.value, value); } else if (typeof type === 'string') { // Transform node. node.type = type; node.value = value; } } // Create a nested node tree from newOptions, and append it to the parent. function appendNode( parent: Node, newOptions: G2ViewTree, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ) { if (!parent) return; const discovered = [[parent, newOptions]]; while (discovered.length) { const [parent, nodeOptions] = discovered.shift(); const node = createNode(nodeOptions, mark, composition); if (Array.isArray(parent.children)) parent.push(node); const { children } = nodeOptions; if (Array.isArray(children)) { for (const child of children) { discovered.push([node, child]); } } else if (typeof children === 'function') { discovered.push([node, children]); } } } // Update node tree from options. export function updateRoot( node: Node, options: G2ViewTree, definedType: string, mark: Record<string, new () => Node>, composition: Record<string, new () => Node>, ) { const rootOptions = normalizeRootOptions( node, options, definedType, mark, composition, ); const discovered: [Node, Node, G2ViewTree][] = [[null, node, rootOptions]]; while (discovered.length) { const [parent, oldNode, newNode] = discovered.shift(); // If there is no oldNode, create a node tree directly. if (!oldNode) { appendNode(parent, newNode, mark, composition); } else if (!newNode) { oldNode.remove(); } else { updateNode(oldNode, newNode); const { children: newChildren } = newNode; const { children: oldChildren } = oldNode; if (Array.isArray(newChildren) && Array.isArray(oldChildren)) { // Only update node specified in newChildren, // the extra oldChildren will remain still. const n = Math.max(newChildren.length, oldChildren.length); for (let i = 0; i < n; i++) { const newChild = newChildren[i]; const oldChild = oldChildren[i]; discovered.push([oldNode, oldChild, newChild]); } } else if (typeof newChildren === 'function') { discovered.push([oldNode, null, newChildren]); } } } } export function createEmptyPromise<T>(): [ Promise<T>, (reason?: any) => void, (value: T | PromiseLike<T>) => void, ] { let reject: (reason?: any) => void; let resolve: (value: T | PromiseLike<T>) => void; const cloned = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return [cloned, resolve, reject]; }