前言
前段时间公司有个需求要开发一个数据关系的界面,类似UML建模工具里面表之间关系的图形界面,目前用的前端框架是React、rxjs,图形界面这块定下来采用的是D3的最新版本V7,所以现在需要基于React框架下开发这个界面,前期查了一些相关资料,国内基于React、D3 V7版本结合开发的比较少,差不多都是V3、V4版本,V4版本国内还有中文翻译V4之后就停了,所以结合个人在当前的需求背景下以及使用过程中的碰到的一些问题记录下来,一方面供有需要人的可以借鉴下,一方面也是给自己做个总结。
用的D3版本v7.0.0
,需要开发的功能:
1.拖拽、缩放功能
2.连线并带有箭头,线条有文字
3.能添加节点、删除结点
4.添加节点需计算位置,尽量保证不重叠
5.节点与节点之间需要通信更新数据
6.节点不同层级展示的背景颜色不一致
7.节点可折叠、展开
代码结构
import * as d3 from 'd3';
import * as React from 'react';
import ReactDOM from 'react-dom';
import './index.scss';
// 节点高
const nodeHalfHeight = 300 / 2;
// 节点宽度
const nodeWidth = 240;
// 折叠之后的高度
const foldHeight = 85 / 2;
// 未选择表数据标识
const NO_DATA = 'NO_DATA';
// 获取随机ID
const getRandomId = () => Math.random().toString(32).slice(2);
// 记录当前操作折叠的nodeId
let nodeIds: Array<any> = [];
const D3DataModel = (props: any): React.ReactElement => {
const refs = React.useRef(null);
// 表数据
const [d3NodeData, setD3NodeData] = React.useState(() => {
// nodeId 用来构建连线以及生成表格区域的ID
// level 用来根据层级绘画表格背景色
// data_type 用来区分是否渲染无数据背景图片
return [{ x: 10, y: 10, data_type: NO_DATA, nodeId: getRandomId(), level: 1 }];
});
// d3缩放范围
const [transformInfo, setTransformInfo] = React.useState<any>(null);
React.useEffect(() => {
drawModel();
}, [d3NodeData.length]);
const getD3Data = (): any => {
...3.Demo数据
};
/**
* 计算线条文字位置
*
* @param {*} data
* @return {*}
*/
const calcuLabelPoint = (data: any): number => {
...12.计算文字坐标
};
/**
* 获取缩放对象
*
* @param {*} g
* @return {*}
*/
const d3ZoomObj = (g: any): any => {
...5.缩放
};
/**
* 获取拖拽对象
*
* @param {*} simulation 力模型
* @return {*} {object}
*/
const d3DragObj = (simulation: any): any => {
...6.拖拽
};
/**
* 构建表格
*
* @param {*} g
* @param {*} data
* @param {*} drag
* @return {*}
*/
const buildTable = (g: any, data: any, drag: any): any => {
...7.构建表格节点
};
/**
* 构建线条
*
* @param {*} g
* @param {*} data
* @return {*} {*}
*/
const buildLine = (g: any, data: any): any => {
...8.构建线条
};
/**
* 构建线条文字
*
* @param {*} g
* @param {*} data
* @return {*} {*}
*/
const buildLineLabel = (g: any, data: any): any => {
...9.构建线条文字
};
/**
* 构建箭头
*
* @param {*} g
* @return {*} {*}
*/
const buildArrow = (g: any): any => {
...10.构建箭头
};
/**
* 绘画
*
*/
const drawModel = () => {
...2.绘制函数
};
/**
* 渲染数据表
*
* @param {*} props
*/
const renderDataTable = (props: any) => {
...13.渲染React组件到图形中
};
return (
<section className={'d3-dataModel-area'}>
<div className={'popup-element'} />
<div className={'d3-element'} ref={refs} />
</section>
);
};
export default D3DataModel;
代码拆解
1.DOM节点
这个DOM节点用于挂载ant组件Tooltip
、Select
生成的DOM,因为我们当前这种方式节点内部元素DataTableComp
中有使用到ant组件,导致D3
重绘时ant
生成的一些DOM
节点没有清除,统一挂载到这个区域统一清除。
<div className={'popup-element'} />
D3
绘制的图形节点全部在这个div
中。
<div className={'d3-element'} ref={refs} />
<section className={'d3-dataModel-area'}>
{/* ant组件弹框元素挂载节点 */}
<div className={'popup-element'} />
{/* d3绘制节点 */}
<div className={'d3-element'} ref={refs} />
</section>
2.绘制函数
这个函数主要是整合其他函数,统一入口。
React.useEffect(() => {
drawModel();
}, [d3NodeData.length]);
/**
* 绘画
*
*/
const drawModel = () => {
const { edges } = getD3Data();
// 先移除svg
d3.selectAll('svg').remove();
// 构建svg
const svg = d3.select(refs.current).append('svg');
// 构建容器g
const g = svg.append('g').attr('transform', transformInfo);
// 构建力模型,防止模型重叠
const simulation = d3.forceSimulation(d3NodeData).force('collide', d3.forceCollide().radius(100));
// 缩放
const zoom = d3ZoomObj(g);
// 获取拖拽对象
const drag = d3DragObj(simulation);
// 构建表格区节点
const d3DataTable = buildTable(g, d3NodeData, drag);
// 构建线条
const line = buildLine(g, edges);
// 连线名称
const lineLabel = buildLineLabel(g, edges);
// 绘制箭头
const arrows = buildArrow(g);
simulation.on('tick', () => {
// 更新节点位置
d3DataTable.attr('transform', (d) => {
return d && 'translate(' + d.x + ',' + d.y + ')';
});
// 更新连线位置
line.attr('d', (d: any) => {
// 节点的x+节点宽度
const M1 = d.source.x + nodeWidth;
// 节点的y+节点的一半高度
let pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
// 起点折叠
if (nodeIds.includes(d.source.nodeId)) {
pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
}
//