前言
因业务关系,新增绘制流程图需求,最开始想使用第三方库,调研后总有部分功能不能满足,考虑后期需求变更的情况,决定自己研发。经多番调研,确认使用svg和d3绘制流程图,并封装为npm包。本文主要讲解封装库的前期规划、部分实现方式以及里面涉及到的一些好的idea。这是封装好库react-data-flow,并在不断更新优化中,目前的功能按照公司的需求定制的,后期新增自定义功能。
前期规划
首先确认需求,目前需要支持的需求有:
- 背景画布,canvas;
- 绘制节点;
- 节点间拖拽绘制边;
- 节点和节点间的连线规则;
- 节点选中效果;
- 节点选中后边的动画效果;
- 边上的显示文本及相关事件;
- 图谱居中、缩放功能;
暂时梳理的需求有上面8项,分析需求,除了节点拖拽绘制边及图谱的居中和缩放功能,使用svg比较容易就能实现。需要考虑的是节点的事件、边的事件以及图谱上的拖拽已经缩放事件。d3已经有了成熟的方法能实现,引入d3,包的体积比较大,很多功能不能使用,考虑上面的需求,只需引入d3-zoom和d3-selection就可满足。
到此可以确认使用的技术:
React、Canvas、SVG、d3-selection、d3-zoom
绘制画布
分解需求:
- 横线和纵线(点线),线与间隔[3, 3];
- 线与线间的间距为20;
- 绘制多条横线和纵线即可;
需求已很明确,绘制多条横线和纵线组合起来,背景就完成了。下面的绘制背景的方法是使用Canvas绘制,不清楚Canvas绘制线条语法,点我。
绘制线条封装:
// canvas的ref
const girdRef = useRef<HTMLCanvasElement>(null);
const canvasWidth = 1920 * 2;
const canvasHeight = 1080 * 2;
const grid = {
strokeColor: '#E2E2F0',
strokeWidth: 1,
distance: 20,
isLineDash: true,
lineDash: [3, 3],
}
const drawGridLine = (x1: number, y1: number, x2: number, y2: number) => {
if (girdRef && girdRef.current) {
const gridCanvas: any = girdRef.current.getContext('2d');
const { strokeWidth, strokeColor, lineDash } = grid;
gridCanvas.beginPath();
gridCanvas.moveTo(x1, y1);
gridCanvas.lineTo(x2, y2);
gridCanvas.setLineDash(lineDash);
gridCanvas.lineWidth = strokeWidth;
gridCanvas.strokeStyle = strokeColor;
gridCanvas.stroke();
}
}
线条绘制方法已经绘制好,只需要传入起点和终点左边即可;
接下来的工作,需要绘制多条线条。方法如下:
const drawGrid = () => {
const distance = grid.distance;
const rowNumber = Math.ceil(canvasHeight / distance);
const colNumber = Math.ceil(canvasWidth / distance);
for (let i = 0; i < rowNumber; i++) {
drawGridLine(0, i * distance, canvasWidth, i * distance);
}
for (let j = 0; j < colNumber; j++) {
drawGridLine(j * distance, 0, j * distance, canvasHeight);
}
}
获取当前容器的宽高,除以间距,得到条数。到此,背景已绘制完成。
svg绘制节点
不清楚svg语法的点我,节点是一个矩形,需要故需要绘制矩形,svg绘制矩形根据左上角点的坐标和宽高绘制的,所以只需确定坐标即可。
代码如下:
const position = {
x: 300, y: 300
}
const rect = {
strokeWidth: 1,
strokeColor: '#2994FF',
fill: '#FAFBFC',
width: 180,
height: 50,
distance: 10,
radius: 4,
hover: {
fill: '#E9F3FC',
},
delRadius: 10,
}
<g>
<rect
className="rectNode"
x={position.x - width / 2}
y={position.y - height / 2}
rx={radius}
ry={radius}
width={width}
height={height}
stroke={strokeColor}
fill={rectFill}
/>
<text
className="rectTextNode"
id={`text_id_${node.id}`}
x={position.x}
y={position.y + rectText.marginTop}
fill={rectText.fill}
style={{ textAnchor: 'middle', fontSize: rectText.fontSize, userSelect: 'none' }}>
{title}
</text>
</g>
只需要知道api,绘制图形挺简单,多数的操作都是细节的调整。
svg绘制边
语法:
命令 | 参数 | 说明 |
---|---|---|
M m | x y | 移动画笔到制定坐标 |
L l | x y | 绘制一条到给定坐标的线 |
H h | x | 绘制一条到给定x坐标的横线 |
V v | y | 绘制一条到给定y坐标的垂线 |
A a | rx ry x-axis-rotation large-arc sweep x y | 圆弧曲线命令有7个参数,依次表示x方向半径、y方向半径、旋转角度、大圆标识、顺逆时针标识、目标点x、目标点y。大圆标识和顺逆时针以0和1表示。0表示小圆、逆时针 |
Q q | x1 y1 x y | 绘制一条从当前点到x,y控制点为x1,y1的二次贝塞尔曲线 |
T t | x y | 绘制一条从当前点到x,y的光滑二次贝塞尔曲线,控制点为前一个Q命令的控制点的中心对称点,如果没有前一条则已当前点为控制点。 |
C c | x1 y1 x2 y2 x y | 绘制一条从当前点到x,y控制点为x1,y1 x2,y2的三次贝塞尔曲线 |
S s | x2 y2 x y | 绘制一条从当前点到x,y的光滑三次贝塞尔曲线。第一个控制点为前一个C命令的第二个控制点的中心对称点,如果没有前一条曲线,则第一个控制点为当前的点。 |
代码:
<path
d={` M 275, 231
L 315, 231
L 341.5, 231
L 341.5, 231
L 368, 231
L 408, 231`}
strokeWidth="1"
stroke="#ccc"
fill="none"
markerEnd="url(#arrow)"
/>
多节点和多边结合及相关事件的思路
上面简单介绍了绘制点和绘制线条的方式,点与边的结合确定坐标即可。
对于每个节点的事件而言,有两种方法,一种是通过d3-selection获取元素demo,从而操作demo,如下:
d3.select(svgElement).on('click', function(){})
第二种方法,React的方式,在元素上绑定onClick事件。如下:
<g onClick={handleClick}>
<rect />
<text />
</g>
对于拖拽事件而言,同样事两种方法,一种事d3提供的方法,推荐这种,如下:
d3.select(svgElement).call(d3.drag().on('drag', function() {}))
另外一种方式,在react元素上绑定拖拽事件,需要借助第三方库,如react-dnd
、react-draggable
等库。
总结
本文只是简单介绍了如何通过canvas、svg、d3绘制简单的图形,如果详细讲解上面封装的库,内容太多,不知从和开始介绍。若有疑问或建议欢迎评论区留言。