前端k线图,

  前端k线图,

  因为公司的项目要求,需要制作k线图,让交易者清楚的看到某个交易品种在各个时间段的报价以及当前的实时报价。

  我在考虑两个方向,一个是SVG,类似于Highcharts等插件的实现,一个是HTML5的canvas。

  SVG是一种使用XML描述2D图形的语言。Canvas通过JavaScript绘制2D图形。画布是逐像素渲染的。

  经过以上对比,不难发现SVG更适合渲染频率较低的静态场景。所以为了实现实时报价更新和绘图,只能选择canvas

  2. 实现哪些需求

  历史报价实时报价来绘制图表

  支持拖拽查看历史时间段的报价图。

  支持鼠标滚轮和触摸板双指放大或缩小图表。

  支持鼠标指针移动查看鼠标位置报价。

  3. 代码实现过程

  1.准备工作

  /** * K线-K线图渲染函数*日期:2019年12月18日作者:isnan */const block _ margin=2;//方块的水平间距const start _ price _ index= open _ price ;//起始价格在数据组中的位置const END _ PRICE _ INDEX= close//数据组中结束价格的位置const MIN _ PRICE _ INDEX= low//数据组中最低价的位置const MAX _ PRICE _ INDEX= high//数据组中最高价的位置const TIME _ INDEX= time//时间在数据组中的位置const LINE _ WIDTH=1;//1px宽度(中线,x轴等。)const BOTTOM _ SPACE=40//底部空格const TOP _ SPACE=20//head SPACE const RIGHT _ SPACE=60;//右空格let _ addeventlistener,_ removeeventlistener,前缀=“”;//addEventListener浏览器兼容函数render Kline (id,/* optional */options) {if(!id)返回;options=options { };this.id=id//画布框id //检测事件模型if(window . addevent listener){ _ addevent listener= addevent listener ;_ removeEventListener= removeEventListener ;} else { _ addevent listener= attach event ;_ remove event listener= detach event 前缀= on}//options params this . sharpness=options . sharpness;//清晰度(正整数太大可能会卡,具体看电脑配置,建议在2 ~ 5的范围内)。this . block width=options . block width;//正方形的宽度(最小为3,最大为49。为防止中线位置偏差,设置为奇数;如果是偶数,则减1)this . buy color=options . buy color # f 05452 ;//color up this . sell color=options . sell color # 25c 875 ;//color falls this . font color=options . font color # 66666 ;//Text color this . line color=options . line color # dddddd ;//参考线颜色。digitspoint=选项。digits point 2;//引用的数字(有几个小数位)this。horizontalsells=options。horizontal sells 5;//横截多少格(中间虚线数=5-1)这个。交叉状态=选项。CrosslineStatus true//鼠标移动十字丝显示状态//basic params this . total width=0;//总宽度this . moving range=0;//横向移动的距离取正值,使用时加负号this.minPrice=9999999this . max price=0;//所有绘制的数据中最小/最大的数据用来绘制Y轴this . diff price=0;//最高报价与最低报价之差this . perprice pixel=0;//每个单位报价占多少像素?this . center space=0;//X轴到绘图区顶部的距离this . xdatespace=6;//X轴上的时间绘制间隔是多少组this . fromspacenum=0;//X轴上的时间标绘从(fromSpaceNum%xDateSpace)组数据this.dataArr=[]开始;//data this . lastdata timestamp=undefined;//历史行情中的第一个时间戳,用来和实时行情对比得出这个。BuyColorGB={R: 0,G: 0,B:0 };this.sellColorRGB={r: 0,g: 0,b:0 };this . process params();this . init();}定义一些常量和变量,生成一个构造函数,接收两个参数,一个是id,canvas会在框中插入这个id,第二个参数是一些配置项,可选。

  /** *锐度{number}清晰度*购买颜色{string}颜色-涨* sellColor {string}颜色-跌* fontColor {string}文字颜色* lineColor {string}参考线颜色*区块宽度{number}方块的宽度* digitsPoint {number}报价有几位小数*水平单元格{number}水平方向切割几个格子*交叉状态{boolean}鼠标移动十字线显示状态*/2.初始化方法和帆布画布的翻转

  渲染克莱恩。原型。init=function(){ let cBox=document。getelementbyid(这个。id);//创建帆布并获得帆布上下文这个。画布=文档。createelement(“canvas”);如果(这个。帆布这个。画布。获取上下文){ this。CTX=这个。画布。获取上下文(“2d”);} this.canvas.innerHTML=您的当前浏览器不支持html 5 canvas ;cbox。appendchild(这个。画布);这个。实际宽度=cbox。客户端宽度;这个。实际高度=cbox。客户身高;这个。放大画布();}//因为绘制区域超出帆布区域,此方法也用来代替以及清除清空画布的作用渲染克莱恩。原型。放大画布=function(){ this。画布。宽度=这个。实际宽度*这个。锐度;这个。画布。身高=这个。实际高度*这个。锐度;这个。画布。风格。身高=这个。画布。身高/这个。锐度 px ;这个。画布。风格。宽度=这个。画布。宽度/这个。锐度 px ;这个。中心空间=这个。画布。高度-(BOTTOM _ SPACE TOP _ SPACE)* this。锐度;//将帆布原点坐标转换到右上角这个。转换原点();//基本设置。CTX。线宽=LINE _ WIDTH * this。锐度;这个。CTX。font=` $ { 12 * this。锐度} px arial `;//还原之前滚动的距离这个。CTX。翻译(-这个。移动范围*这个。锐度,0);//控制台。日志(这个。移动范围);}初始化方法初始化了一个画布,放大画布是一个替代以及清除的方法,其中需要注意的是transformOrigin这个方法,因为正常的帆布原点坐标在坐上角,但是我们需要绘制的图像是从右侧开始绘制的,所以我这里为了方便绘图,把整个帆布做了一次转换,原点坐标转到了右上角位置。

  //切换坐标系走向(原点在左上角或者右上角)渲染Kline。原型。转换原点=函数(){ this。CTX。翻译(这个。画布。宽度,0);this.ctx.scale(-1,1);}这里有一点需要注意的是,虽然翻转过来绘制一些矩形,直线没什么问题,但是绘制文本是不行的,绘制文本需要还原回去,不然文字就是翻转过来的状态。如下图所示:

  3.移动、拖拽、滚轮事件

  //监听鼠标移动渲染克莱恩。原型。add MouseMove=function(){ this。canvas[_ addevent监听器](前缀mousemove ,mosueMoveEvent);this.canvas[_addEventListener](前缀mouseleave ,e={ this.event=undefinedthis。放大画布();这个。update data();});const _ this=这个函数mosueMoveEvent (e) { if(!_this.dataArr.length)返回;_ this.event=e event_ this。放大画布();_这个。update data();}}//拖拽事件渲染克莱恩。原型。addmousedrag=function(){ let pageX,moveX=0;this.canvas[_addEventListener](前缀mousedown ,e={ e=e event pagex=e . pagex this . canvas[_ addevent listener](前缀mousemove ,dragMouseMoveEvent);});this.canvas[_addEventListener](前缀mouseup ,e={ this。canvas[_ removeevent监听器](前缀mousemove ,dragMouseMoveEvent);});this.canvas[_addEventListener](前缀mouseleave ,e={ this。canvas[_ removeevent监听器](前缀mousemove ,dragMouseMoveEvent);});const _ this=这个函数dragMouseMoveEvent (e) { if(!_this.dataArr.length)返回;e=e 事件;moveX=e . pageX-pageX;pageX=e.pageX_ this。翻译Kline(moveX);//控制台。log(moveX);}}//Mac双指行为鼠标滚轮渲染克莱恩。原型。addmousewheel=function(){ addwheelistener(this。画布、轮子事件);const _ this=this function wheel event(e){ if(math。ABS(e . deltax)!==0 Math.abs(e.deltaY)!==0)返回;//没有固定方向,忽略if(e . deltax 0)return _ this。翻译Kline(parse int(-e . deltax));//向右if(e . deltax 0)return _ this。翻译Kline(parse int(-e . deltax));//向左if(e . ctrl键){ if(e . deltay 0)return _ this。刻度线(-1);//向内if(e . deltay 0)return _ this。刻度线(1);//向外} else { if(e . deltay 0)return _ this。刻度线(1);//向上if(e . deltay 0)return _ this。刻度线(-1);//向下} }}滚轮事件上一篇已经说过了,这里就是对不同情况做相应的处理;

  鼠标移动事件把事件更新到this上,然后调用updateData方法,绘制图像即可。会调用下面方法画出十字线。

  函数drawCrossLine () { if(!this.crossLineStatus !这个事件)返回;让cRect=this。画布。getboundingclientrect();//layerX有兼容性问题,使用clientX让x=this。画布。宽度-(这个。事件。clientX-crect。左-这个。移动范围)*这个。锐度;设y=(这个。事件。客户-crect。顶)*这个。锐度;//在报价范围内画线if(y TOP _ SPACE * this。锐度 y这个。画布。height-BOTTOM _ SPACE *这个。锐度)返回;这个。画破折号(这个。移动范围*这个。锐度,y,这个。画布。加宽这个。移动范围*这个。锐度,y, # 999999 );this.drawDash(x,TOP_SPACE*this.sharpness,x,this。画布。height-BOTTOM _ SPACE *这个。锐度, # 999999 );//报价这个。CTX。save();这个。CTX。翻译(这个。移动范围*这个。锐度,0);//填充文字时需要把帆布的转换还原回来,防止文字翻转变形设str=(这个。最高价-(y-TOP _ SPACE * this。锐度)/这个。每像素).来固定(这个。数字点);这个。转换原点();这个。CTX。翻译(这个。画布。width-RIGHT _ SPACE *这个。锐度,0);这个。画rect(-3 * this。sharpness,y-10*this.sharpness,this.ctx.measureText(str).宽度6*this.sharpness,20*this.sharpness, # CCC );this.drawText(str,0,y,RIGHT _ SPACE * this。锐度)这个。CTX。restore();}拖拽事件pageX的移动距离传递给translateKLine方法来实现横向滚动查看。

  /** * 缩放图表* @param {int}比例时间缩放倍数* 正数为放大,负数为缩小,数值*2 代表蜡烛图宽度的变化度2这个。块宽2 * 2 *-3这个。区块宽度-3 * 2 *为了保证缩放的效果, * 应该以当前可视区域的中心为基准缩放* 所以缩放前后两边的长度在总长度中所占比例应该一样* 公式:(旧范围0.5 *画布宽度)/旧totallen=(新范围0.5 *画布宽度)/newTotalLen * diff range=新范围-旧范围*=(旧范围* newTotalLen 0.5 *画布宽度* newTotalLen-0.5 *画布宽度* oldTotalLen)/oldTotalLen-旧范围*/render Kline。原型。scale Kline=function(scale times){ if(!this.dataArr.length)返回;设oldTotalLen=this . totalwidththis。块宽=比例乘以* 2;这个。流程参数();这个。computetotalwidth();假设new range=(this。移动范围*这个。锐度*这个。此总宽度。画布。宽度/2 *这个。总宽度-这个。画布。width/2 * oldTotalLen)/oldTotalLen/this。锐度;让diff range=new range-this。移动范围;//console.log(newRange,this.movingRange,diff range);这个。平移Kline(不同范围);}//移动图表渲染克莱恩。原型。translate Kline=function(range){ if(!this.dataArr.length)返回;这个。移动范围=parse int(range);设maxMovingRange=(this。总宽度-这个。画布。宽度)/这个。锐度这个。区块宽度;如果(这个。总宽度=这个。画布。宽度 这个。移动范围=0){ this。移动范围=0;} else if(这个。移动范围=最大移动范围){ this。移动范围=最大移动范围;}这个。放大画布();这个。update data();}4.核心方法更新数据

  所有的绘制过程都是在这个方法中完成的,这样无论想要什么操作,都可以通过此方法重绘帆布来实现,需要做的只是改变原型上的一些属性而已,比如想要左右移动,只需要把this.movingRange设置好,再调用updateData就完成了。

  渲染克莱恩。原型。update data=function(isUpdateHistory){ if(!this.dataArr.length)返回;if(isUpdateHistory){ this。from space num=0;} //console.log(数据);这个。computetotalwidth();这个。computespacey();这个。CTX。save();//把原点坐标向下方移动顶部空间的距离,开始绘制水平线this.ctx.translate(0,TOP _ SPACE * this。锐度);这个。drawhorizontalline();//把原点坐标再向左边移动右空格的距离,开始绘制垂直线和蜡烛图这个。CTX。翻译(RIGHT _ SPACE * this。锐度,0);//开始绘制蜡烛图让项目,栏设线宽=LINE _ WIDTH * this。锐度,边距=块边距=BLOCK _ MARGIN * this。锐度,块宽=这个。块宽*这个。锐度;//乘上清晰度系数后的间距、块宽度让blockHeight,lineHeight,blockYPoint,line point//单一方块、单一中间线的高度、y坐标点让realTime,real time point//实时(最后)报价及y坐标点对于(设I=0;这是。数据排列。长度;I){ item=this。数据arr[I];if(item[START _ PRICE _ INDEX]item[END _ PRICE _ INDEX]){//跌了sell col=this。卖色;块高=(item[START _ PRICE _ INDEX]-item[END _ PRICE _ INDEX])* this。perprice像素;挡点=(这个。最高价格-item[START _ PRICE _ INDEX])* this。perprice像素;} else { //涨了买col=这个。买色;块高=(item[END _ PRICE _ INDEX]-item[START _ PRICE _ INDEX])* this。perprice像素;挡点=(这个。最高价格-item[END _ PRICE _ INDEX])* this。perprice像素;}行高=(item[MAX _ PRICE _ INDEX]-item[MIN _ PRICE _ INDEX])* this。perprice像素;liney point=(这个。最高价格-item[MAX _ PRICE _ INDEX])* this。perprice像素;//if(I===0)控制台。log(行高、块高、线点、块点);行高=行高2 *这个。锐度?行高:2 *这个。锐度;块高=块高2 *这个。锐度?积木高度:2 *这个。锐度;if(I===0){实时=item[END _ PRICE _ INDEX];实时ypoint=block ypoint(item[START _ PRICE _ INDEX]item[END _ PRICE _ INDEX]?块高度:0)};//绘制垂直方向的参考线、以及x轴的日期时间如果(我%这个。xdatespace====(这个。来自spacenum % this。xdatespace)){ this。绘制虚线(边距(块宽-1 *此。sharpness)/2,0,margin(块宽-1 * this。锐度)/2,这个。中心空间);这个。CTX。save();//填充文字时需要把帆布的转换还原回来,防止文字翻转变形这个。转换原点();//翻转后将原点移回翻转前的位置这个。CTX。翻译(这个。画布。宽度,0);这个。drawtext(process xdate(item[TIME _ INDEX],this.dataType),-(margin(block width-1 * this。锐度)/2),这个。中间空间12 *这个。锐度,未定义,居中,顶部);这个。CTX。restore();}这个。绘制rect(边距(块宽-1 * this。sharpness)/2,lineYPoint,lineWidth,lineHeight,col);this.drawRect(margin,blockYPoint,blockWidth,blockHeight,col);margin=margin块宽度块边距;} //绘制实时报价线、价格这个。drawline((这个。移动范围-RIGHT _ SPACE)*这个。锐度,realTimeYPoint,(这个。移动范围-RIGHT _ SPACE)*这个。锐度这个。画布。width,realTimeYPoint, # cccccc );这个。CTX。save();这个。CTX。翻译(-RIGHT _ SPACE * this。锐度,0);这个。转换原点();这个。绘制rect((17-this。移动范围)*这个。锐度,实时ypoint-10 * this。锐度,this.ctx.measureText(实时).宽度6*this.sharpness,20*this.sharpness, # CCC );this.drawText(实时,(20-this。移动范围)*这个。锐度,实时ypoint);这个。CTX。restore();//最后绘制y轴上报价,放在最上层这个。CTX。翻译(-RIGHT _ SPACE * this。锐度,0);这个。drawyprice();这个。CTX。restore();画交叉线。叫(这个);}这个方法不难,只是绘制时为了方便计算位置,需要经常变换原点坐标,不要搞错了就好。

  还要注意的是,变量sharpness代表的是定义,整个画布的宽度和高度是在原来的基础上加上这个系数得到的。所以计算时要特别注意带上这个系数。

  5.更新历史实时报价方法

  //实时报价render Kline . prototype . updaterealtimequote=function(quote){ if(!报价)退货;pushQuoteInData.call(this,quote);}/* * *历史报价* @ param { Array } data * @ param { int } type报价类型默认为60(1小时)* (1,5,15,30,60,240,1440,10080,3200) (1分钟,5分钟,15分钟,30分钟,1小时,4小时,日,周,月)*/render Kline . prototype . updatehistoryquote=function(data,type=60) {if(!数组的数据实例!data.length)返回;this.dataArr=datathis.dataType=typethis . updatedata(true);}6.通话演示

  div id= myCanvasBox style= width:1000 px;高度:500像素;/div script let data=[{ time :1576648800, open_price: 1476.94 , high: 1477.44 , low: 1476.76 , close: 1476.96 },//.];let options={ sharpness: 3,blockWidth: 11,horizontal cells:10 };let kLine=new render kLine( myCanvasBox ,options);//更新历史报价kLine.updateHistoryQuote(数据);//模拟实时行情let real time=` { time :1575858840, open _ price: 1476.96 , high: 1482.12 , low: 1470.96 , close : 1476.96 setInterval(()={ let real time copy=JSON . parse(real time);realTimeCopy.time=parseInt(新日期()。getTime()/1000);real time copy . close=(1476.96-(math . random()* 4-2))。toFixed(2);Kline . updaterealtimequote(real time copy);},Parseint(数学。random () * 1000 500))/script7。翻译

  4. 总结

  这个功能还没有完成,还有很多其他的功能和一些细节需要开发,比如贝塞尔曲线的绘制,第一次加载,加载更多的历史行情等等。现在只是简单总结一下这次遇到的问题,以及一些收获,下一阶段改进后再做详细记录。

  这是我第一次用画布画完整的项目,整个过程还是很有收获的。以后想尝试其他不同的东西,比如游戏。

  Canvas的性能非常高,它的动画过程是不断重绘的。学会变换坐标系,对画图很有帮助。用好ctx.save和ctx.restore数学很重要.这就是本文的全部内容。希望对大家的学习和支持有帮助。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: