UNPKG

@antv/g6

Version:

graph visualization frame work

255 lines (247 loc) 8.48 kB
/** * @fileOverview graphic util * @author huangtonger@aliyun.com */ const MathUtil = require('./math'); const BaseUtil = require('./base'); const Global = require('../global'); const PI = Math.PI; const sin = Math.sin; const cos = Math.cos; // 一共支持8个方向的自环,每个环占的角度是45度,在计算时再二分,为22.5度 const SELF_LINK_SIN = sin(PI / 8); const SELF_LINK_COS = cos(PI / 8); function traverse(data, fn) { if (fn(data) === false) { return; } BaseUtil.each(data.children, child => { traverse(child, fn); }); } const GraphicUtil = { getBBox(element, parent) { const bbox = element.getBBox(); let leftTop = { x: bbox.minX, y: bbox.minY }; let rightBottom = { x: bbox.maxX, y: bbox.maxY }; // 根据父元素变换矩阵 if (parent) { const matrix = parent.getMatrix(); leftTop = MathUtil.applyMatrix(leftTop, matrix); rightBottom = MathUtil.applyMatrix(rightBottom, matrix); } return { minX: leftTop.x, minY: leftTop.y, maxX: rightBottom.x, maxY: rightBottom.y }; }, // 获取某元素的自环边配置 getLoopCfgs(cfg) { const item = cfg.sourceNode || cfg.targetNode; const containerMatrix = item.get('group') .getMatrix(); const bbox = item.getKeyShape() .getBBox(); const loopCfg = cfg.loopCfg || {}; // 距离keyShape边的最高距离 const dist = loopCfg.dist || Math.max(bbox.width, bbox.height) * 2; // 自环边与keyShape的相对位置关系 const position = loopCfg.position || Global.loopPosition; const r = Math.max(bbox.width, bbox.height) / 2; const scaleRate = (r + dist) / r; // 中心取group上真实位置 const center = [ containerMatrix[ 6 ], containerMatrix[ 7 ] ]; const sinDelta = r * SELF_LINK_SIN; const cosDelta = r * SELF_LINK_COS; let startPoint = [ cfg.startPoint.x, cfg.startPoint.y ]; let endPoint = [ cfg.endPoint.x, cfg.endPoint.y ]; // 如果定义了锚点的,直接用锚点坐标,否则,根据自环的 cfg 计算 if (startPoint[0] === endPoint[0] && startPoint[1] === endPoint[1]) { switch (position) { case 'top': startPoint = [ center[0] - sinDelta, center[1] - cosDelta ]; endPoint = [ center[0] + sinDelta, center[1] - cosDelta ]; break; case 'top-right': startPoint = [ center[0] + sinDelta, center[1] - cosDelta ]; endPoint = [ center[0] + cosDelta, center[1] - sinDelta ]; break; case 'right': startPoint = [ center[0] + cosDelta, center[1] - sinDelta ]; endPoint = [ center[0] + cosDelta, center[1] + sinDelta ]; break; case 'bottom-right': startPoint = [ center[0] + cosDelta, center[1] + sinDelta ]; endPoint = [ center[0] + sinDelta, center[1] + cosDelta ]; break; case 'bottom': startPoint = [ center[0] + sinDelta, center[1] + cosDelta ]; endPoint = [ center[0] - sinDelta, center[1] + cosDelta ]; break; case 'bottom-left': startPoint = [ center[0] - sinDelta, center[1] + cosDelta ]; endPoint = [ center[0] - cosDelta, center[1] + sinDelta ]; break; case 'left': startPoint = [ center[0] - cosDelta, center[1] + sinDelta ]; endPoint = [ center[0] - cosDelta, center[1] - sinDelta ]; break; case 'top-left': startPoint = [ center[0] - cosDelta, center[1] - sinDelta ]; endPoint = [ center[0] - sinDelta, center[1] - cosDelta ]; break; default: startPoint = [ center[0] - sinDelta, center[1] - cosDelta ]; endPoint = [ center[0] + sinDelta, center[1] - cosDelta ]; } // 如果逆时针画,交换起点和终点 if (loopCfg.clockwise === false) { const swap = [ startPoint[0], startPoint[1] ]; startPoint = [ endPoint[0], endPoint[1] ]; endPoint = [ swap[0], swap[1] ]; } } const startVec = [ startPoint[0] - center[0], startPoint[1] - center[1] ]; const startExtendVec = BaseUtil.vec2.scale([], startVec, scaleRate); const controlPoint1 = [ center[0] + startExtendVec[0], center[1] + startExtendVec[1] ]; const endVec = [ endPoint[0] - center[0], endPoint[1] - center[1] ]; const endExtendVec = BaseUtil.vec2.scale([], endVec, scaleRate); const controlPoint2 = [ center[0] + endExtendVec[0], center[1] + endExtendVec[1] ]; cfg.startPoint = { x: startPoint[0], y: startPoint[1] }; cfg.endPoint = { x: endPoint[0], y: endPoint[1] }; cfg.controlPoints = [ { x: controlPoint1[0], y: controlPoint1[1] }, { x: controlPoint2[0], y: controlPoint2[1] } ]; return cfg; }, traverseTree(data, fn) { if (typeof fn !== 'function') { return; } traverse(data, fn); }, radialLayout(data, layout) { // 布局方式有 H / V / LR / RL / TB / BT const VERTICAL_LAYOUTS = [ 'V', 'TB', 'BT' ]; const min = { x: Infinity, y: Infinity }; const max = { x: -Infinity, y: -Infinity }; // 默认布局是垂直布局TB,此时x对应rad,y对应r let rScale = 'x'; let radScale = 'y'; if (layout && VERTICAL_LAYOUTS.indexOf(layout) >= 0) { // 若是水平布局,y对应rad,x对应r radScale = 'x'; rScale = 'y'; } let count = 0; this.traverseTree(data, node => { count++; if (node.x > max.x) { max.x = node.x; } if (node.x < min.x) { min.x = node.x; } if (node.y > max.y) { max.y = node.y; } if (node.y < min.y) { min.y = node.y; } }); const avgRad = PI * 2 / count; const radDiff = max[radScale] - min[radScale]; if (radDiff === 0) { return data; } this.traverseTree(data, node => { const radial = (node[radScale] - min[radScale]) / radDiff * (PI * 2 - avgRad) + avgRad; const r = Math.abs(rScale === 'x' ? node.x - data.x : node.y - data.y); node.x = r * Math.cos(radial); node.y = r * Math.sin(radial); }); return data; }, /** * 根据 label 所在线条的位置百分比,计算 label 坐标 * @param {object} pathShape G 的 path 实例,一般是 Edge 实例的 keyShape * @param {number} percent 范围 0 - 1 的线条百分比 * @param {number} refX x 轴正方向为基准的 label 偏移 * @param {number} refY y 轴正方向为基准的 label 偏移 * @param {boolean} rotate 是否根据线条斜率旋转文本 * @return {object} 文本的 x, y, 文本的旋转角度 */ getLabelPosition(pathShape, percent, refX, refY, rotate) { const TAN_OFFSET = 0.0001; let vector = []; const point = pathShape.getPoint(percent); if (point === null) { return { x: 0, y: 0, angle: 0 }; } // 头尾最可能,放在最前面,使用 g path 上封装的方法 if (percent < TAN_OFFSET) { vector = pathShape.getStartTangent().reverse(); } else if (percent > (1 - TAN_OFFSET)) { vector = pathShape.getEndTangent(); } else { // 否则取指定位置的点,与少量偏移的点,做微分向量 const offsetPoint = pathShape.getPoint(percent + TAN_OFFSET); vector.push([ point.x, point.y ]); vector.push([ offsetPoint.x, offsetPoint.y ]); } let rad = Math.atan2(vector[1][1] - vector[0][1], vector[1][0] - vector[0][0]); if (rad < 0) { rad += PI * 2; } if (refX) { point.x += cos(rad) * refX; point.y += sin(rad) * refX; } if (refY) { // 默认方向是 x 轴正方向,法线是 求出角度 - 90° let normal = rad - PI / 2; // 若法线角度在 y 轴负方向,切到正方向,保证 refY 相对于 y 轴正方向 if (rad > 1 / 2 * PI && rad < 3 * 1 / 2 * PI) { normal -= PI; } point.x += cos(normal) * refY; point.y += sin(normal) * refY; } // 需要原始的旋转角度计算 textAlign const result = { x: point.x, y: point.y, angle: rad }; if (rotate) { if (rad > 1 / 2 * PI && rad < 3 * 1 / 2 * PI) { rad -= PI; } return { rotate: rad, ...result }; } return result; } }; module.exports = GraphicUtil;