@antv/g6
Version:
graph visualization frame work
255 lines (247 loc) • 8.48 kB
JavaScript
/**
* @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;