综述:使用canvas实现了一个多分支流向图,总结下主要的实现思路,有需要的朋友可直接使用,假设如果不使用canvas来绘制,使用svg来设计又该如何实现呢,思考下?
1.效果展示
2.实现思路解析
- 需求分析
一个流程图,主要由横线,空心圆,空心圆中的实心圆三部分组成,并且随着状态的变化能控制其颜色
可以支持多分支,多分支中还包括奇数个分支和偶数个分支,实现思路上两者稍微不同
- 核心思路说明
通过配置的数组解析绘制,如果是非数组元素,则就是画直线和圆圈,实心圆,并分别计算各自的位置(x,y)坐标,依次类推。
如果是一个数组的元素,则说明是一个多分支的元素,需要向上和向下开分支,绘制多条直线和圆圈,实心圆,依次类推即可,并分别计算各自的位置(x,y)坐标,依次类推。
3.遇到的问题总结
- canvas是基于矢量绘制的图像,存在模糊的问题(参考连接),细节见js代码种的最后一个模块
- 对于绘制直线的颜色控制逻辑,没有理清楚,废了些时间
4.代码展示
- javascript代码(组件实现代码),render方法过于冗余,但是好处是比较好理解,后期应该优化下
/**
*流向图组件,mouyao
*/
var opsDirectionMap = function(option){
this.const(option);
this.init();
};
/*
*配置项引入
*/
opsDirectionMap.prototype.const=function(option){
this.r=option.r||4;//节点半径
this.config=option;
this.data = option.data||[];
this.mLeft = option.mLeft||-20;//起点距左边距离
this.space = option.space||18*this.r;//节点之间距离
this.angle =2*this.r;//分支上下之间的高度
this.nodeArr = []; //存储所有的圆点的信息和坐标
};
/*
*配置项引入
*/
opsDirectionMap.prototype.init =function(){
var myCanvas=document.getElementById(this.config.placeId);
this.resolveVagueProblem(myCanvas);
this.render(myCanvas);
};
/*
*判定是否数组
*/
opsDirectionMap.prototype.isArrayFn =function(o) {
return Object.prototype.toString.call(o) === '[object Array]';
};
/*
*根据当前节点的执行状态,渲染圆点前的线条的颜色
*/
opsDirectionMap.prototype.drawDashLine =function(ctx, x1, y1, x2, y2,data,index){
ctx.lineWidth=1;
ctx.beginPath();
var x=(x2-x1)/2;
if(index>0&&!this.isArrayFn(this.data[index-1])){
ctx.moveTo(x1,y1);
ctx.lineTo(x1+x ,y1);
ctx.moveTo(x1+x,y1);
ctx.lineTo(x1+x ,y2);
ctx.moveTo(x1+x,y2);
ctx.lineTo(x2 ,y2);
}else if(index>0&&this.isArrayFn(this.data[index-1])){
ctx.moveTo(x1,y1);
ctx.lineTo(x1+x ,y1);
ctx.moveTo(x1+x,y1);
ctx.lineTo(x1+x ,y2);
ctx.moveTo(x1+x,y2);
ctx.lineTo(x2 ,y2);
}else{
if(index!==0){ //删除第一个圆点的连接线
ctx.moveTo(x1,y1);
ctx.lineTo(x2 ,y2);
}
}
if(data.isExcuted===true){
ctx.strokeStyle="#009aff";
}else if(data.isExcuted===false&&index!==0&&this.data[index-1].isExcuted===true&&!this.isArrayFn(this.data[index-1])){
ctx.strokeStyle="#009aff";
}else if(data.isExcuted===false&&this.isArrayFn(this.data[index-1])){
//如果上一个元素是数组
var arr=[];
this.data[index-1].some(function(item){
if(item.isExcuted===true){
arr.push(true);
}
});
if(arr.length===(this.data[index-1]).length){
ctx.strokeStyle="#009aff";
}else{
ctx.strokeStyle="#959595";
}
}else{
ctx.strokeStyle="#959595";
}
ctx.stroke();
};
/*
*绘制线条,圆点,圆心,和说明文字
*/
opsDirectionMap.prototype.render =function(canvas){
var this_ = this;
this_.canvas = canvas;
var ctx = canvas.getContext("2d");//上下文
this_.ctx = ctx;
var x = this_.mLeft; //每个操作的对象的坐标
//var y = canvas.height/2;
//x偏移量:this_.r*Math.sin((90-itemY)*Math.PI/180)
//y偏移量:this_.r*Math.cos((90-itemY)*Math.PI/180)
var y =50;
this_.data.forEach(function(item, index){
if(!(item instanceof Array)){
x = index == 0?x:(x + this_.space);
if((index-1)>=0 && this_.data[index-1] instanceof Array){
var arr = this_.data[index-1];
if(arr.length % 2==0){
var itemY = this_.angle;
for(var i=0;i<arr.length/2;i++){
this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y-Math.tan(itemY*Math.PI/180)*this_.space+this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
itemY = itemY + this_.angle;
}
var itemY = this_.angle;
for(var i=0;i<arr.length/2;i++){
this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y+Math.tan(itemY*Math.PI/180)*this_.space-this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
itemY = itemY + this_.angle;
}
}else{
var itemY = 0;
for(var i=0;i<parseInt(arr.length/2)+1;i++){
console.log(arr[i]);
this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y-Math.tan(itemY*Math.PI/180)*this_.space+this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
itemY = itemY + this_.angle;
}
var itemY = this_.angle;
for(var i=0;i<parseInt(arr.length/2);i++){
this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y+Math.tan(itemY*Math.PI/180)*this_.space-this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
itemY = itemY + this_.angle;
}
}
}
if(index == 0){
ctx.moveTo(x,y);
x = x + this_.space;
}
//绘制非数组直线
if(!((index-1)>=0 && this_.data[index-1] instanceof Array)){
this_.drawDashLine(ctx,x-this_.space, y, x, y,item,index);
}
ctx.moveTo(x + 2*this_.r,y);
//绘制节点,画圆
ctx.arc(x + this_.r,y,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x + this_.r,y:y,data:item});
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
//节点标题note
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle =item.noteColor;//字体颜色
//节点的名称设置
ctx.fillText(item.noteName,x + this_.r,y-this_.r-10);
ctx.fillStyle = "black";//字体颜色
x = x + 2*this_.r;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x,y);
}else{//数组
if(!(this_.data[index-1] instanceof Array)){//上一级不是数组
var itemY = 0;
if(item.length%2==0){//偶数
itemY = this_.angle;
var dataArr = item.slice(0,item.length/2).reverse();
for(var i=0;i<dataArr.length;i++){
this_.drawDashLine(ctx, x, y, x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x + this_.space, y-Math.tan(itemY*Math.PI/180)*(this_.space),this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x + this_.space,y:y-Math.tan(itemY*Math.PI/180)*(this_.space),data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle =dataArr[i].isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x + this_.space, y-Math.tan(itemY*Math.PI/180)*(this_.space)-this_.r-10);
ctx.fill();
ctx.moveTo(x+this_.r,y);
itemY = itemY + this_.angle;
}
itemY = this_.angle;
var dataArr = item.slice(item.length/2,item.length);
for(var i=0;i<dataArr.length;i++){
this_.drawDashLine(ctx, x, y, x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x + this_.space, y+Math.tan(itemY*Math.PI/180)*(this_.space),this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x + this_.space,y:y+Math.tan(itemY*Math.PI/180)*(this_.space),data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle =dataArr[i].isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x + this_.space, y+Math.tan(itemY*Math.PI/180)*(this_.space)+this_.r+10);
ctx.fill();
itemY = itemY + this_.angle;
}
}else{//奇数
var dataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
for(var i=0;i<dataArr.length;i++){
this_.drawDashLine(ctx, x, y, x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x + this_.space,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
ctx.fill();
itemY = itemY + this_.angle;
}
itemY = this_.angle;
var dataArr = item.slice(parseInt(item.length/2)+1,item.length);
for(var i=0;i<dataArr.length;i++){
this_.drawDashLine(ctx, x, y, x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x + this_.space,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
ctx.fill();
itemY = itemY + this_.angle;
}
}
ctx.stroke();
ctx.beginPath();
x = x+this_.space+this_.r;
ctx.moveTo(x,y);
}else{//上一级是数组
if(item.length%2==0){//偶数
var itemY = this_.angle;
var dataArr = item.slice(0,item.length/2).reverse();
for(var i=0;i<dataArr.length;i++){
ctx.moveTo(x,y-Math.tan(itemY*Math.PI/180)*this_.space);
this_.drawDashLine(ctx, x, y-Math.tan(itemY*Math.PI/180)*this_.space,
x+this_.r+ this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x+ this_.space+this_.r,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
ctx.fill();
itemY = itemY + this_.angle;
}
var itemY = this_.angle;
var dataArr = item.slice(item.length/2,item.length);
for(var i=0;i<dataArr.length;i++){
ctx.moveTo(x,y+Math.tan(itemY*Math.PI/180)*this_.space);
this_.drawDashLine(ctx, x, y+Math.tan(itemY*Math.PI/180)*this_.space,
x+this_.r+ this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x+ this_.space+this_.r,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
ctx.fill();
itemY = itemY + this_.angle;
}
}else{//奇数
var itemY = 0;
var dataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
for(var i=0;i<dataArr.length;i++){
ctx.moveTo(x,y-Math.tan(itemY*Math.PI/180)*this_.space);
this_.drawDashLine(ctx, x, y-Math.tan(itemY*Math.PI/180)*this_.space,
x+this_.r+ this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x+ this_.space+this_.r,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
ctx.fill();
itemY = itemY + this_.angle;
}
var itemY = this_.angle;
var dataArr = item.slice(parseInt(item.length/2)+1,item.length);
for(var i=0;i<dataArr.length;i++){
ctx.moveTo(x,y+Math.tan(itemY*Math.PI/180)*this_.space);
this_.drawDashLine(ctx, x, y+Math.tan(itemY*Math.PI/180)*this_.space,
x+this_.r+ this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
ctx.beginPath();
ctx.arc(x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
this_.nodeArr.push({x:x+ this_.space+this_.r,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
//节点信息
ctx.textAlign ="center";
ctx.textBaseline = "middle";
ctx.font = "bold 10px 宋体";//字体大小
ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充颜色
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = dataArr[i].noteColor;//字体颜色
ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
ctx.fill();
itemY = itemY + this_.angle;
}
}
ctx.stroke();
ctx.beginPath();
x = x+this_.space+2*this_.r;
ctx.moveTo(x,y);
}
}
});
};
/*
*因为canvas绘制的是矢量图,会出现模糊问题,使用下边的方法解决
* 参考链接:https://zhuanlan.zhihu.com/p/31426945
*/
opsDirectionMap.prototype.resolveVagueProblem=function(myCanvas) {
var getPixelRatio = function (context) {
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStore;
};
//画文字
myCanvas.style.border = "1px solid silver";
var context = myCanvas.getContext("2d");
var ratio = getPixelRatio(context);
myCanvas.style.width = myCanvas.width + 'px';
myCanvas.style.height =myCanvas.height+ 'px';
myCanvas.width = myCanvas.width * ratio;
myCanvas.height = myCanvas.height * ratio;
context.scale(ratio,ratio);
};
- html调用代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
</head>
<body>
<canvas id="renderArea" width="600" height="100">浏览器不支持canvas</canvas>
<script type="text/javascript" src="ops-direction-map.js"></script>
<script>
var demo=new opsDirectionMap({
placeId:"renderArea",
excutedCirclePointColor:"#009aff",//执行的节点的圆心颜色
circlePointColor:"#ffffff",//未执行的的节点的圆心颜色
data:[{
noteName:'节点1',
noteColor:'#000000', //说明文字的颜色
isExcuted:true//如果这里为true,则其前边的线条为蓝色,圆点为实心,否为白色
},{
noteName:'节点2',
noteColor:'#000000',
isExcuted:true
},[
{
noteName:'节点3-1',
noteColor:'#000000',
isExcuted:true
},
{
noteName:'节点3-2',
noteColor:'#000000',
isExcuted:false
}
],{
noteName:'节点4',
noteColor:'#000000',
isExcuted:false
},{
noteName:'节点5',
noteColor:'#000000',
isExcuted:false
},[
{
noteName:'节点6-1',
noteColor:'#000000',
isExcuted:false
},{
noteName:'节点6-2',
noteColor:'#000000',
isExcuted:false,
}
],{
noteName:'节点7',
noteColor:'#000000',
isExcuted:false
}
]
});
</script>
</body>
</html>