在前端开发中如何通过Canvas实现K线图
今天小编给大家分享一下在前端开发中如何通过Canvas实现K线图的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
1.对于实现的话,我所考虑的有两个方向,一是类似于Highcharts等插件的实现方式 -- svg,一是HTML5的canvas。
SVG 是一种使用 XML 描述 2D 图形的语言。 Canvas 通过 JavaScript 来绘制 2D 图形。 Canvas 是逐像素进行渲染的。
经过上面的比较不难发现, SVG 更适用于偏静态,渲染频率不高的场景,所以这种要实现实时报价更新绘制的情况只能选择 canvas。
2. 实现哪些需求
历史报价 和 实时报价 绘制图表
支持 拖拽 查看历史时间段的报价图表
支持鼠标 滚轮 和触摸板 双指 操作放大或缩小图表
支持鼠标指针 移动 查看鼠标位置报价
3. 代码实现过程
1. 准备工作
/***K-line-K线图渲染函数*Date:2019.12.18Author:isnan*/constBLOCK_MARGIN=2;//方块水平间距constSTART_PRICE_INDEX='open_price';//开始价格在数据组中的位置constEND_PRICE_INDEX='close';//结束价格在数据组中的位置constMIN_PRICE_INDEX='low';//最小价格在数据组中的位置constMAX_PRICE_INDEX='high';//最大价格在数据组中的位置constTIME_INDEX='time';//时间在数据组中的位置constLINE_WIDTH=1;//1px宽度(中间线、x轴等)constBOTTOM_SPACE=40;//底部空间constTOP_SPACE=20;//顶部空间constRIGHT_SPACE=60;//右侧空间let_addEventListener,_removeEventListener,prefix='';//addEventListener浏览器兼容functionRenderKLine(id,/*Optional*/options){if(!id)return;options=options||{};this.id=id;//canvasboxid//detecteventmodelif(window.addEventListener){_addEventListener="addEventListener";_removeEventListener="removeEventListener";}else{_addEventListener="attachEvent";_removeEventListener="detachEvent"prefix="on";}//optionsparamsthis.sharpness=options.sharpness;//清晰度(正整数太大可能会卡顿,取决于电脑配置建议在2~5区间)this.blockWidth=options.blockWidth;//方块的宽度(最小为3,最大49为了防止中间线出现位置偏差设定为奇数,若为偶数则向下减1)this.buyColor=options.buyColor||'#F05452';//color涨this.sellColor=options.sellColor||'#25C875';//color跌this.fontColor=options.fontColor||'#666666';//文字颜色this.lineColor=options.lineColor||'#DDDDDD';//参考线颜色this.digitsPoint=options.digitsPoint||2;//报价的digits(有几位小数)this.horizontalCells=options.horizontalCells||5;//水平方向切割多少格子(中间虚线数=5-1)this.crossLineStatus=options.crossLineStatus||true;//鼠标移动十字线显示状态//basicparamsthis.totalWidth=0;//总宽度this.movingRange=0;//横向移动的距离取正数值,使用时再加负号this.minPrice=9999999;this.maxPrice=0;//绘制的所有数据中最小/最大数据用来绘制y轴this.diffPrice=0;//最大报价与最小报价的差值this.perPricePixel=0;//每一个单位报价占用多少像素this.centerSpace=0;//x轴到顶部的距离绘图区域this.xDateSpace=6;//x轴上的时间绘制间隔多少组this.fromSpaceNum=0;//x轴上的时间绘制从第(fromSpaceNum%xDateSpace)组数据开始this.dataArr=[];//数据this.lastDataTimestamp=undefined;//历史报价中第一个时间戳,用来和实时报价做比较画图this.buyColorRGB={r:0,g:0,b:0};this.sellColorRGB={r:0,g:0,b:0};this.processParams();this.init();}
定义了一些常量和变量,生成一个 构造函数 ,接收两个参数,一个是id,canvas会在插入到这个id的盒子内,第二个参数是一些配置项,可选。
/***sharpness{number}清晰度*buyColor{string}color-涨*sellColor{string}color-跌*fontColor{string}文字颜色*lineColor{string}参考线颜色*blockWidth{number}方块的宽度*digitsPoint{number}报价有几位小数*horizontalCells{number}水平方向切割几个格子*crossLineStatus{boolean}鼠标移动十字线显示状态*/
2. init方法和canvas画布的翻转
RenderKLine.prototype.init=function(){letcBox=document.getElementById(this.id);//创建canvas并获得canvas上下文this.canvas=document.createElement("canvas");if(this.canvas&&this.canvas.getContext){this.ctx=this.canvas.getContext("2d");}this.canvas.innerHTML='您的当前浏览器不支持HTML5canvas';cBox.appendChild(this.canvas);this.actualWidth=cBox.clientWidth;this.actualHeight=cBox.clientHeight;this.enlargeCanvas();}//因为绘制区域超出canvas区域,此方法也用来代替clearRect清空画布的作用RenderKLine.prototype.enlargeCanvas=function(){this.canvas.width=this.actualWidth*this.sharpness;this.canvas.height=this.actualHeight*this.sharpness;this.canvas.style.height=this.canvas.height/this.sharpness+'px';this.canvas.style.width=this.canvas.width/this.sharpness+'px';this.centerSpace=this.canvas.height-(BOTTOM_SPACE+TOP_SPACE)*this.sharpness;//将canvas原点坐标转换到右上角this.transformOrigin();//basesettingsthis.ctx.lineWidth=LINE_WIDTH*this.sharpness;this.ctx.font=`${12*this.sharpness}pxArial`;//还原之前滚动的距离this.ctx.translate(-this.movingRange*this.sharpness,0);//console.log(this.movingRange);}
init方法初始化了一个canvas,enlargeCanvas是一个替代clearRect的方法,其中需要注意的是 transformOrigin 这个方法,因为正常的canvas原点坐标在坐上角,但是我们需要绘制的图像是从右侧开始绘制的,所以我这里为了方便绘图,把整个canvas做了一次转换,原点坐标转到了右上角位置。
//切换坐标系走向(原点在左上角or右上角)RenderKLine.prototype.transformOrigin=function(){this.ctx.translate(this.canvas.width,0);this.ctx.scale(-1,1);}
这里有一点需要注意的是,虽然翻转过来绘制一些矩形,直线没什么问题,但是绘制文本是不行的,绘制文本需要还原回去,不然文字就是翻转过来的状态。如下图所示:
3. 移动、拖拽、滚轮事件
//监听鼠标移动RenderKLine.prototype.addMouseMove=function(){this.canvas[_addEventListener](prefix+"mousemove",mosueMoveEvent);this.canvas[_addEventListener](prefix+"mouseleave",e=>{this.event=undefined;this.enlargeCanvas();this.updateData();});const_this=this;functionmosueMoveEvent(e){if(!_this.dataArr.length)return;_this.event=e||event;_this.enlargeCanvas();_this.updateData();}}//拖拽事件RenderKLine.prototype.addMouseDrag=function(){letpageX,moveX=0;this.canvas[_addEventListener](prefix+'mousedown',e=>{e=e||event;pageX=e.pageX;this.canvas[_addEventListener](prefix+'mousemove',dragMouseMoveEvent);});this.canvas[_addEventListener](prefix+'mouseup',e=>{this.canvas[_removeEventListener](prefix+'mousemove',dragMouseMoveEvent);});this.canvas[_addEventListener](prefix+'mouseleave',e=>{this.canvas[_removeEventListener](prefix+'mousemove',dragMouseMoveEvent);});const_this=this;functiondragMouseMoveEvent(e){if(!_this.dataArr.length)return;e=e||event;moveX=e.pageX-pageX;pageX=e.pageX;_this.translateKLine(moveX);//console.log(moveX);}}//Mac双指行为&鼠标滚轮RenderKLine.prototype.addMouseWheel=function(){addWheelListener(this.canvas,wheelEvent);const_this=this;functionwheelEvent(e){if(Math.abs(e.deltaX)!==0&&Math.abs(e.deltaY)!==0)return;//没有固定方向,忽略if(e.deltaX<0)return_this.translateKLine(parseInt(-e.deltaX));//向右if(e.deltaX>0)return_this.translateKLine(parseInt(-e.deltaX));//向左if(e.ctrlKey){if(e.deltaY>0)return_this.scaleKLine(-1);//向内if(e.deltaY<0)return_this.scaleKLine(1);//向外}else{if(e.deltaY>0)return_this.scaleKLine(1);//向上if(e.deltaY<0)return_this.scaleKLine(-1);//向下}}}
滚轮事件 上一篇已经说过了,这里就是对不同情况做相应的处理;
鼠标移动事件 把event更新到 this 上,然后调用 updateData 方法,绘制图像即可。会调用下面方法画出十字线。
functiondrawCrossLine(){if(!this.crossLineStatus||!this.event)return;letcRect=this.canvas.getBoundingClientRect();//layerX有兼容性问题,使用clientXletx=this.canvas.width-(this.event.clientX-cRect.left-this.movingRange)*this.sharpness;lety=(this.event.clientY-cRect.top)*this.sharpness;//在报价范围内画线if(y<TOP_SPACE*this.sharpness||y>this.canvas.height-BOTTOM_SPACE*this.sharpness)return;this.drawDash(this.movingRange*this.sharpness,y,this.canvas.width+this.movingRange*this.sharpness,y,'#999999');this.drawDash(x,TOP_SPACE*this.sharpness,x,this.canvas.height-BOTTOM_SPACE*this.sharpness,'#999999');//报价this.ctx.save();this.ctx.translate(this.movingRange*this.sharpness,0);//填充文字时需要把canvas的转换还原回来,防止文字翻转变形letstr=(this.maxPrice-(y-TOP_SPACE*this.sharpness)/this.perPricePixel).toFixed(this.digitsPoint);this.transformOrigin();this.ctx.translate(this.canvas.width-RIGHT_SPACE*this.sharpness,0);this.drawRect(-3*this.sharpness,y-10*this.sharpness,this.ctx.measureText(str).width+6*this.sharpness,20*this.sharpness,"#ccc");this.drawText(str,0,y,RIGHT_SPACE*this.sharpness)this.ctx.restore();}
拖拽事件 把 pageX 的移动距离传递给 translateKLine 方法来实现横向滚动查看。
/***缩放图表*@param{int}scaleTimes缩放倍数*正数为放大,负数为缩小,数值*2代表蜡烛图width的变化度*eg:2>>this.blockWidth+2*2*-3>>this.blockWidth-3*2*为了保证缩放的效果,*应该以当前可视区域的中心为基准缩放*所以缩放前后两边的长度在总长度中所占比例应该一样*公式:(oldRange+0.5*canvasWidth)/oldTotalLen=(newRange+0.5*canvasWidth)/newTotalLen*diffRange=newRange-oldRange*=(oldRange*newTotalLen+0.5*canvasWidth*newTotalLen-0.5*canvasWidth*oldTotalLen)/oldTotalLen-oldRange*/RenderKLine.prototype.scaleKLine=function(scaleTimes){if(!this.dataArr.length)return;letoldTotalLen=this.totalWidth;this.blockWidth+=scaleTimes*2;this.processParams();this.computeTotalWidth();letnewRange=(this.movingRange*this.sharpness*this.totalWidth+this.canvas.width/2*this.totalWidth-this.canvas.width/2*oldTotalLen)/oldTotalLen/this.sharpness;letdiffRange=newRange-this.movingRange;//console.log(newRange,this.movingRange,diffRange);this.translateKLine(diffRange);}//移动图表RenderKLine.prototype.translateKLine=function(range){if(!this.dataArr.length)return;this.movingRange+=parseInt(range);letmaxMovingRange=(this.totalWidth-this.canvas.width)/this.sharpness+this.blockWidth;if(this.totalWidth<=this.canvas.width||this.movingRange<=0){this.movingRange=0;}elseif(this.movingRange>=maxMovingRange){this.movingRange=maxMovingRange;}this.enlargeCanvas();this.updateData();}
4. 核心方法 updateData
所有的绘制过程都是在这个方法中完成的,这样无论想要什么操作,都可以通过此方法重绘canvas来实现,需要做的只是改变原型上的一些属性而已,比如想要左右移动,只需要把 this.movingRange 设置好,再调用 updateData 就完成了。
RenderKLine.prototype.updateData=function(isUpdateHistory){if(!this.dataArr.length)return;if(isUpdateHistory){this.fromSpaceNum=0;}//console.log(data);this.computeTotalWidth();this.computeSpaceY();this.ctx.save();//把原点坐标向下方移动TOP_SPACE的距离,开始绘制水平线this.ctx.translate(0,TOP_SPACE*this.sharpness);this.drawHorizontalLine();//把原点坐标再向左边移动RIGHT_SPACE的距离,开始绘制垂直线和蜡烛图this.ctx.translate(RIGHT_SPACE*this.sharpness,0);//开始绘制蜡烛图letitem,col;letlineWidth=LINE_WIDTH*this.sharpness,margin=blockMargin=BLOCK_MARGIN*this.sharpness,blockWidth=this.blockWidth*this.sharpness;//乘上清晰度系数后的间距、块宽度letblockHeight,lineHeight,blockYPoint,lineYPoint;//单一方块、单一中间线的高度、y坐标点letrealTime,realTimeYPoint;//实时(最后)报价及y坐标点for(leti=0;i<this.dataArr.length;i++){item=this.dataArr[i];if(item[START_PRICE_INDEX]>item[END_PRICE_INDEX]){//跌了sellcol=this.sellColor;blockHeight=(item[START_PRICE_INDEX]-item[END_PRICE_INDEX])*this.perPricePixel;blockYPoint=(this.maxPrice-item[START_PRICE_INDEX])*this.perPricePixel;}else{//涨了buycol=this.buyColor;blockHeight=(item[END_PRICE_INDEX]-item[START_PRICE_INDEX])*this.perPricePixel;blockYPoint=(this.maxPrice-item[END_PRICE_INDEX])*this.perPricePixel;}lineHeight=(item[MAX_PRICE_INDEX]-item[MIN_PRICE_INDEX])*this.perPricePixel;lineYPoint=(this.maxPrice-item[MAX_PRICE_INDEX])*this.perPricePixel;//if(i===0)console.log(lineHeight,blockHeight,lineYPoint,blockYPoint);lineHeight=lineHeight>2*this.sharpness?lineHeight:2*this.sharpness;blockHeight=blockHeight>2*this.sharpness?blockHeight:2*this.sharpness;if(i===0){realTime=item[END_PRICE_INDEX];realTimeYPoint=blockYPoint+(item[START_PRICE_INDEX]>item[END_PRICE_INDEX]?blockHeight:0)};//绘制垂直方向的参考线、以及x轴的日期时间if(i%this.xDateSpace===(this.fromSpaceNum%this.xDateSpace)){this.drawDash(margin+(blockWidth-1*this.sharpness)/2,0,margin+(blockWidth-1*this.sharpness)/2,this.centerSpace);this.ctx.save();//填充文字时需要把canvas的转换还原回来,防止文字翻转变形this.transformOrigin();//翻转后将原点移回翻转前的位置this.ctx.translate(this.canvas.width,0);this.drawText(processXDate(item[TIME_INDEX],this.dataType),-(margin+(blockWidth-1*this.sharpness)/2),this.centerSpace+12*this.sharpness,undefined,'center','top');this.ctx.restore();}this.drawRect(margin+(blockWidth-1*this.sharpness)/2,lineYPoint,lineWidth,lineHeight,col);this.drawRect(margin,blockYPoint,blockWidth,blockHeight,col);margin=margin+blockWidth+blockMargin;}//绘制实时报价线、价格this.drawLine((this.movingRange-RIGHT_SPACE)*this.sharpness,realTimeYPoint,(this.movingRange-RIGHT_SPACE)*this.sharpness+this.canvas.width,realTimeYPoint,'#cccccc');this.ctx.save();this.ctx.translate(-RIGHT_SPACE*this.sharpness,0);this.transformOrigin();this.drawRect((17-this.movingRange)*this.sharpness,realTimeYPoint-10*this.sharpness,this.ctx.measureText(realTime).width+6*this.sharpness,20*this.sharpness,"#ccc");this.drawText(realTime,(20-this.movingRange)*this.sharpness,realTimeYPoint);this.ctx.restore();//最后绘制y轴上报价,放在最上层this.ctx.translate(-RIGHT_SPACE*this.sharpness,0);this.drawYPrice();this.ctx.restore();drawCrossLine.call(this);}
这个方法不难,只是绘制时为了方便计算位置,需要经常变换原点坐标,不要搞错了就好。
还需要注意的是 sharpness 这个变量,代表清晰度,整个canvas的宽高是在原有的基础上乘上了这个系数得到的,所以,计算时需要特别注意带上这个系数。
5. 更新历史&实时报价方法
//实时报价RenderKLine.prototype.updateRealTimeQuote=function(quote){if(!quote)return;pushQuoteInData.call(this,quote);}/***历史报价*@param{Array}data数据*@param{int}type报价类型默认60(1小时)*(1,5,15,30,60,240,1440,10080,43200)(1分钟5分钟15分钟30分钟1小时4小时日周月)*/RenderKLine.prototype.updateHistoryQuote=function(data,type=60){if(!datainstanceofArray||!data.length)return;this.dataArr=data;this.dataType=type;this.updateData(true);}
6. 调用demo
<divid="myCanvasBox"></div><script>letdata=[{"time":1576648800,"open_price":"1476.94","high":"1477.44","low":"1476.76","close":"1476.96"},//...];letoptions={sharpness:3,blockWidth:11,horizontalCells:10};letkLine=newRenderKLine("myCanvasBox",options);//更新历史报价kLine.updateHistoryQuote(data);//模拟实时报价letrealTime=`{"time":1575858840,"open_price":"1476.96","high":"1482.12","low":"1470.96","close":"1476.96"}`;setInterval(()=>{letrealTimeCopy=JSON.parse(realTime);realTimeCopy.time=parseInt(newDate().getTime()/1000);realTimeCopy.close=(1476.96-(Math.random()*4-2)).toFixed(2);kLine.updateRealTimeQuote(realTimeCopy);},parseInt(Math.random()*1000+500))</script>
以上就是“在前端开发中如何通过Canvas实现K线图”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。