綜述:使用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>