import { clone, deepMix, each, filter, find, flatten, get, isBoolean, isFunction, isNil, isObject, isString, isUndefined, mix, remove, set, size, uniqueId, isEqual, isPlainObject, reduce, } from '@antv/util'; import { Attribute, Coordinate, Event as GEvent, GroupComponent, ICanvas, IGroup, IShape, Scale } from '../dependents'; import { AxisOption, ComponentOption, CoordinateCfg, CoordinateOption, Data, Datum, FacetCfgMap, FilterCondition, GeometryOption, LegendOption, LooseObject, Options, Point, Region, ScaleOption, TooltipOption, ViewCfg, ViewPadding, ViewAppendPadding, EventPayload, Padding, } from '../interface'; import { GROUP_Z_INDEX, LAYER, PLOT_EVENTS, VIEW_LIFE_CIRCLE } from '../constant'; import Base from '../base'; import { Facet, getFacet } from '../facet'; import Geometry from '../geometry/base'; import Element from '../geometry/element'; import { createInteraction, Interaction } from '../interaction'; import { getTheme } from '../theme'; import { BBox } from '../util/bbox'; import { getCoordinateClipCfg, isPointInCoordinate } from '../util/coordinate'; import { uniq } from '../util/helper'; import { findDataByPoint } from '../util/tooltip'; import { parsePadding } from '../util/padding'; import { getDefaultCategoryScaleRange } from '../util/scale'; import { createTheme } from '../theme/util'; import Chart from './chart'; import { getComponentController, getComponentControllerNames } from './controller'; import Annotation from './controller/annotation'; import { Controller } from './controller/base'; import CoordinateController from './controller/coordinate'; import Tooltip from './controller/tooltip'; import Slider from './controller/slider'; import Scrollbar from './controller/scrollbar'; import Axis from './controller/axis'; import Gesture from './controller/gesture'; import Legend from './controller/legend'; import Event from './event'; import defaultLayout, { Layout } from './layout'; import { ScalePool } from './util/scale-pool'; import { PaddingCal } from './layout/padding-cal'; import { calculatePadding } from './layout/auto'; import { defaultSyncViewPadding } from './util/sync-view-padding'; /** * G2 视图 View 类 */ export class View extends Base { /** view id,全局唯一。 */ public id: string; /** 父级 view,如果没有父级,则为空。 */ public parent: View; /** 所有的子 view。 */ public views: View[] = []; /** 所有的 geometry 实例。 */ public geometries: Geometry[] = []; /** 所有的组件 controllers。 */ public controllers: Controller[] = []; /** 所有的 Interaction 实例。 */ public interactions: Record = {}; /** view 区域空间。 */ public viewBBox: BBox; /** 坐标系的位置大小,ViewBBox - padding = coordinateBBox。 */ public coordinateBBox: BBox; /** view 的 padding 大小,传入的配置(不是解析之后的值)。 */ public padding: ViewPadding; /** padding的基础上增加的调整值 */ public appendPadding: ViewAppendPadding; /** G.Canvas 实例。 */ public canvas: ICanvas; /** 存储最终计算的 padding 结果 */ public autoPadding: PaddingCal; /** 三层 Group 图形中的背景层。 */ public backgroundGroup: IGroup; /** 三层 Group 图形中的中间层。 */ public middleGroup: IGroup; /** 三层 Group 图形中的前景层。 */ public foregroundGroup: IGroup; /** 是否对超出坐标系范围的 Geometry 进行剪切 */ public limitInPlot: boolean = false; /** * 标记 view 的大小位置范围,均是 0 ~ 1 范围,便于开发者使用,起始点为左上角。 */ protected region: Region; /** 主题配置,存储当前主题配置。 */ protected themeObject: LooseObject; // 配置信息存储 protected options: Options = { data: [], animate: true, // 默认开启动画 }; // 初始化为空 /** 过滤之后的数据 */ protected filteredData: Data; /** 配置开启的组件插件,默认为全局配置的组件。 */ private usedControllers: string[] = getComponentControllerNames(); /** 所有的 scales */ private scalePool: ScalePool = new ScalePool(); /** 布局函数 */ protected layoutFunc: Layout = defaultLayout; /** 生成的坐标系实例,{@link https://github.com/antvis/coord/blob/master/src/coord/base.ts|Coordinate} */ protected coordinateInstance: Coordinate; /** Coordinate 相关的控制器类,负责坐标系实例的创建、更新、变换等 */ protected coordinateController: CoordinateController; /** 分面类实例 */ protected facetInstance: Facet; /** 当前鼠标是否在 plot 内(CoordinateBBox) */ private isPreMouseInPlot: boolean = false; /** 默认标识位,用于判定数据是否更新 */ private isDataChanged: boolean = false; /** 用于判断坐标系范围是否发生变化的标志位 */ private isCoordinateChanged: boolean = false; /** 从当前这个 view 创建的 scale key */ private createdScaleKeys = new Map(); /** 背景色样式的 shape */ private backgroundStyleRectShape; /** 是否同步子 view 的 padding */ private syncViewPadding; constructor(props: ViewCfg) { super({ visible: props.visible }); const { id = uniqueId('view'), parent, canvas, backgroundGroup, middleGroup, foregroundGroup, region = { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } }, padding, appendPadding, theme, options, limitInPlot, syncViewPadding, } = props; this.parent = parent; this.canvas = canvas; this.backgroundGroup = backgroundGroup; this.middleGroup = middleGroup; this.foregroundGroup = foregroundGroup; this.region = region; this.padding = padding; this.appendPadding = appendPadding; // 接受父 view 传入的参数 this.options = { ...this.options, ...options }; this.limitInPlot = limitInPlot; this.id = id; this.syncViewPadding = syncViewPadding; // 初始化 theme this.themeObject = isObject(theme) ? deepMix({}, getTheme('default'), createTheme(theme)) : getTheme(theme); this.init(); } /** * 设置 layout 布局函数 * @param layout 布局函数 * @returns void */ public setLayout(layout: Layout) { this.layoutFunc = layout; } /** * 生命周期:初始化 * @returns voids */ public init() { // 计算画布的 viewBBox this.calculateViewBBox(); // 事件委托机制 this.initEvents(); // 初始化组件 controller this.initComponentController(); this.initOptions(); } /** * 生命周期:渲染流程,渲染过程需要处理数据更新的情况。 * render 函数仅仅会处理 view 和子 view。 * @param isUpdate 是否触发更新流程。 * @param params render 事件参数 */ public render(isUpdate: boolean = false, payload?: EventPayload) { this.emit(VIEW_LIFE_CIRCLE.BEFORE_RENDER, Event.fromData(this, VIEW_LIFE_CIRCLE.BEFORE_RENDER, payload)); // 递归渲染 this.paint(isUpdate); this.emit(VIEW_LIFE_CIRCLE.AFTER_RENDER, Event.fromData(this, VIEW_LIFE_CIRCLE.AFTER_RENDER, payload)); if (this.visible === false) { // 用户在初始化的时候声明 visible: false this.changeVisible(false); } } /** * 生命周期:清空图表上所有的绘制内容,但是不销毁图表,chart 仍可使用。 * @returns void */ public clear() { this.emit(VIEW_LIFE_CIRCLE.BEFORE_CLEAR); // 1. 清空缓存和计算数据 this.filteredData = []; this.coordinateInstance = undefined; this.isDataChanged = false; // 复位 this.isCoordinateChanged = false; // 复位 // 2. 清空 geometries const geometries = this.geometries; for (let i = 0; i < geometries.length; i++) { geometries[i].clear(); // view 中使用 geometry 的时候,还需要清空它的容器,不然下一次 chart.geometry() 的时候,又创建了一个,导致泄露, #2799。 geometries[i].container.remove(true); geometries[i].labelsContainer.remove(true); } this.geometries = []; // 3. 清空 controllers const controllers = this.controllers; for (let i = 0; i < controllers.length; i++) { if (controllers[i].name === 'annotation') { // 需要清空配置项 (controllers[i] as Annotation).clear(true); } else { controllers[i].clear(); } } // 4. 删除 scale 缓存 this.createdScaleKeys.forEach((v: boolean, k: string) => { this.getRootView().scalePool.deleteScale(k); }); this.createdScaleKeys.clear(); // 递归处理子 view const views = this.views; for (let i = 0; i < views.length; i++) { views[i].clear(); } this.emit(VIEW_LIFE_CIRCLE.AFTER_CLEAR); } /** * 生命周期:销毁,完全无法使用。 * @returns void */ public destroy() { // 销毁前事件,销毁之后已经没有意义了,所以不抛出事件 this.emit(VIEW_LIFE_CIRCLE.BEFORE_DESTROY); const interactions = this.interactions; // 销毁 interactions each(interactions, (interaction) => { if (interaction) { // 有可能已经销毁,设置了 undefined interaction.destroy(); } }); this.clear(); // 销毁 controller 中的组件 const controllers = this.controllers; for (let i = 0, len = controllers.length; i < len; i++) { const controller = controllers[i]; controller.destroy(); } this.backgroundGroup.remove(true); this.middleGroup.remove(true); this.foregroundGroup.remove(true); super.destroy(); } /* end 生命周期函数 */ /** * 显示或者隐藏整个 view。 * @param visible 是否可见 * @returns View */ public changeVisible(visible: boolean): View { super.changeVisible(visible); const geometries = this.geometries; for (let i = 0, len = geometries.length; i < len; i++) { const geometry = geometries[i]; geometry.changeVisible(visible); } const controllers = this.controllers; for (let i = 0, len = controllers.length; i < len; i++) { const controller = controllers[i]; controller.changeVisible(visible); } this.foregroundGroup.set('visible', visible); this.middleGroup.set('visible', visible); this.backgroundGroup.set('visible', visible); // group.set('visible', visible) 不会触发自动刷新 this.getCanvas().draw(); return this; } /** * 装载数据源。 * * ```ts * view.data([{ city: '杭州', sale: 100 }, { city: '上海', sale: 110 } ]); * ``` * * @param data 数据源,json 数组。 * @returns View */ public data(data: Data): View { set(this.options, 'data', data); this.isDataChanged = true; return this; } /** * @deprecated * This method will be removed at G2 V4.1. Replaced by {@link #data(data)} */ public source(data: Data): View { console.warn('This method will be removed at G2 V4.1. Please use chart.data() instead.'); return this.data(data); } /** * 设置数据筛选规则。 * * ```ts * view.filter('city', (value: any, datum: Datum) => value !== '杭州'); * * // 删除 'city' 字段对应的筛选规则。 * view.filter('city', null); * ``` * * @param field 数据字段 * @param condition 筛选规则 * @returns View */ public filter(field: string, condition: FilterCondition | null): View { if (isFunction(condition)) { set(this.options, ['filters', field], condition); return this; } // condition 为空,则表示删除过滤条件 if (!condition && get(this.options, ['filters', field])) { delete this.options.filters[field]; } return this; } /** * 开启或者关闭坐标轴。 * * ```ts * view.axis(false); // 不展示坐标轴 * ``` * @param field 坐标轴开关 */ public axis(field: boolean): View; /** * 对特定的某条坐标轴进行配置。 * * @example * ```ts * view.axis('city', false); // 不展示 'city' 字段对应的坐标轴 * * // 将 'city' 字段对应的坐标轴的标题隐藏 * view.axis('city', { * title: null, * }); * ``` * * @param field 要配置的坐标轴对应的字段名称 * @param axisOption 坐标轴具体配置,更详细的配置项可以参考:https://github.com/antvis/component#axis */ public axis(field: string, axisOption: AxisOption): View; public axis(field: string | boolean, axisOption?: AxisOption): View { if (isBoolean(field)) { set(this.options, ['axes'], field); } else { set(this.options, ['axes', field], axisOption); } return this; } /** * 对图例进行整体配置。 * * ```ts * view.legend(false); // 关闭图例 * * view.legend({ * position: 'right', * }); // 图例进行整体配置 * ``` * @param field * @returns View */ public legend(field: LegendOption): View; /** * 对特定的图例进行配置。 * * @example * ```ts * view.legend('city', false); // 关闭某个图例,通过数据字段名进行关联 * * // 对特定的图例进行配置 * view.legend('city', { * position: 'right', * }); * ``` * * @param field 图例对应的数据字段名称 * @param legendOption 图例配置,更详细的配置项可以参考:https://github.com/antvis/component#axis * @returns View */ public legend(field: string, legendOption: LegendOption): View; public legend(field: string | LegendOption, legendOption?: LegendOption): View { if (isBoolean(field)) { set(this.options, ['legends'], field); } else if (isString(field)) { set(this.options, ['legends', field], legendOption); if (isPlainObject(legendOption) && legendOption?.selected) { set(this.options, ['filters', field], (name: string) => { return legendOption?.selected[name] ?? true; }); } } else { // 设置全局的 legend 配置 set(this.options, ['legends'], field); } return this; } /** * 批量设置 scale 配置。 * * ```ts * view.scale({ * sale: { * min: 0, * max: 100, * } * }); * ``` * Scale 的详细配置项可以参考:https://github.com/antvis/scale#api * @returns View */ public scale(field: Record): View; /** * 为特性的数据字段进行 scale 配置。 * * ```ts * view.scale('sale', { * min: 0, * max: 100, * }); * ``` * * @returns View */ public scale(field: string, scaleOption: ScaleOption): View; public scale(field: string | Record, scaleOption?: ScaleOption): View { if (isString(field)) { set(this.options, ['scales', field], scaleOption); } else if (isObject(field)) { each(field, (v: ScaleOption, k: string) => { set(this.options, ['scales', k], v); }); } return this; } /** * tooltip 提示信息配置。 * * ```ts * view.tooltip(false); // 关闭 tooltip * * view.tooltip({ * shared: true * }); * ``` * * @param cfg Tooltip 配置,更详细的配置项参考:https://github.com/antvis/component#tooltip * @returns View */ public tooltip(cfg: boolean | TooltipOption): View { set(this.options, 'tooltip', cfg); return this; } /** * 辅助标记配置。 * * ```ts * view.annotation().line({ * start: ['min', 85], * end: ['max', 85], * style: { * stroke: '#595959', * lineWidth: 1, * lineDash: [3, 3], * }, * }); * ``` * 更详细的配置项:https://github.com/antvis/component#annotation * @returns [[Annotation]] */ public annotation(): Annotation { return this.getController('annotation'); } /** * @deprecated * This method will be removed at G2 V4.1. Replaced by {@link #guide()} */ public guide(): Annotation { console.warn('This method will be removed at G2 V4.1. Please use chart.annotation() instead.'); return this.annotation(); } /** * 坐标系配置。 * * @example * ```ts * view.coordinate({ * type: 'polar', * cfg: { * radius: 0.85, * }, * actions: [ * [ 'transpose' ], * ], * }); * ``` * * @param option * @returns */ public coordinate(option?: CoordinateOption): CoordinateController; /** * 声明坐标系类型,并进行配置。 * * ```ts * // 直角坐标系,并进行转置变换 * view.coordinate('rect').transpose(); * * // 默认创建直角坐标系 * view.coordinate(); * ``` * * @param type 坐标系类型 * @param [coordinateCfg] 坐标系配置 * @returns */ public coordinate(type: string, coordinateCfg?: CoordinateCfg): CoordinateController; public coordinate(type: string | CoordinateOption, coordinateCfg?: CoordinateCfg): CoordinateController { // 提供语法糖,使用更简单 if (isString(type)) { set(this.options, 'coordinate', { type, cfg: coordinateCfg } as CoordinateOption); } else { set(this.options, 'coordinate', type); } // 更新 coordinate 配置 this.coordinateController.update(this.options.coordinate); return this.coordinateController; } /** * @deprecated * This method will be removed at G2 V4.1. Replaced by {@link #coordinate()} */ public coord(type: string | CoordinateOption, coordinateCfg?: CoordinateCfg): CoordinateController { console.warn('This method will be removed at G2 V4.1. Please use chart.coordinate() instead.'); // @ts-ignore return this.coordinate(type, coordinateCfg); } /** * view 分面绘制。 * * ```ts * view.facet('rect', { * rowField: 'province', * columnField: 'category', * eachView: (innerView: View, facet?: FacetData) => { * innerView.line().position('city*sale'); * }, * }); * ``` * * @param type 分面类型 * @param cfg 分面配置, [[FacetCfgMap]] * @returns View */ public facet(type: T, cfg: FacetCfgMap[T]): View { // 先销毁掉之前的分面 if (this.facetInstance) { this.facetInstance.destroy(); } // 创建新的分面 const Ctor = getFacet(type); if (!Ctor) { throw new Error(`facet '${type}' is not exist!`); } this.facetInstance = new Ctor(this, { ...cfg, type }); return this; } /* * 开启或者关闭动画。 * * ```ts * view.animate(false); * ``` * * @param status 动画状态,true 表示开始,false 表示关闭 * @returns View */ public animate(status: boolean): View { set(this.options, 'animate', status); return this; } /** * 更新配置项,用于配置项式声明。 * @param options 配置项 */ public updateOptions(options: Options) { this.clear(); // 清空 mix(this.options, options); // 需要把已存在的 view 销毁,否则会重复创建 // 目前针对配置项还没有特别好的 view 更新机制,为了不影响主流流程,所以在这里直接销毁 this.views.forEach((view) => view.destroy()); this.views = []; this.initOptions(); // 初始化坐标系大小,保证 padding 计算正确 this.coordinateBBox = this.viewBBox; return this; } /** * 往 `view.options` 属性中存储配置项。 * @param name 属性名称 * @param opt 属性值 * @returns view */ public option(name: string, opt: any): View { // 对于内置的 option,避免覆盖。 // name 在原型上,说明可能是内置 API,存在 option 被覆盖的风险,不处理 if (View.prototype[name]) { throw new Error(`Can't use built in variable name "${name}", please change another one.`); } // 存入到 option 中 set(this.options, name, opt); return this; } /** * 设置主题。 * * ```ts * view.theme('dark'); // 'dark' 需要事先通过 `registerTheme()` 接口注册完成 * * view.theme({ defaultColor: 'red' }); * ``` * * @param theme 主题名或者主题配置 * @returns View */ public theme(theme: string | LooseObject): View { this.themeObject = isObject(theme) ? deepMix({}, this.themeObject, createTheme(theme)) : getTheme(theme); return this; } /* end 一系列传入配置的 API */ /** * Call the interaction based on the interaction name * * ```ts * view.interaction('my-interaction', { extra: 'hello world' }); * ``` * 详细文档可以参考:https://g2.antv.vision/zh/docs/api/general/interaction * @param name interaction name * @param cfg interaction config * @returns */ public interaction(name: string, cfg?: LooseObject): View { const existInteraction = this.interactions[name]; // 存在则先销毁已有的 if (existInteraction) { existInteraction.destroy(); } // 新建交互实例 const interaction = createInteraction(name, this, cfg); if (interaction) { interaction.init(); this.interactions[name] = interaction; } return this; } /** * 移除当前 View 的 interaction * ```ts * view.removeInteraction('my-interaction'); * ``` * @param name interaction name */ public removeInteraction(name: string) { const existInteraction = this.interactions[name]; // 存在则先销毁已有的 if (existInteraction) { existInteraction.destroy(); this.interactions[name] = undefined; } } /** * 修改数据,数据更新逻辑,数据更新仅仅影响当前这一层的 view * * ```ts * view.changeData([{ city: '北京', sale: '200' }]); * ``` * * @param data * @returns void */ public changeData(data: Data) { this.isDataChanged = true; this.emit(VIEW_LIFE_CIRCLE.BEFORE_CHANGE_DATA, Event.fromData(this, VIEW_LIFE_CIRCLE.BEFORE_CHANGE_DATA, null)); // 1. 保存数据 this.data(data); // 2. 渲染 this.paint(true); // 3. 遍历子 view 进行 change data const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; // FIXME 子 view 有自己的数据的情况,该如何处理? view.changeData(data); } this.emit(VIEW_LIFE_CIRCLE.AFTER_CHANGE_DATA, Event.fromData(this, VIEW_LIFE_CIRCLE.AFTER_CHANGE_DATA, null)); } /* View 管理相关的 API */ /** * 创建子 view * * ```ts * const innerView = view.createView({ * start: { x: 0, y: 0 }, * end: { x: 0.5, y: 0.5 }, * padding: 8, * }); * ``` * * @param cfg * @returns View */ public createView(cfg?: Partial): View { // 将会在 4.1 版本中移除递归嵌套 view,仅仅只允许 chart - view 两层。 // 这个 API 理论上用户量不多,所以暂时不发大版本,所以先暂时打一个 warning。 if (this.parent && this.parent.parent) { // 存在 3 层 结构了 console.warn('The view nesting recursive feature will be removed at G2 V4.1. Please avoid to use it.'); } // 子 view 共享 options 配置数据 const sharedOptions = { data: this.options.data, scales: clone(this.options.scales), axes: clone(this.options.axes), coordinate: clone(this.coordinateController.getOption()), tooltip: clone(this.options.tooltip), legends: clone(this.options.legends), animate: this.options.animate, visible: this.visible, }; const v = new View({ parent: this, canvas: this.canvas, // 子 view 共用三层 group backgroundGroup: this.backgroundGroup.addGroup({ zIndex: GROUP_Z_INDEX.BG }), middleGroup: this.middleGroup.addGroup({ zIndex: GROUP_Z_INDEX.MID }), foregroundGroup: this.foregroundGroup.addGroup({ zIndex: GROUP_Z_INDEX.FORE }), theme: this.themeObject, padding: this.padding, ...cfg, options: { ...sharedOptions, ...get(cfg, 'options', {}), }, }); this.views.push(v); return v; } /** * @deprecated * This method will be removed at G2 V4.1. Replaced by {@link #createView()} */ public view(cfg?: Partial) { console.warn('This method will be removed at G2 V4.1. Please use chart.createView() instead.'); return this.createView(cfg); } /** * 删除一个子 view * @param view * @return removedView */ public removeView(view: View): View { const removedView = remove(this.views, (v: View) => v === view)[0]; if (removedView) { removedView.destroy(); } return removedView; } /* end View 管理相关的 API */ // 一些 get 方法 /** * 获取当前坐标系实例。 * @returns [[Coordinate]] */ public getCoordinate() { return this.coordinateInstance; } /** * 获取当前 view 的主题配置。 * @returns themeObject */ public getTheme(): LooseObject { return this.themeObject; } /** * 获得 x 轴字段的 scale 实例。 * @returns view 中 Geometry 对于的 x scale */ public getXScale(): Scale { // 拿第一个 Geometry 的 X scale // 隐藏逻辑:一个 view 中的 Geometry 必须 x 字段一致 const g = this.geometries[0]; return g ? g.getXScale() : null; } /** * 获取 y 轴字段的 scales 实例。 * @returns view 中 Geometry 对于的 y scale 数组 */ public getYScales(): Scale[] { // 拿到所有的 Geometry 的 Y scale,然后去重 const tmpMap = {}; const yScales = []; this.geometries.forEach((g: Geometry) => { const yScale = g.getYScale(); const field = yScale.field; if (!tmpMap[field]) { tmpMap[field] = true; yScales.push(yScale); } }); return yScales; } /** * 获取 x 轴或者 y 轴对应的所有 scale 实例。 * @param dimType x | y * @returns x 轴或者 y 轴对应的所有 scale 实例。 */ public getScalesByDim(dimType: 'x' | 'y'): Record { const geometries = this.geometries; const scales = {}; for (let i = 0, len = geometries.length; i < len; i++) { const geometry = geometries[i]; const scale = dimType === 'x' ? geometry.getXScale() : geometry.getYScale(); if (scale && !scales[scale.field]) { scales[scale.field] = scale; } } return scales; } /** * 根据字段名去获取 scale 实例。 * @param field 数据字段名称 * @param key id */ public getScale(field: string, key?: string): Scale { const defaultKey = key ? key : this.getScaleKey(field); // 调用根节点 view 的方法获取 return this.getRootView().scalePool.getScale(defaultKey); } /** * @deprecated * This method will be removed at G2 V4.1. Please use `getScale`. */ public getScaleByField(field: string, key?: string): Scale { return this.getScale(field, key); } /** * 返回所有配置信息。 * @returns 所有的 view API 配置。 */ public getOptions(): Options { return this.options; } /** * 获取 view 的数据(过滤后的数据)。 * @returns 处理过滤器之后的数据。 */ public getData() { return this.filteredData; } /** * 获取原始数据 * @returns 传入 G2 的原始数据 */ public getOriginalData() { return this.options.data; } /** * 获取布局后的边距 padding * @returns */ public getPadding(): Padding { return this.autoPadding.getPadding(); } /** * 获取当前 view 有的 geometries * @returns */ public getGeometries() { return this.geometries; } /** * 获取 view 中的所有 geome */ public getElements(): Element[] { return reduce( this.geometries, (elements: Element[], geometry: Geometry) => { return elements.concat(geometry.getElements()); }, [] ); } /** * 根据一定的规则查找 Geometry 的 Elements。 * * ```typescript * getElementsBy((element) => { * const data = element.getData(); * * return data.a === 'a'; * }); * ``` * * @param condition 定义查找规则的回调函数。 * @returns */ public getElementsBy(condition: (element: Element) => boolean): Element[] { return this.getElements().filter((el) => condition(el)); } /** * 获得绘制的层级 group。 * @param layer 层级名称。 * @returns 对应层级的 Group。 */ public getLayer(layer: LAYER): IGroup { return layer === LAYER.BG ? this.backgroundGroup : layer === LAYER.MID ? this.middleGroup : layer === LAYER.FORE ? this.foregroundGroup : this.foregroundGroup; } /** * 对外暴露方法,判断一个点是否在绘图区域(即坐标系范围)内部。 * @param point 坐标点 */ public isPointInPlot(point: Point): boolean { return isPointInCoordinate(this.getCoordinate(), point); } /** * 获得所有的 legend 对应的 attribute 实例。 * @returns 维度字段的 Attribute 数组 */ public getLegendAttributes(): Attribute[] { return flatten(this.geometries.map((g: Geometry) => g.getGroupAttributes())) as unknown as Attribute[]; } /** * 获取所有的分组字段的 scale 实例。 * @returns 获得分组字段的 scale 实例数组。 */ public getGroupScales(): Scale[] { // 拿到所有的 Geometry 的 分组字段 scale,然后打平去重 const scales = this.geometries.map((g: Geometry) => g.getGroupScales()); return uniq(flatten(scales)); } /** * 获取 G.Canvas 实例。 * @returns G.Canvas 画布实例。 */ public getCanvas(): ICanvas { return (this.getRootView() as unknown as Chart).canvas; } /** * 获得根节点 view。 */ public getRootView(): View { let v = this as View; while (true) { if (v.parent) { v = v.parent; continue; } break; } return v; } /** * 获取该数据在可视化后,对应的画布坐标点。 * @param data 原始数据记录 * @returns 对应的画布坐标点 */ public getXY(data: Datum): Point { const coordinate = this.getCoordinate(); const xScales = this.getScalesByDim('x'); const yScales = this.getScalesByDim('y'); let x; let y; each(data, (value, key) => { if (xScales[key]) { x = xScales[key].scale(value); } if (yScales[key]) { y = yScales[key].scale(value); } }); if (!isNil(x) && !isNil(y)) { return coordinate.convert({ x, y }); } } public getController(name: 'tooltip'): Tooltip; public getController(name: 'axis'): Axis; public getController(name: 'legend'): Legend; public getController(name: 'scrollbar'): Scrollbar; public getController(name: 'slider'): Slider; public getController(name: 'annotation'): Annotation; public getController(name: 'gestucre'): Gesture; public getController(name: string): Controller; /** * 获取 name 对应的 controller 实例 * @param name */ public getController(name: string): Controller { return find(this.controllers, (c: Controller) => c.name === name); } /** * 显示 point 坐标点对应的 tooltip。 * @param point 画布坐标点 * @returns View */ public showTooltip(point: Point): View { const tooltip = this.getController('tooltip'); if (tooltip) { tooltip.showTooltip(point); } return this; } /** * 隐藏 tooltip。 * @returns View */ public hideTooltip(): View { const tooltip = this.getController('tooltip'); if (tooltip) { tooltip.hideTooltip(); } return this; } /** * 将 tooltip 锁定到当前位置不能移动。 * @returns View */ public lockTooltip(): View { const tooltip = this.getController('tooltip'); if (tooltip) { tooltip.lockTooltip(); } return this; } /** * 将 tooltip 锁定解除。 * @returns View */ public unlockTooltip(): View { const tooltip = this.getController('tooltip'); if (tooltip) { tooltip.unlockTooltip(); } return this; } /** * 是否锁定 tooltip。 * @returns 是否锁定 */ public isTooltipLocked() { const tooltip = this.getController('tooltip'); return tooltip && tooltip.isTooltipLocked(); } /** * 获取当前 point 对应的 tooltip 数据项。 * @param point 坐标点 * @returns tooltip 数据项 */ public getTooltipItems(point: Point) { const tooltip = this.getController('tooltip'); return tooltip ? tooltip.getTooltipItems(point) : []; } /** * 获取逼近的点的数据集合 * @param point 当前坐标点 * @returns 数据 */ public getSnapRecords(point: Point) { const geometries = this.geometries; let rst = []; for (let i = 0, len = geometries.length; i < len; i++) { const geom = geometries[i]; const dataArray = geom.dataArray; geom.sort(dataArray); // 先进行排序,便于 tooltip 查找 let record; for (let j = 0, dataLen = dataArray.length; j < dataLen; j++) { const data = dataArray[j]; record = findDataByPoint(point, data, geom); if (record) { rst.push(record); } } } // 同样递归处理子 views const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; const snapRecords = view.getSnapRecords(point); rst = rst.concat(snapRecords); } return rst; } /** * 获取所有的 pure component 组件,用于布局。 */ public getComponents(): ComponentOption[] { let components = []; const controllers = this.controllers; for (let i = 0, len = controllers.length; i < len; i++) { const controller = controllers[i]; components = components.concat(controller.getComponents()); } return components; } /** * 将 data 数据进行过滤。 * @param data * @returns 过滤之后的数据 */ public filterData(data: Data): Data { const { filters } = this.options; // 不存在 filters,则不需要进行数据过滤 if (size(filters) === 0) { return data; } // 存在过滤器,则逐个执行过滤,过滤器之间是 与 的关系 return filter(data, (datum: Datum, idx: number) => { // 所有的 filter 字段 const fields = Object.keys(filters); // 所有的条件都通过,才算通过 return fields.every((field: string) => { const condition = filters[field]; // condition 返回 true,则保留 return condition(datum[field], datum, idx); }); }); } /** * 对某一个字段进行过滤 * @param field * @param data */ public filterFieldData(field: string, data: Data): Data { const { filters } = this.options; const condition = get(filters, field); if (isUndefined(condition)) { return data; } return data.filter((datum: Datum, idx: number) => condition(datum[field], datum, idx)); } /** * 调整 coordinate 的坐标范围。 */ public adjustCoordinate() { const { start: curStart, end: curEnd } = this.getCoordinate(); const start = this.coordinateBBox.bl; const end = this.coordinateBBox.tr; // 在 defaultLayoutFn 中只会在 coordinateBBox 发生变化的时候会调用 adjustCoordinate(),所以不用担心被置位 if (isEqual(curStart, start) && isEqual(curEnd, end)) { this.isCoordinateChanged = false; // 如果大小没有变化则不更新 return; } this.isCoordinateChanged = true; this.coordinateInstance = this.coordinateController.adjust(start, end); } protected paint(isUpdate: boolean) { this.renderDataRecursive(isUpdate); // 处理 sync scale 的逻辑 this.syncScale(); this.emit(VIEW_LIFE_CIRCLE.BEFORE_PAINT); // 初始化图形、组件位置,计算 padding this.renderPaddingRecursive(isUpdate); // 布局图形、组件 this.renderLayoutRecursive(isUpdate); // 背景色 shape this.renderBackgroundStyleShape(); // 最终的绘制 render this.renderPaintRecursive(isUpdate); this.emit(VIEW_LIFE_CIRCLE.AFTER_PAINT); this.isDataChanged = false; // 渲染完毕复位 } /** * 渲染背景样式的 shape。 * 放到 view 中创建的原因是让使用 view 绘制图形的时候,也能够处理背景色 */ private renderBackgroundStyleShape() { // 只有根节点才处理 if (this.parent) { return; } const background = get(this.themeObject, 'background'); // 配置了背景色 if (background) { // 1. 不存在则创建 if (!this.backgroundStyleRectShape) { this.backgroundStyleRectShape = this.backgroundGroup.addShape('rect', { attrs: {}, zIndex: -1, // 背景色 shape 不设置事件捕获 capture: false, }); this.backgroundStyleRectShape.toBack(); } // 2. 有了 shape 之后设置背景,位置(更新的时候) const { x, y, width, height } = this.viewBBox; this.backgroundStyleRectShape.attr({ fill: background, x, y, width, height, }); } else { // 没有配置背景色 if (this.backgroundStyleRectShape) { this.backgroundStyleRectShape.remove(true); this.backgroundStyleRectShape = undefined; } } } /** * 递归计算每个 view 的 padding 值,coordinateBBox 和 coordinateInstance * @param isUpdate */ protected renderPaddingRecursive(isUpdate: boolean) { // 1. 子 view 大小相对 coordinateBBox,changeSize 的时候需要重新计算 this.calculateViewBBox(); // 2. 更新 coordinate this.adjustCoordinate(); // 3. 初始化组件 component this.initComponents(isUpdate); // 4. 布局计算每隔 view 的 padding 值 // 4.1. 自动加 auto padding -> absolute padding,并且增加 appendPadding this.autoPadding = calculatePadding(this).shrink(parsePadding(this.appendPadding)); // 4.2. 计算出新的 coordinateBBox,更新 Coordinate // 这里必须保留,原因是后面子 view 的 viewBBox 或根据 parent 的 coordinateBBox this.coordinateBBox = this.viewBBox.shrink(this.autoPadding.getPadding()); this.adjustCoordinate(); // 刷新 tooltip (tooltip crosshairs 依赖 coordinate 位置) const tooltipController = this.controllers.find((c) => c.name === 'tooltip'); tooltipController.update(); // 同样递归处理子 views const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; view.renderPaddingRecursive(isUpdate); } } /** * 递归处理 view 的布局,最终是计算各个 view 的 coordinateBBox 和 coordinateInstance * @param isUpdate */ protected renderLayoutRecursive(isUpdate: boolean) { // 1. 同步子 view padding // 根据配置获取 padding const syncViewPaddingFn = this.syncViewPadding === true ? defaultSyncViewPadding : isFunction(this.syncViewPadding) ? this.syncViewPadding : undefined; if (syncViewPaddingFn) { syncViewPaddingFn(this, this.views, PaddingCal); // 同步 padding 之后,更新 coordinate this.views.forEach((v: View) => { v.coordinateBBox = v.viewBBox.shrink(v.autoPadding.getPadding()); v.adjustCoordinate(); }); } // 3. 将 view 中的组件按照 view padding 移动到对应的位置 this.doLayout(); // 同样递归处理子 views const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; view.renderLayoutRecursive(isUpdate); } } /** * 最终递归绘制组件和图形 * @param isUpdate */ protected renderPaintRecursive(isUpdate: boolean) { const middleGroup = this.middleGroup; if (this.limitInPlot) { const { type, attrs } = getCoordinateClipCfg(this.coordinateInstance); middleGroup.setClip({ type, attrs, }); } else { // 清除已有的 clip middleGroup.setClip(undefined); } // 1. 渲染几何标记 this.paintGeometries(isUpdate); // 2. 绘制组件 this.renderComponents(isUpdate); // 同样递归处理子 views const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; view.renderPaintRecursive(isUpdate); } } // end Get 方法 /** * 创建 scale,递归到顶层 view 去创建和缓存 scale * @param field * @param data * @param scaleDef * @param key */ protected createScale(field: string, data: Data, scaleDef: ScaleOption, key: string): Scale { // 1. 合并 field 对应的 scaleDef,合并原则是底层覆盖顶层(就近原则) const currentScaleDef = get(this.options.scales, [field]); const mergedScaleDef = { ...currentScaleDef, ...scaleDef }; // 2. 是否存在父 view,在则递归,否则创建 if (this.parent) { return this.parent.createScale(field, data, mergedScaleDef, key); } // 3. 在根节点 view 通过 scalePool 创建 return this.scalePool.createScale(field, data, mergedScaleDef, key); } /** * 递归渲染中的数据处理 * @param isUpdate */ private renderDataRecursive(isUpdate: boolean) { // 1. 处理数据 this.doFilterData(); // 2. 创建实例 this.createCoordinate(); // 3. 初始化 Geometry this.initGeometries(isUpdate); // 4. 处理分面逻辑,最终都是生成子 view 和 geometry this.renderFacet(isUpdate); // 同样递归处理子 views const views = this.views; for (let i = 0, len = views.length; i < len; i++) { const view = views[i]; view.renderDataRecursive(isUpdate); } } /** * 计算 region,计算实际的像素范围坐标 * @private */ private calculateViewBBox() { let x; let y; let width; let height; if (this.parent) { const bbox = this.parent.coordinateBBox; // 存在 parent, 那么就是通过父容器大小计算 x = bbox.x; y = bbox.y; width = bbox.width; height = bbox.height; } else { // 顶层容器,从 canvas 中取值 宽高 x = 0; y = 0; width = this.canvas.get('width'); height = this.canvas.get('height'); } const { start, end } = this.region; // 根据 region 计算当前 view 的 bbox 大小。 const viewBBox = new BBox( x + width * start.x, y + height * start.y, width * (end.x - start.x), height * (end.y - start.y) ); if (!this.viewBBox || !this.viewBBox.isEqual(viewBBox)) { // viewBBox 发生变化的时候进行更新 this.viewBBox = new BBox( x + width * start.x, y + height * start.y, width * (end.x - start.x), height * (end.y - start.y) ); } // 初始的 coordinate bbox 大小 this.coordinateBBox = this.viewBBox; } /** * 初始化事件机制:G 4.0 底层内置支持 name:event 的机制,那么只要所有组件都有自己的 name 即可。 * * G2 的事件只是获取事件委托,然后在 view 嵌套结构中,形成事件冒泡机制。 * 当前 view 只委托自己 view 中的 Component 和 Geometry 事件,并向上冒泡 * @private */ private initEvents() { // 三层 group 中的 shape 事件都会通过 G 冒泡上来的 this.foregroundGroup.on('*', this.onDelegateEvents); this.middleGroup.on('*', this.onDelegateEvents); this.backgroundGroup.on('*', this.onDelegateEvents); this.canvas.on('*', this.onCanvasEvent); } private onCanvasEvent = (evt: GEvent): void => { const name = evt.name; if (!name.includes(':')) { // 非委托事件 const e = this.createViewEvent(evt); // 处理 plot 事件 this.doPlotEvent(e); this.emit(name, e); } }; /** * 初始化插件 */ private initComponentController() { const usedControllers = this.usedControllers; for (let i = 0, len = usedControllers.length; i < len; i++) { const controllerName = usedControllers[i]; const Ctor = getComponentController(controllerName); if (Ctor) { this.controllers.push(new Ctor(this)); } } } private createViewEvent(evt: GEvent) { const { shape, name } = evt; const data = shape ? shape.get('origin') : null; // 事件在 view 嵌套中冒泡(暂不提供阻止冒泡的机制) const e = new Event(this, evt, data); e.type = name; return e; } /** * 触发事件之后 * @param evt */ private onDelegateEvents = (evt: GEvent): void => { // 阻止继续冒泡,防止重复事件触发 // evt.preventDefault(); const { name } = evt; if (!name.includes(':')) { return; } // 事件在 view 嵌套中冒泡(暂不提供阻止冒泡的机制) const e = this.createViewEvent(evt); // 包含有基本事件、组合事件 this.emit(name, e); // const currentTarget = evt.currentTarget as IShape; // const inheritNames = currentTarget.get('inheritNames'); // if (evt.delegateObject || inheritNames) { // const events = this.getEvents(); // each(inheritNames, (subName) => { // const eventName = `${subName}:${type}`; // if (events[eventName]) { // this.emit(eventName, e); // } // }); // } }; /** * 处理 PLOT_EVENTS * plot event 需要处理所有的基础事件,并判断是否在画布中,然后再决定是否要 emit。 * 对于 mouseenter、mouseleave 比较特殊,需要做一下数学比较。 * @param e */ private doPlotEvent(e: Event) { const { type, x, y } = e; const point = { x, y }; const ALL_EVENTS = [ 'mousedown', 'mouseup', 'mousemove', 'mouseleave', 'mousewheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'click', 'dblclick', 'contextmenu', ]; if (ALL_EVENTS.includes(type)) { const currentInPlot = this.isPointInPlot(point); const newEvent = e.clone(); if (currentInPlot) { const TYPE = `plot:${type}`; // 组合 plot 事件 newEvent.type = TYPE; this.emit(TYPE, newEvent); if (type === 'mouseleave' || type === 'touchend') { // 在plot 内部却离开画布 this.isPreMouseInPlot = false; } } // 对于 mouseenter, mouseleave 的计算处理 if (type === 'mousemove' || type === 'touchmove') { if (this.isPreMouseInPlot && !currentInPlot) { if (type === 'mousemove') { newEvent.type = PLOT_EVENTS.MOUSE_LEAVE; this.emit(PLOT_EVENTS.MOUSE_LEAVE, newEvent); } newEvent.type = PLOT_EVENTS.LEAVE; this.emit(PLOT_EVENTS.LEAVE, newEvent); } else if (!this.isPreMouseInPlot && currentInPlot) { if (type === 'mousemove') { newEvent.type = PLOT_EVENTS.MOUSE_ENTER; this.emit(PLOT_EVENTS.MOUSE_ENTER, newEvent); } newEvent.type = PLOT_EVENTS.ENTER; this.emit(PLOT_EVENTS.ENTER, newEvent); } // 赋新的状态值 this.isPreMouseInPlot = currentInPlot; } else if (type === 'mouseleave' || type === 'touchend') { // 可能不在 currentInPlot 中 if (this.isPreMouseInPlot) { if (type === 'mouseleave') { newEvent.type = PLOT_EVENTS.MOUSE_LEAVE; this.emit(PLOT_EVENTS.MOUSE_LEAVE, newEvent); } newEvent.type = PLOT_EVENTS.LEAVE; this.emit(PLOT_EVENTS.LEAVE, newEvent); this.isPreMouseInPlot = false; } } } } // view 生命周期 —— 渲染流程 /** * 处理筛选器,筛选数据 * @private */ private doFilterData() { const { data } = this.options; this.filteredData = this.filterData(data); } /** * 初始化 Geometries * @private */ private initGeometries(isUpdate: boolean) { // 初始化图形的之前,先创建 / 更新 scales this.createOrUpdateScales(); // 实例化 Geometry,然后 view 将所有的 scale 管理起来 const coordinate = this.getCoordinate(); const scaleDefs = get(this.options, 'scales', {}); const geometries = this.geometries; for (let i = 0, len = geometries.length; i < len; i++) { const geometry = geometries[i]; // 保持 scales 引用不要变化 geometry.scales = this.getGeometryScales(); const cfg = { coordinate, // 使用 coordinate 引用,可以保持 coordinate 的同步更新 scaleDefs, data: this.filteredData, theme: this.themeObject, isDataChanged: this.isDataChanged, isCoordinateChanged: this.isCoordinateChanged, }; if (isUpdate) { // 数据发生更新 geometry.update(cfg); } else { geometry.init(cfg); } } // Geometry 初始化之后,生成了 scale,然后进行调整 scale 配置 this.adjustScales(); } /** * 根据 Geometry 的所有字段创建 scales * 如果存在,则更新,不存在则创建 */ private createOrUpdateScales() { const fields = this.getScaleFields(); const groupedFields = this.getGroupedFields(); const { data, scales = {} } = this.getOptions(); const filteredData = this.filteredData; for (let i = 0, len = fields.length; i < len; i++) { const field = fields[i]; const scaleDef = scales[field]; // 调用方法,递归去创建 const key = this.getScaleKey(field); this.createScale( field, // 分组字段的 scale 使用未过滤的数据创建 groupedFields.includes(field) ? data : filteredData, scaleDef, key ); // 缓存从当前 view 创建的 scale key this.createdScaleKeys.set(key, true); } } /** * 处理 scale 同步逻辑 */ private syncScale() { // 最终调用 root view 的 this.getRootView().scalePool.sync(this.getCoordinate(), this.theme); } /** * 获得 Geometry 中的 scale 对象 */ private getGeometryScales(): Record { const fields = this.getScaleFields(); const scales = {}; for (let i = 0; i < fields.length; i++) { const field = fields[i]; scales[field] = this.getScaleByField(field); } return scales; } private getScaleFields() { const fields = []; const tmpMap = new Map(); const geometries = this.geometries; for (let i = 0; i < geometries.length; i++) { const geometry = geometries[i]; const geometryScales = geometry.getScaleFields(); uniq(geometryScales, fields, tmpMap); } return fields; } private getGroupedFields() { const fields = []; const tmpMap = new Map(); const geometries = this.geometries; for (let i = 0; i < geometries.length; i++) { const geometry = geometries[i]; const groupFields = geometry.getGroupFields(); uniq(groupFields, fields, tmpMap); } return fields; } /** * 调整 scale 配置 * @private */ private adjustScales() { // 调整目前包括: // 分类 scale,调整 range 范围 this.adjustCategoryScaleRange(); } /** * 调整分类 scale 的 range,防止超出坐标系外面 * @private */ private adjustCategoryScaleRange() { const xyScales = [this.getXScale(), ...this.getYScales()].filter((e) => !!e); const coordinate = this.getCoordinate(); const scaleOptions = this.options.scales; each(xyScales, (scale: Scale) => { const { field, values, isCategory, isIdentity } = scale; // 分类或者 identity 的 scale 才进行处理 if (isCategory || isIdentity) { // 存在 value 值,且用户没有配置 range 配置 if (values && !get(scaleOptions, [field, 'range'])) { // 更新 range scale.range = getDefaultCategoryScaleRange(scale, coordinate, this.theme); } } }); } /** * 根据 options 配置、Geometry 字段配置,自动生成 components * @param isUpdate 是否是更新 * @private */ private initComponents(isUpdate: boolean) { // 先全部清空,然后 render const controllers = this.controllers; for (let i = 0; i < controllers.length; i++) { const controller = controllers[i]; // 更新则走更新逻辑;否则清空载重绘 if (isUpdate) { controller.update(); } else { controller.clear(); controller.render(); } } } private doLayout() { this.layoutFunc(this); } /** * 创建坐标系 * @private */ private createCoordinate() { const start = this.coordinateBBox.bl; const end = this.coordinateBBox.tr; this.coordinateInstance = this.coordinateController.create(start, end); } /** * 根据 options 配置自动渲染 geometry * @private */ private paintGeometries(isUpdate: boolean) { const doAnimation = this.options.animate; // geometry 的 paint 阶段 const coordinate = this.getCoordinate(); const canvasRegion = { x: this.viewBBox.x, y: this.viewBBox.y, minX: this.viewBBox.minX, minY: this.viewBBox.minY, maxX: this.viewBBox.maxX, maxY: this.viewBBox.maxY, width: this.viewBBox.width, height: this.viewBBox.height, }; const geometries = this.geometries; for (let i = 0; i < geometries.length; i++) { const geometry = geometries[i]; geometry.coordinate = coordinate; geometry.canvasRegion = canvasRegion; if (!doAnimation) { // 如果 view 不执行动画,那么 view 下所有的 geometry 都不执行动画 geometry.animate(false); } geometry.paint(isUpdate); } } /** * 最后的绘制组件 * @param isUpdate */ private renderComponents(isUpdate: boolean) { const components = this.getComponents(); // 先全部清空,然后 render for (let i = 0; i < components.length; i++) { const co = components[i]; (co.component as GroupComponent).render(); } } /** * 渲染分面,会在其中进行数据分面,然后进行子 view 创建 * @param isUpdate */ private renderFacet(isUpdate: boolean) { if (this.facetInstance) { if (isUpdate) { this.facetInstance.update(); } else { this.facetInstance.clear(); // 计算分面数据 this.facetInstance.init(); // 渲染组件和 views this.facetInstance.render(); } } } private initOptions() { const { geometries = [], interactions = [], views = [], annotations = [], coordinate, events, facets, } = this.options; // 设置坐标系 if (this.coordinateController) { // 更新 coordinate controller coordinate && this.coordinateController.update(coordinate); } else { // 创建 coordinate controller this.coordinateController = new CoordinateController(coordinate); } // 创建 geometry 实例 for (let i = 0; i < geometries.length; i++) { const geometryOption = geometries[i]; this.createGeometry(geometryOption); } // 创建 interactions 实例 for (let j = 0; j < interactions.length; j++) { const interactionOption = interactions[j]; const { type, cfg } = interactionOption; this.interaction(type, cfg); } // 创建 view 实例 for (let k = 0; k < views.length; k++) { const viewOption = views[k]; this.createView(viewOption); } // 设置 annotation const annotationComponent = this.getController('annotation'); for (let l = 0; l < annotations.length; l++) { const annotationOption = annotations[l]; annotationComponent.annotation(annotationOption); } // 设置 events if (events) { each(events, (eventCallback, eventName) => { this.on(eventName, eventCallback); }); } if (facets) { each(facets, (facet) => { const { type, ...rest } = facet; this.facet(type, rest); }); } } private createGeometry(geometryOption: GeometryOption) { const { type, cfg = {} } = geometryOption; if (this[type]) { const geometry = this[type](cfg); each(geometryOption, (v, k) => { if (isFunction(geometry[k])) { geometry[k](v); } }); } } /** * scale key 的创建方式 * @param field */ private getScaleKey(field: string): string { return `${this.id}-${field}`; } } /** * 注册 geometry 组件 * @param name * @param Ctor * @returns Geometry */ export function registerGeometry(name: string, Ctor: any) { // 语法糖,在 view API 上增加原型方法 View.prototype[name.toLowerCase()] = function (cfg: any = {}) { const props = { /** 图形容器 */ container: this.middleGroup.addGroup(), labelsContainer: this.foregroundGroup.addGroup(), ...cfg, }; const geometry = new Ctor(props); this.geometries.push(geometry); return geometry; }; } export default View;