@antv/g6
Version:
graph visualization frame work
1,146 lines (1,024 loc) • 34.1 kB
JavaScript
/*
* @Author: moyee
* @Date: 2019-07-30 12:10:26
* @LastEditors: moyee
* @LastEditTime: 2019-08-23 11:44:32
* @Description: Group Controller
*/
const isString = require('@antv/util/lib/type/is-string');
const deepMix = require('@antv/util/lib/deep-mix');
class CustomGroup {
getDefaultCfg() {
return {
default: {
lineWidth: 1,
stroke: '#A3B1BF',
// lineDash: [ 5, 5 ],
strokeOpacity: 0.9,
fill: '#F3F9FF',
fillOpacity: 0.8,
opacity: 0.8,
disCoefficient: 0.6,
minDis: 40,
maxDis: 100
},
hover: {
stroke: '#faad14',
fill: '#ffe58f',
fillOpacity: 0.3,
opacity: 0.3,
lineWidth: 3
},
// 收起状态样式
collapse: {
r: 30,
width: 80,
height: 40,
// lineDash: [ 5, 5 ],
stroke: '#A3B1BF',
lineWidth: 3,
fill: '#F3F9FF',
offsetX: -15,
offsetY: 5
},
icon: 'https://gw.alipayobjects.com/zos/rmsportal/MXXetJAxlqrbisIuZxDO.svg',
operatorBtn: {
collapse: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/uZVdwjJGqDooqKLKtvGA.svg',
width: 16,
height: 16
},
expand: {
width: 16,
height: 16,
img: 'https://gw.alipayobjects.com/zos/rmsportal/MXXetJAxlqrbisIuZxDO.svg'
}
},
visible: false
};
}
constructor(graph) {
// const { cfg = {} } = options;
this.graph = graph;
const groupStyle = graph.get('groupStyle');
this.styles = deepMix({}, this.getDefaultCfg(), groupStyle);
// 创建的群组集合
this.customGroup = {};
this.delegateInGroup = {};
this.nodePoint = [];
}
/**
* 生成群组
* @param {string} groupId 群组ID
* @param {array} nodes 群组中的节点集合
* @param {string} type 群组类型,默认为circle,支持rect
* @param {number} zIndex 群组层级,默认为0
* @param {boolean} updateDataModel 是否更新节点数据,默认为false,只有当手动创建group时才为true
* @param {object} title 分组标题配置
* @memberof ItemGroup
* @return {object} null
*/
create(groupId, nodes, type = 'circle', zIndex = 0, updateDataModel = false, title = {}) {
const graph = this.graph;
const customGroup = graph.get('customGroup');
const hasGroupIds = customGroup.get('children').map(data => data.get('id'));
if (hasGroupIds.indexOf(groupId) > -1) {
return console.warn(`已经存在ID为 ${groupId} 的分组,请重新设置分组ID!`);
}
const nodeGroup = customGroup.addGroup({
id: groupId,
zIndex
});
const autoPaint = graph.get('autoPaint');
graph.setAutoPaint(false);
const { default: defaultStyle } = this.styles;
// 计算群组左上角左边、宽度、高度及x轴方向上的最大值
const { x, y, width, height, maxX } = this.calculationGroupPosition(nodes);
const paddingValue = this.getGroupPadding(groupId);
const groupBBox = graph.get('groupBBoxs');
groupBBox[groupId] = { x, y, width, height, maxX };
// 根据groupId获取group数据,判断是否需要添加title
let groupTitle = null;
// 只有手动创建group时执行以下逻辑
if (updateDataModel) {
const groups = graph.get('groups');
// 如果是手动创建group,则原始数据中是没有groupId信息的,需要将groupId添加到node中
nodes.forEach(nodeId => {
const node = graph.findById(nodeId);
const model = node.getModel();
if (!model.groupId) {
model.groupId = groupId;
}
});
// 如果是手动创建 group,则将 group 也添加到 groups 中
if (!groups.find(data => data.id === groupId)) {
groups.push({
id: groupId,
title
});
graph.set({
groups
});
}
}
const groupData = graph.get('groups').filter(data => data.id === groupId);
if (groupData && groupData.length > 0) {
groupTitle = groupData[0].title;
}
// group title 坐标
let titleX = 0;
let titleY = 0;
// step 1:绘制群组外框
let keyShape = null;
if (type === 'circle') {
const r = width > height ? width / 2 : height / 2;
const cx = (width + 2 * x) / 2;
const cy = (height + 2 * y) / 2;
const lastR = r + paddingValue;
keyShape = nodeGroup.addShape('circle', {
attrs: {
...defaultStyle,
x: cx,
y: cy,
r: lastR
},
capture: true,
zIndex,
groupId
});
titleX = cx;
titleY = cy - lastR;
// 更新群组及属性样式
this.setDeletageGroupByStyle(groupId, nodeGroup,
{ width, height, x: cx, y: cy, r: lastR });
} else {
const rectPadding = paddingValue * defaultStyle.disCoefficient;
keyShape = nodeGroup.addShape('rect', {
attrs: {
...defaultStyle,
x: x - rectPadding,
y: y - rectPadding,
width: width + rectPadding * 2,
height: height + rectPadding * 2
},
capture: true,
zIndex,
groupId
});
titleX = x - rectPadding + 15;
titleY = y - rectPadding + 15;
// 更新群组及属性样式
this.setDeletageGroupByStyle(groupId, nodeGroup, {
x: x - rectPadding,
y: y - rectPadding,
width: width + rectPadding,
height: height + rectPadding,
btnOffset: maxX - 3
});
}
// 添加group标题
if (groupTitle) {
const { offsetX = 0, offsetY = 0, text = groupTitle, ...titleStyle } = groupTitle;
const textShape = nodeGroup.addShape('text', {
attrs: {
text,
stroke: '#444',
x: titleX + offsetX,
y: titleY + offsetY,
...titleStyle
},
className: 'group-title'
});
textShape.set('capture', false);
}
nodeGroup.set('keyShape', keyShape);
// 设置graph中groupNodes的值
graph.get('groupNodes')[groupId] = nodes;
graph.setAutoPaint(autoPaint);
graph.paint();
}
/**
* 修改Group样式
* @param {Item} keyShape 群组的keyShape
* @param {Object | String} style 样式
*/
setGroupStyle(keyShape, style) {
if (!keyShape || keyShape.get('destroyed')) {
return;
}
let styles = {};
const { hover: hoverStyle, default: defaultStyle } = this.styles;
if (isString(style)) {
if (style === 'default') {
styles = deepMix({}, defaultStyle);
} else if (style === 'hover') {
styles = deepMix({}, hoverStyle);
}
} else {
styles = deepMix({}, defaultStyle, style);
}
for (const s in styles) {
keyShape.attr(s, styles[s]);
}
}
/**
* 根据GroupID计算群组位置,包括左上角左边及宽度和高度
*
* @param {object} nodes 符合条件的node集合:选中的node或具有同一个groupID的node
* @return {object} 根据节点计算出来的包围盒坐标
* @memberof ItemGroup
*/
calculationGroupPosition(nodes) {
const graph = this.graph;
let minx = Infinity;
let maxx = -Infinity;
let miny = Infinity;
let maxy = -Infinity;
// 获取已节点的所有最大最小x y值
for (const id of nodes) {
const element = isString(id) ? graph.findById(id) : id;
const bbox = element.getBBox();
const { minX, minY, maxX, maxY } = bbox;
if (minX < minx) {
minx = minX;
}
if (minY < miny) {
miny = minY;
}
if (maxX > maxx) {
maxx = maxX;
}
if (maxY > maxy) {
maxy = maxY;
}
}
const x = Math.floor(minx);
const y = Math.floor(miny);
const width = Math.ceil(maxx) - x;
const height = Math.ceil(maxy) - y;
return {
x,
y,
width,
height,
maxX: Math.ceil(maxx)
};
}
/**
* 当group中含有group时,获取padding值
* @param {string} groupId 节点分组ID
* @return {number} 在x和y方向上的偏移值
*/
getGroupPadding(groupId) {
const graph = this.graph;
const { default: defaultStyle } = this.styles;
// 检测操作的群组中是否包括子群组
const groups = graph.get('groups');
const hasSubGroup = !!groups.filter(g => g.parentId === groupId).length > 0;
const paddingValue = hasSubGroup ? defaultStyle.maxDis : defaultStyle.minDis;
return paddingValue;
}
/**
* 设置群组对象及属性值
*
* @param {string} groupId 群组ID
* @param {Group} deletage 群组元素
* @param {object} property 属性值,里面包括width、height和maxX
* @memberof ItemGroup
*/
setDeletageGroupByStyle(groupId, deletage, property) {
const { width, height, x, y, r, btnOffset } = property;
const customGroupStyle = this.customGroup[groupId];
if (!customGroupStyle) {
// 首次赋值
this.customGroup[groupId] = {
nodeGroup: deletage,
groupStyle: {
width,
height,
x,
y,
r,
btnOffset
}
};
} else {
// 更新时候merge配置项
const { groupStyle } = customGroupStyle;
const styles = deepMix({}, groupStyle, property);
this.customGroup[groupId] = {
nodeGroup: deletage,
groupStyle: styles
};
}
}
/**
* 根据群组ID获取群组及属性对象
*
* @param {string} groupId 群组ID
* @return {Item} 群组
* @memberof ItemGroup
*/
getDeletageGroupById(groupId) {
return this.customGroup[groupId];
}
/**
* 收起和展开群组
* @param {string} groupId 群组ID
*/
collapseExpandGroup(groupId) {
const customGroup = this.getDeletageGroupById(groupId);
const { nodeGroup } = customGroup;
const hasHidden = nodeGroup.get('hasHidden');
// 该群组已经处于收起状态,需要展开
if (hasHidden) {
nodeGroup.set('hasHidden', false);
this.expandGroup(groupId);
} else {
nodeGroup.set('hasHidden', true);
this.collapseGroup(groupId);
}
}
/**
* 将临时节点递归地设置到groupId及父节点上
* @param {string} groupId 群组ID
* @param {string} tmpNodeId 临时节点ID
*/
setGroupTmpNode(groupId, tmpNodeId) {
const graph = this.graph;
const graphNodes = graph.get('groupNodes');
const groups = graph.get('groups');
if (graphNodes[groupId].indexOf(tmpNodeId) < 0) {
graphNodes[groupId].push(tmpNodeId);
}
// 获取groupId的父群组
const parentGroup = groups.filter(g => g.id === groupId);
let parentId = null;
if (parentGroup.length > 0) {
parentId = parentGroup[0].parentId;
}
// 如果存在父群组,则把临时元素也添加到父群组中
if (parentId) {
this.setGroupTmpNode(parentId, tmpNodeId);
}
}
/**
* 收起群组,隐藏群组中的节点及边,群组外部相邻的边都连接到群组上
*
* @param {string} id 群组ID
* @memberof ItemGroup
*/
collapseGroup(id) {
const customGroup = this.getDeletageGroupById(id);
const { nodeGroup } = customGroup;
// 收起群组后的默认样式
const { collapse } = this.styles;
const graph = this.graph;
const groupType = graph.get('groupType');
const autoPaint = graph.get('autoPaint');
graph.setAutoPaint(false);
const nodesInGroup = graph.get('groupNodes')[id];
const { width: w, height: h } = this.calculationGroupPosition(nodesInGroup);
// 更新Group的大小
const keyShape = nodeGroup.get('keyShape');
const { r, width, height, offsetX, offsetY, ...otherStyle } = collapse;
for (const style in otherStyle) {
keyShape.attr(style, otherStyle[style]);
}
let options = {
groupId: id,
id: `${id}-custom-node`,
x: keyShape.attr('x'),
y: keyShape.attr('y'),
style: {
r
},
shape: 'circle'
};
const titleShape = nodeGroup.findByClassName('group-title');
// 收起群组时候动画
if (groupType === 'circle') {
const radius = keyShape.attr('r');
keyShape.animate({
onFrame(ratio) {
return {
r: radius - ratio * (radius - r)
};
}
}, 500, 'easeCubic');
if (titleShape) {
titleShape.attr({
x: keyShape.attr('x') + offsetX,
y: keyShape.attr('y') + offsetY
});
}
} else if (groupType === 'rect') {
keyShape.animate({
onFrame(ratio) {
return {
width: w - ratio * (w - width),
height: h - ratio * (h - height)
};
}
}, 500, 'easeCubic');
if (titleShape) {
titleShape.attr({
x: keyShape.attr('x') + 10,
y: keyShape.attr('y') + height / 2 + 5
});
}
options = {
groupId: id,
id: `${id}-custom-node`,
x: keyShape.attr('x') + width / 2,
y: keyShape.attr('y') + height / 2,
size: [ width, height ],
shape: 'rect'
};
}
const edges = graph.getEdges();
// 获取所有source在群组外,target在群组内的边
const sourceOutTargetInEdges = edges.filter(edge => {
const model = edge.getModel();
return !nodesInGroup.includes(model.source) && nodesInGroup.includes(model.target);
});
// 获取所有source在群组外,target在群组内的边
const sourceInTargetOutEdges = edges.filter(edge => {
const model = edge.getModel();
return nodesInGroup.includes(model.source) && !nodesInGroup.includes(model.target);
});
// 获取群组中节点之间的所有边
const edgeAllInGroup = edges.filter(edge => {
const model = edge.getModel();
return nodesInGroup.includes(model.source) && nodesInGroup.includes(model.target);
});
// 隐藏群组中的所有节点
nodesInGroup.forEach(nodeId => {
const node = graph.findById(nodeId);
const model = node.getModel();
const { groupId } = model;
if (groupId && groupId !== id) {
// 存在群组,则隐藏
const currentGroup = this.getDeletageGroupById(groupId);
const { nodeGroup } = currentGroup;
nodeGroup.hide();
}
node.hide();
});
edgeAllInGroup.forEach(edge => {
const source = edge.getSource();
const target = edge.getTarget();
if (source.isVisible() && target.isVisible()) {
edge.show();
} else {
edge.hide();
}
});
// 群组中存在source和target其中有一个在群组内,一个在群组外的情况
if (sourceOutTargetInEdges.length > 0 || sourceInTargetOutEdges.length > 0) {
const delegateNode = graph.add('node', options);
delegateNode.set('capture', false);
delegateNode.hide();
this.delegateInGroup[id] = {
delegateNode
};
// 将临时添加的节点加入到群组中,以便拖动节点时候线跟着拖动
this.setGroupTmpNode(id, `${id}-custom-node`);
this.updateEdgeInGroupLinks(id, sourceOutTargetInEdges, sourceInTargetOutEdges);
}
graph.paint();
graph.setAutoPaint(autoPaint);
}
/**
* 收起群组时生成临时的节点,用于连接群组外的节点
*
* @param {string} groupId 群组ID
* @param {array} sourceOutTargetInEdges 出度的边
* @param {array} sourceInTargetOutEdges 入度的边
* @memberof ItemGroup
*/
updateEdgeInGroupLinks(groupId, sourceOutTargetInEdges, sourceInTargetOutEdges) {
const graph = this.graph;
// 更新source在外的节点
const edgesOuts = {};
sourceOutTargetInEdges.map(edge => {
const model = edge.getModel();
const id = edge.get('id');
const { target } = model;
edgesOuts[id] = target;
graph.updateItem(edge, {
target: `${groupId}-custom-node`
});
return true;
});
// 更新target在外的节点
const edgesIn = {};
sourceInTargetOutEdges.map(edge => {
const model = edge.getModel();
const id = edge.get('id');
const { source } = model;
edgesIn[id] = source;
graph.updateItem(edge, {
source: `${groupId}-custom-node`
});
return true;
});
// 缓存群组groupId下的edge和临时生成的node节点
this.delegateInGroup[groupId] = deepMix({
sourceOutTargetInEdges,
sourceInTargetOutEdges,
edgesOuts,
edgesIn
}, this.delegateInGroup[groupId]);
}
/**
* 展开群组,恢复群组中的节点及边
*
* @param {string} id 群组ID
* @memberof ItemGroup
*/
expandGroup(id) {
const graph = this.graph;
const groupType = graph.get('groupType');
const autoPaint = graph.get('autoPaint');
graph.setAutoPaint(false);
// 显示之前隐藏的节点和群组
const nodesInGroup = graph.get('groupNodes')[id];
const noCustomNodes = nodesInGroup.filter(node => node.indexOf('custom-node') === -1);
const { width, height } = this.calculationGroupPosition(noCustomNodes);
const { nodeGroup } = this.getDeletageGroupById(id);
const keyShape = nodeGroup.get('keyShape');
const { default: defaultStyle, collapse } = this.styles;
for (const style in defaultStyle) {
keyShape.attr(style, defaultStyle[style]);
}
const titleShape = nodeGroup.findByClassName('group-title');
// 检测操作的群组中是否包括子群组
const paddingValue = this.getGroupPadding(id);
if (groupType === 'circle') {
const r = width > height ? width / 2 : height / 2;
keyShape.animate({
onFrame(ratio) {
return {
r: collapse.r + ratio * (r - collapse.r + paddingValue)
};
}
}, 500, 'easeCubic');
} else if (groupType === 'rect') {
const { width: w, height: h } = collapse;
keyShape.animate({
onFrame(ratio) {
return {
width: w + ratio * (width - w + paddingValue * defaultStyle.disCoefficient * 2),
height: h + ratio * (height - h + paddingValue * defaultStyle.disCoefficient * 2)
};
}
}, 500, 'easeCubic');
}
if (titleShape) {
// 根据groupId获取group数据,判断是否需要添加title
let groupTitle = null;
const groupData = graph.get('groups').filter(data => data.id === id);
if (groupData && groupData.length > 0) {
groupTitle = groupData[0].title;
}
const { offsetX = 0, offsetY = 0 } = groupTitle;
if (groupType === 'circle') {
titleShape.animate({
onFrame(ratio) {
return {
x: keyShape.attr('x') + offsetX,
y: keyShape.attr('y') - ratio * keyShape.attr('r') + offsetY
};
}
}, 600, 'easeCubic');
} else if (groupType === 'rect') {
titleShape.animate({
onFrame(ratio) {
return {
x: keyShape.attr('x') + ratio * (15 + offsetX),
y: keyShape.attr('y') + ratio * (15 + offsetY)
};
}
}, 600, 'easeCubic');
}
}
// 群组动画一会后再显示节点和边
setTimeout(() => {
nodesInGroup.forEach(nodeId => {
const node = graph.findById(nodeId);
const model = node.getModel();
const { groupId } = model;
if (groupId && groupId !== id) {
// 存在群组,则显示
const currentGroup = this.getDeletageGroupById(groupId);
const { nodeGroup } = currentGroup;
nodeGroup.show();
const hasHidden = nodeGroup.get('hasHidden');
if (!hasHidden) {
node.show();
}
} else {
node.show();
}
});
const edges = graph.getEdges();
// 获取群组中节点之间的所有边
const edgeAllInGroup = edges.filter(edge => {
const model = edge.getModel();
return nodesInGroup.includes(model.source) || nodesInGroup.includes(model.target);
});
edgeAllInGroup.forEach(edge => {
const source = edge.getSource();
const target = edge.getTarget();
if (source.isVisible() && target.isVisible()) {
edge.show();
}
});
}, 300);
const delegates = this.delegateInGroup[id];
if (delegates) {
const { sourceOutTargetInEdges,
sourceInTargetOutEdges,
edgesOuts,
edgesIn,
delegateNode } = delegates;
// 恢复source在外的节点
sourceOutTargetInEdges.map(edge => {
const id = edge.get('id');
const sourceOuts = edgesOuts[id];
graph.updateItem(edge, {
target: sourceOuts
});
return true;
});
// 恢复target在外的节点
sourceInTargetOutEdges.map(edge => {
const id = edge.get('id');
const sourceIn = edgesIn[id];
graph.updateItem(edge, {
source: sourceIn
});
return true;
});
// 删除群组中的临时节点ID
const tmpNodeModel = delegateNode.getModel();
this.deleteTmpNode(id, tmpNodeModel.id);
graph.remove(delegateNode);
delete this.delegateInGroup[id];
}
graph.setAutoPaint(autoPaint);
graph.paint();
}
deleteTmpNode(groupId, tmpNodeId) {
const graph = this.graph;
const groups = graph.get('groups');
const nodesInGroup = graph.get('groupNodes')[groupId];
const index = nodesInGroup.indexOf(tmpNodeId);
nodesInGroup.splice(index, 1);
// 获取groupId的父群组
const parentGroup = groups.filter(g => g.id === groupId);
let parentId = null;
if (parentGroup.length > 0) {
parentId = parentGroup[0].parentId;
}
// 如果存在父群组,则把临时元素也添加到父群组中
if (parentId) {
this.deleteTmpNode(parentId, tmpNodeId);
}
}
/**
* 删除节点分组
* @param {string} groupId 节点分组ID
* @memberof ItemGroup
*/
remove(groupId) {
const graph = this.graph;
const customGroup = this.getDeletageGroupById(groupId);
if (!customGroup) {
console.warn(`请确认输入的groupId ${groupId} 是否有误!`);
return;
}
const { nodeGroup } = customGroup;
const autoPaint = graph.get('autoPaint');
graph.setAutoPaint(false);
const groupNodes = graph.get('groupNodes');
const nodes = groupNodes[groupId];
// 删除原群组中node中的groupID
nodes.forEach(nodeId => {
const node = graph.findById(nodeId);
const model = node.getModel();
const gId = model.groupId;
if (!gId) {
return;
}
if (groupId === gId) {
delete model.groupId;
// 使用没有groupID的数据更新节点
graph.updateItem(node, model);
}
});
nodeGroup.destroy();
// 删除customGroup中groupId的数据
delete this.customGroup[groupId];
// 删除groups数据中的groupId
const groups = graph.get('groups');
if (groups.length > 0) {
const filterGroup = groups.filter(group => group.id !== groupId);
graph.set('groups', filterGroup);
}
let parentGroupId = null;
let parentGroupData = null;
for (const group of groups) {
if (groupId !== group.id) {
continue;
}
parentGroupId = group.parentId;
parentGroupData = group;
break;
}
if (parentGroupData) {
delete parentGroupData.parentId;
}
// 删除groupNodes中的groupId数据
delete groupNodes[groupId];
if (parentGroupId) {
groupNodes[parentGroupId] = groupNodes[parentGroupId].filter(node => !nodes.includes(node));
}
graph.setAutoPaint(autoPaint);
graph.paint();
}
/**
* 更新节点分组位置及里面的节点和边的位置
* @param {string} groupId 节点分组ID
* @param {object} position delegate的坐标位置
*/
updateGroup(groupId, position) {
const graph = this.graph;
const groupType = graph.get('groupType');
// 更新群组里面节点和线的位置
this.updateItemInGroup(groupId, position);
// 判断是否拖动出了parent group外面,如果拖出了parent Group外面,则更新数据,去掉group关联
// 获取groupId的父Group的ID
const { groups } = graph.save();
let parentGroupId = null;
let parentGroupData = null;
for (const group of groups) {
if (groupId !== group.id) {
continue;
}
parentGroupId = group.parentId;
parentGroupData = group;
break;
}
if (parentGroupId) {
const { nodeGroup: parentGroup } = this.getDeletageGroupById(parentGroupId);
// const parentGroup = customGroup[parentGroupId].nodeGroup;
const parentKeyShape = parentGroup.get('keyShape');
this.setGroupStyle(parentKeyShape, 'default');
const parentGroupBBox = parentKeyShape.getBBox();
const { minX, minY, maxX, maxY } = parentGroupBBox;
// 检查是否拖出了父Group
const { nodeGroup: currentGroup } = this.getDeletageGroupById(groupId);
// const currentGroup = customGroup[groupId].nodeGroup;
const currentKeyShape = currentGroup.get('keyShape');
const currentKeyShapeBBox = currentKeyShape.getBBox();
const { x, y } = currentKeyShapeBBox;
if (!(x < maxX && x > minX && y < maxY && y > minY)) {
// 拖出了parent group,则取消parent group ID
delete parentGroupData.parentId;
// 同时删除groupID中的节点
const groupNodes = graph.get('groupNodes');
const currentGroupNodes = groupNodes[groupId];
const parentGroupNodes = groupNodes[parentGroupId];
groupNodes[parentGroupId] = parentGroupNodes.filter(node => currentGroupNodes.indexOf(node) === -1);
const { x: x1, y: y1, width, height } = this.calculationGroupPosition(groupNodes[parentGroupId]);
const paddingValue = this.getGroupPadding(parentGroupId);
const groupTitleShape = parentGroup.findByClassName('group-title');
let titleX = 0;
let titleY = 0;
if (groupType === 'circle') {
const r = width > height ? width / 2 : height / 2;
const cx = (width + 2 * x1) / 2;
const cy = (height + 2 * y1) / 2;
parentKeyShape.attr({
r: r + paddingValue,
x: cx,
y: cy
});
titleX = cx;
titleY = cy - parentKeyShape.attr('r');
} else if (groupType === 'rect') {
const { default: defaultStyle } = this.styles;
const rectPadding = paddingValue * defaultStyle.disCoefficient;
parentKeyShape.attr({
x: x1 - rectPadding,
y: y1 - rectPadding
});
titleX = x1 - rectPadding + 15;
titleY = y1 - rectPadding + 15;
}
if (groupTitleShape) {
const titleConfig = parentGroupData.title;
let offsetX = 0;
let offsetY = 0;
if (titleConfig) {
offsetX = titleConfig.offsetX;
offsetY = titleConfig.offsetY;
}
groupTitleShape.attr({
x: titleX + offsetX,
y: titleY + offsetY
});
}
}
}
}
/**
* 更新节点分组中节点和边的位置
* @param {string} groupId 节点分组ID
* @param {object} position delegate的坐标位置
*/
updateItemInGroup(groupId, position) {
const graph = this.graph;
const groupType = graph.get('groupType');
const groupNodes = graph.get('groupNodes');
// step 1:先修改groupId中的节点位置
const nodeInGroup = groupNodes[groupId];
const { nodeGroup } = this.getDeletageGroupById(groupId);
const originBBox = nodeGroup.getBBox();
const otherGroupId = [];
nodeInGroup.forEach((nodeId, index) => {
const node = graph.findById(nodeId);
const model = node.getModel();
const nodeGroupId = model.groupId;
if (nodeGroupId && !otherGroupId.includes(nodeGroupId)) {
otherGroupId.push(nodeGroupId);
}
if (!this.nodePoint[index]) {
this.nodePoint[index] = {
x: model.x,
y: model.y
};
}
// 群组拖动后节点的位置:deletateShape的最终位置-群组起始位置+节点位置
const x = position.x - originBBox.x + this.nodePoint[index].x;
const y = position.y - originBBox.y + this.nodePoint[index].y;
this.nodePoint[index] = {
x, y
};
graph.updateItem(node, { x, y });
});
// step 2:修改父group中其他节点的位置
// otherGroupId中是否包括当前groupId,如果不包括,则添加进去
if (!otherGroupId.includes(groupId)) {
otherGroupId.push(groupId);
}
// 更新完群组位置后,重新设置群组起始位置
otherGroupId.forEach(id => {
// 更新群组位置
const { nodeGroup } = this.getDeletageGroupById(id);
const groupKeyShape = nodeGroup.get('keyShape');
const noCustomNodes = groupNodes[id].filter(node => node.indexOf('custom-node') === -1);
const { x, y, width, height } = this.calculationGroupPosition(noCustomNodes);
let titleX = 0;
let titleY = 0;
if (groupType === 'circle') {
const cx = (width + 2 * x) / 2;
const cy = (height + 2 * y) / 2;
groupKeyShape.attr({
x: cx,
y: cy
});
titleX = cx;
titleY = cy - groupKeyShape.attr('r');
} else if (groupType === 'rect') {
// 节点分组状态
const hasHidden = nodeGroup.get('hasHidden');
const paddingValue = this.getGroupPadding(id);
let keyshapePosition = {};
const { default: defaultStyle } = this.styles;
const rectPadding = paddingValue * defaultStyle.disCoefficient;
titleX = x - rectPadding + 15;
titleY = y - rectPadding + 15;
if (hasHidden) {
// 无标题,或节点分组是展开的情况
keyshapePosition = {
x: x - rectPadding,
y: y - rectPadding
};
titleY = titleY + 10;
} else {
keyshapePosition = {
x: x - rectPadding,
y: y - rectPadding,
width: width + rectPadding * 2,
height: height + rectPadding * 2
};
}
groupKeyShape.attr(keyshapePosition);
}
// 如果存在标题,则更新标题位置
this.updateGroupTitle(nodeGroup, id, titleX, titleY);
});
}
/**
* 更新节点分组的 Title
* @param {Group} group 当前 Group 实例
* @param {string} groupId 分组ID
* @param {number} x x坐标
* @param {number} y y坐标
*/
updateGroupTitle(group, groupId, x, y) {
const graph = this.graph;
const groupTitleShape = group.findByClassName('group-title');
if (groupTitleShape) {
let titleConfig = null;
const groupData = graph.get('groups').filter(data => data.id === groupId);
if (groupData && groupData.length > 0) {
titleConfig = groupData[0].title;
}
let offsetX = 0;
let offsetY = 0;
if (titleConfig) {
offsetX = titleConfig.offsetX || 0;
offsetY = titleConfig.offsetY || 0;
}
groupTitleShape.attr({
x: x + offsetX,
y: y + offsetY
});
}
}
/**
* 拖动节点时候动态改变节点分组大小
* @param {Event} evt 事件句柄
* @param {Group} currentGroup 当前操作的群组
* @param {Item} keyShape 当前操作的keyShape
* @description 节点拖入拖出后动态改变群组大小
*/
dynamicChangeGroupSize(evt, currentGroup, keyShape) {
const { item } = evt;
const model = item.getModel();
// 节点所在的GroupId
const { groupId } = model;
const graph = this.graph;
const groupType = graph.get('groupType');
const groupNodes = graph.get('groupNodes');
const nodes = groupNodes[groupId];
// 拖出节点后,根据最新的节点数量,重新计算群组大小
// 如果只有一个节点,拖出后,则删除该组
if (nodes.length === 0) {
// step 1: 从groupNodes中删除
delete groupNodes[groupId];
// step 2: 从groups数据中删除
const groupsData = graph.get('groups');
graph.set('groups', groupsData.filter(gdata => gdata.id !== groupId));
// step 3: 删除原来的群组
currentGroup.remove();
} else {
const { x, y, width, height } = this.calculationGroupPosition(nodes);
// 检测操作的群组中是否包括子群组
const paddingValue = this.getGroupPadding(groupId);
let titleX = 0;
let titleY = 0;
if (groupType === 'circle') {
const r = width > height ? width / 2 : height / 2;
const cx = (width + 2 * x) / 2;
const cy = (height + 2 * y) / 2;
keyShape.attr({
r: r + paddingValue,
x: cx,
y: cy
});
titleX = cx;
titleY = cy - keyShape.attr('r');
} else if (groupType === 'rect') {
const { default: defaultStyle } = this.styles;
const rectPadding = paddingValue * defaultStyle.disCoefficient;
keyShape.attr({
x: x - rectPadding,
y: y - rectPadding,
width: width + rectPadding * 2,
height: height + rectPadding * 2
});
titleX = x - rectPadding + 15;
titleY = y - rectPadding + 15;
}
// 如果存在标题,则更新标题位置
this.updateGroupTitle(currentGroup, groupId, titleX, titleY);
}
this.setGroupStyle(keyShape, 'default');
}
resetNodePoint() {
this.nodePoint.length = 0;
}
destroy() {
this.graph = null;
this.styles = {};
this.customGroup = {};
this.delegateInGroup = {};
this.resetNodePoint();
}
}
module.exports = CustomGroup;