import { Action, IGroup, Util } from '@antv/g2'; import { get, isNil, last, size } from '@antv/util'; import { Data } from '../../types'; import { DrillDownCfg } from '../../types/drill-down'; import { deepAssign } from '../../utils/deep-assign'; // 面包屑文字和分割符'/'之间的距离 const PADDING = 4; // 面包屑位置距离树图的距离 const PADDING_LEFT = 0; // 面包屑位置距离树图的顶部距离 export const PADDING_TOP = 5; /** Group name of breadCrumb: 面包屑 */ export const BREAD_CRUMB_NAME = 'drilldown-bread-crumb'; // 面包屑默认配置 export const DEFAULT_BREAD_CRUMB_CONFIG: DrillDownCfg['breadCrumb'] = { /** 位置,默认:左上角 */ position: 'top-left', dividerText: '/', textStyle: { fontSize: 12, fill: 'rgba(0, 0, 0, 0.65)', cursor: 'pointer', }, activeTextStyle: { fill: '#87B5FF', }, }; /** * hierarchy 数据转换的参数 */ export const HIERARCHY_DATA_TRANSFORM_PARAMS = 'hierarchy-data-transform-params'; /** * Hierarchy plot 节点的数据 */ export type HierarchyNode = { /** 节点的原始数据,树型结构(todo 是否更正 key 为 origin) */ data: { name: string; value?: any; children: { name: string; value?: any }[] }; /** 在构建节点数据时候,增加的扩展配置, 用于存储 transformData 的入参配置 */ [HIERARCHY_DATA_TRANSFORM_PARAMS]: object; /** 当前的层级结构,每一次下钻都会更新. 不是 unique */ depth: number; /** 当前所处高度,depth + height = 总的层级 */ height: number; parent: N; children: N[]; }; type HistoryCache = { name: string; id: string; children: Data; }[]; /** * @description 下钻交互的 action * @author liuzhenying * * 适用于:hierarchy plot */ export class DrillDownAction extends Action { /** Action name */ public name = 'drill-down'; // 存储历史下钻数据 protected historyCache: HistoryCache = []; // 面包屑 group private breadCrumbGroup: IGroup = null; // 面包屑基础配置 private breadCrumbCfg: DrillDownCfg['breadCrumb'] = DEFAULT_BREAD_CRUMB_CONFIG; /** * 点击事件, 下钻数据,并绘制面包屑 */ public click() { const data = get(this.context, ['event', 'data', 'data']); if (!data) return false; this.drill(data); this.drawBreadCrumb(); } /** * 重置位置,初始化及触发 chart afterchangesize 回调时使用 */ public resetPosition() { // 当在第一层级未绘制面包屑,此时 changedata 触发 resetPosition 函数,需判断 this.breadCrumbGroup 是否存在 if (!this.breadCrumbGroup) return; const coordinate = this.context.view.getCoordinate(); const breadCrumbGroup = this.breadCrumbGroup; const bbox = breadCrumbGroup.getBBox(); const { position } = this.getButtonCfg(); // @todo 后续抽取一个函数来处理,以及增加 margin 或者 padding 的设置 // 非 polar 的,需要使用 coordinate,除却图表组件 let point = { x: coordinate.start.x, y: coordinate.end.y - (bbox.height + PADDING_TOP * 2) }; if (coordinate.isPolar) { // 默认,左上角直接出发 point = { x: 0, y: 0 }; } if (position === 'bottom-left') { // 涉及到坐标反转的问题 point = { x: coordinate.start.x, y: coordinate.start.y }; } /** PADDING_LEFT, PADDING_TOP 与画布边缘的距离 */ const matrix = Util.transform(null, [['t', point.x + PADDING_LEFT, point.y + bbox.height + PADDING_TOP]]); breadCrumbGroup.setMatrix(matrix); } /** * 返回上一层 */ public back(): void { if (size(this.historyCache)) { this.backTo(this.historyCache.slice(0, -1)); } } /** * 重置 */ public reset(): void { if (this.historyCache[0]) { this.backTo(this.historyCache.slice(0, 1)); } // 清空 this.historyCache = []; this.hideCrumbGroup(); } /** * 下钻数据并更新 view 显示层 * @param nodeInfo 下钻数据 */ protected drill(nodeInfo: HierarchyNode) { const { view } = this.context; const transformData = get(view, ['interactions', 'drill-down', 'cfg', 'transformData'], (v) => v); // 重新 update 数据 const drillData = transformData({ data: nodeInfo.data, ...nodeInfo[HIERARCHY_DATA_TRANSFORM_PARAMS] }); view.changeData(drillData); // 存储历史记录 const historyCache: HistoryCache = []; let node = nodeInfo; while (node) { const nodeData = node.data; historyCache.unshift({ id: `${nodeData.name}_${node.height}_${node.depth}`, name: nodeData.name, // children 是实际数据 children: transformData({ data: nodeData, ...nodeInfo[HIERARCHY_DATA_TRANSFORM_PARAMS] }), }); node = node.parent; } this.historyCache = (this.historyCache || []).slice(0, -1).concat(historyCache); } /** * 回退事件,点击面包屑时触发 * @param historyCache 当前要回退到的历史 */ protected backTo(historyCache: HistoryCache) { if (!historyCache || historyCache.length <= 0) { return; } const { view } = this.context; const data = last(historyCache).children; // 处理后的数组 view.changeData(data); if (historyCache.length > 1) { this.historyCache = historyCache; this.drawBreadCrumb(); } else { // 清空 this.historyCache = []; this.hideCrumbGroup(); } } /** * 获取 mix 默认的配置和用户配置 */ private getButtonCfg() { const { view } = this.context; const drillDownConfig: DrillDownCfg = get(view, ['interactions', 'drill-down', 'cfg', 'drillDownConfig']); return deepAssign(this.breadCrumbCfg, drillDownConfig?.breadCrumb, this.cfg); } /** * 显示面包屑 */ private drawBreadCrumb() { this.drawBreadCrumbGroup(); this.resetPosition(); this.breadCrumbGroup.show(); } /** * 绘制 Button 和 文本 */ private drawBreadCrumbGroup() { const config = this.getButtonCfg(); const cache = this.historyCache; // 初始化面包屑 group if (!this.breadCrumbGroup) { this.breadCrumbGroup = this.context.view.foregroundGroup.addGroup({ name: BREAD_CRUMB_NAME, }); } else { this.breadCrumbGroup.clear(); } // 绘制面包屑 let left = 0; cache.forEach((record, index) => { // 添加文本 const textShape = this.breadCrumbGroup.addShape({ type: 'text', id: record.id, name: `${BREAD_CRUMB_NAME}_${record.name}_text`, attrs: { text: index === 0 && !isNil(config.rootText) ? config.rootText : record.name, ...config.textStyle, x: left, y: 0, }, }); const textShapeBox = textShape.getBBox(); left += textShapeBox.width + PADDING; // 增加文本事件 textShape.on('click', (event) => { const targetId = event.target.get('id'); if (targetId !== last(cache)?.id) { const newHistoryCache = cache.slice(0, cache.findIndex((d) => d.id === targetId) + 1); this.backTo(newHistoryCache); } }); // active 效果内置 textShape.on('mouseenter', (event) => { const targetId = event.target.get('id'); if (targetId !== last(cache)?.id) { textShape.attr(config.activeTextStyle); } else { textShape.attr({ cursor: 'default' }); } }); textShape.on('mouseleave', () => { textShape.attr(config.textStyle); }); if (index < cache.length - 1) { // 添加反斜杠 const dividerShape = this.breadCrumbGroup.addShape({ type: 'text', name: `${config.name}_${record.name}_divider`, attrs: { text: config.dividerText, ...config.textStyle, x: left, y: 0, }, }); const dividerBox = dividerShape.getBBox(); left += dividerBox.width + PADDING; } }); } /** * 隐藏面包屑 */ private hideCrumbGroup() { if (this.breadCrumbGroup) { this.breadCrumbGroup.hide(); } } /** * @override * destroy: 销毁资源 */ public destroy() { if (this.breadCrumbGroup) { this.breadCrumbGroup.remove(); } super.destroy(); } }