UNPKG

@antv/g2plot

Version:

An interactive and responsive charting library

356 lines (312 loc) 9.15 kB
import { Chart, Event, Element, View } from '@antv/g2'; import { each } from '@antv/util'; import EE from '@antv/event-emitter'; import { bind } from 'size-sensor'; import { Options, StateName, StateCondition, Size, StateObject, Annotation } from '../types'; import { getContainerSize, getAllElementsRecursively, deepAssign, pick } from '../utils'; import { Adaptor } from './adaptor'; /** 单独 pick 出来的用于基类的类型定义 */ export type PickOptions = Pick< Options, | 'width' | 'height' | 'padding' | 'appendPadding' | 'renderer' | 'pixelRatio' | 'autoFit' | 'syncViewPadding' | 'supportCSSTransform' | 'limitInPlot' | 'locale' | 'defaultInteractions' >; const SOURCE_ATTRIBUTE_NAME = 'data-chart-source-type'; /** plot 图表容器的配置 */ export const PLOT_CONTAINER_OPTIONS = [ 'padding', 'appendPadding', 'renderer', 'pixelRatio', 'syncViewPadding', 'supportCSSTransform', 'limitInPlot', ]; /** * 所有 plot 的基类 */ export abstract class Plot<O extends PickOptions> extends EE { /** * 获取默认的 options 配置项 * 每个组件都可以复写 */ static getDefaultOptions(): any { return { renderer: 'canvas', xAxis: { nice: true, label: { autoRotate: false, autoHide: { type: 'equidistance', cfg: { minGap: 6 } }, }, }, yAxis: { nice: true, label: { autoHide: true, autoRotate: false, }, }, animation: true, }; } /** plot 类型名称 */ public abstract readonly type: string; /** plot 的 schema 配置 */ public options: O; /** plot 绘制的 dom */ public readonly container: HTMLElement; /** G2 chart 实例 */ public chart: Chart; /** resizer unbind */ private unbind: () => void; constructor(container: string | HTMLElement, options: O) { super(); this.container = typeof container === 'string' ? document.getElementById(container) : container; this.options = deepAssign({}, this.getDefaultOptions(), options); this.createG2(); this.bindEvents(); } /** * 创建 G2 实例 */ private createG2() { const { width, height, defaultInteractions } = this.options; this.chart = new Chart({ container: this.container, autoFit: false, // G2Plot 使用 size-sensor 进行 autoFit ...this.getChartSize(width, height), localRefresh: false, // 默认关闭,目前 G 还有一些位置问题,难以排查! ...pick(this.options, PLOT_CONTAINER_OPTIONS), defaultInteractions, }); // 给容器增加标识,知道图表的来源区别于 G2 this.container.setAttribute(SOURCE_ATTRIBUTE_NAME, 'G2Plot'); } /** * 计算默认的 chart 大小。逻辑简化:如果存在 width 或 height,则直接使用,否则使用容器大小 * @param width * @param height */ private getChartSize(width: number, height: number): Size { const chartSize = getContainerSize(this.container); return { width: width || chartSize.width || 400, height: height || chartSize.height || 400 }; } /** * 绑定代理所有 G2 的事件 */ private bindEvents() { if (this.chart) { this.chart.on('*', (e: Event) => { if (e?.type) { this.emit(e.type, e); } }); } } /** * 获取默认的 options 配置项 * 每个组件都可以复写 */ protected getDefaultOptions(): any { return Plot.getDefaultOptions(); } /** * 每个组件有自己的 schema adaptor */ protected abstract getSchemaAdaptor(): Adaptor<O>; /** * 绘制 */ public render() { // 暴力处理,先清空再渲染,需要 G2 层自行做好更新渲染 this.chart.clear(); // 因为子 view 会继承父 view 的 options 配置(包括 legend,所以会导致 legend 重复创建) // 所以这里给 chart 实例的 options 配置清空 // 最好的解法是在 G2 view.clear 方法的时候,重置 options 配置。或者提供方法去 resetOptions // #1684 理论上在多 view 图形上,只要存在 custom legend,都存在类似问题(子弹图、双轴图) // @ts-ignore this.chart.options = { data: [], animate: true, }; this.chart.views = []; // 删除已有的 views // 执行 adaptor this.execAdaptor(); // 渲染 this.chart.render(); // 绑定 this.bindSizeSensor(); } /** * 更新: 更新配置且重新渲染 * @param options */ public update(options: Partial<O>) { this.updateOption(options); this.render(); } /** * 更新配置 * @param options */ protected updateOption(options: Partial<O>) { this.options = deepAssign({}, this.options, options); } /** * 设置状态 * @param type 状态类型,支持 'active' | 'inactive' | 'selected' 三种 * @param conditions 条件,支持数组 * @param status 是否激活,默认 true */ public setState(type: StateName, condition: StateCondition, status: boolean = true) { const elements = getAllElementsRecursively(this.chart); each(elements, (ele: Element) => { if (condition(ele.getData())) { ele.setState(type, status); } }); } /** * 获取状态 */ public getStates(): StateObject[] { const elements = getAllElementsRecursively(this.chart); const stateObjects: StateObject[] = []; each(elements, (element: Element) => { const data = element.getData(); const states = element.getStates(); each(states, (state) => { stateObjects.push({ data, state, geometry: element.geometry, element }); }); }); return stateObjects; } /** * 更新数据 * @override * @param options */ public changeData(data: any) { // @ts-ignore this.update({ data }); // TODO: 临时方案,最好使用下面的方式去更新数据 // this.chart.changeData(data); } /** * 修改画布大小 * @param width * @param height */ public changeSize(width: number, height: number) { this.chart.changeSize(width, height); } /** * 增加图表标注。通过 id 标识,如果匹配到,就做更新 */ public addAnnotations(annotations: Annotation[], view?: View): void { view = view ? view : this.chart; const incoming = [...annotations]; const controller = view.getController('annotation'); const current = controller.getComponents().map((co) => co.extra); controller.clear(true); for (let i = 0; i < current.length; i++) { let annotation = current[i]; const findIndex = incoming.findIndex((item) => item.id && item.id === annotation.id); if (findIndex !== -1) { annotation = deepAssign({}, annotation, incoming[findIndex]); incoming.splice(findIndex, 1); } controller.annotation(annotation); } incoming.forEach((annotation) => controller.annotation(annotation)); view.render(true); } /** * 删除图表标注。通过 id 标识,如果匹配到,就做删除 */ public removeAnnotations(annotations: Array<{ id: string } & Partial<Annotation>>): void { const controller = this.chart.getController('annotation'); const current = controller.getComponents().map((co) => co.extra); controller.clear(true); for (let i = 0; i < current.length; i++) { const annotation = current[i]; if (!annotations.find((item) => item.id && item.id === annotation.id)) { controller.annotation(annotation); } } this.chart.render(true); } /** * 销毁 */ public destroy() { // 取消 size-sensor 的绑定 this.unbindSizeSensor(); // G2 的销毁 this.chart.destroy(); // 清空已经绑定的事件 this.off(); this.container.removeAttribute(SOURCE_ATTRIBUTE_NAME); } /** * 执行 adaptor 操作 */ protected execAdaptor() { const adaptor = this.getSchemaAdaptor(); const { padding, appendPadding } = this.options; // 更新 padding this.chart.padding = padding; // 更新 appendPadding this.chart.appendPadding = appendPadding; // 转化成 G2 API adaptor({ chart: this.chart, options: this.options, }); } /** * 当图表容器大小变化的时候,执行的函数 */ protected triggerResize() { this.chart.forceFit(); } /** * 绑定 dom 容器大小变化的事件 */ private bindSizeSensor() { if (this.unbind) { return; } const { autoFit = true } = this.options; if (autoFit) { this.unbind = bind(this.container, () => { // 获取最新的宽高信息 const { width, height } = getContainerSize(this.container); // 主要是防止绑定的时候触发 resize 回调 if (width !== this.chart.width || height !== this.chart.height) { this.triggerResize(); } }); } } /** * 取消绑定 */ private unbindSizeSensor() { if (this.unbind) { this.unbind(); this.unbind = undefined; } } }