HTML5 Canvas 完成K线图的示例编码

日期:2021-02-22 类型:科技新闻 

关键词:商城网站建设,微商好助手,微商引流,电商网站模板,微信商家小程序

由于企业的新项目要求,必须做1个K线图,可让买卖者清晰的看到某1买卖种类在各个時间段内的报价,和当今的即时报价。

我所考虑到的有两个方位,1是相近于Highcharts等软件的完成方法 -- svg,1是HTML5的canvas。

SVG 是1种应用 XML 叙述 2D 图型的語言。 Canvas 根据 JavaScript 来绘图 2D 图型。 Canvas 是逐像素开展3D渲染的。

 '

历经上面的较为不难发现, SVG 更可用于偏静态数据,3D渲染频率不高的情景,因此这类要完成即时报价升级绘图的状况只能挑选 canvas

2. 完成哪些要求

历史时间报价即时报价 绘图图表

适用 拖拽 查询历史时间時间段的报价图表

适用电脑鼠标 滚轮 和触碰板 双指 实际操作变大或变小图表

适用电脑鼠标指针 挪动 查询电脑鼠标部位报价

3. 编码完成全过程

1. 提前准备工作中

/**
 * K-line - K线图3D渲染涵数
 * Date: 2019.12.18  Author: 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; //顶部室内空间
const RIGHT_SPACE = 60; //右边室内空间
let _addEventListener, _removeEventListener, prefix = ''; //addEventListener 访问器适配
function RenderKLine (id, /*Optional*/options) {
  if (!id) return;
  options = options || {};
  this.id = id;   //canvas box id
  // detect event model
  if (window.addEventListener) {
    _addEventListener = "addEventListener";
    _removeEventListener = "removeEventListener";
  } else {
    _addEventListener = "attachEvent";
    _removeEventListener = "detachEvent"
    prefix = "on";
  }
  // options params
  this.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; //电脑鼠标挪动10字线显示信息情况

  //basic params
  this.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; //历史时间报价中第1个時间戳, 用来和即时报价做较为画图
  this.buyColorRGB = {r: 0, g: 0, b: 0};
  this.sellColorRGB = {r: 0, g: 0, b: 0};
  
  this.processParams();
  this.init();
}

界定了1些变量定义和自变量,转化成1个 结构涵数 ,接受两个主要参数,1个是id,canvas会在插进到这个id的盒子内,第2个主要参数是1些配备项,可选。

/**
 *    sharpness {number} 清楚度
 *    buyColor {string} color - 涨
 *    sellColor {string} color - 跌
 *    fontColor {string} 文本色调
 *    lineColor {string} 参照线色调
 *    blockWidth {number} 方块的宽度
 *    digitsPoint {number} 报价有几位小数
 *    horizontalCells {number} 水平方位激光切割几个格子
 *    crossLineStatus {boolean} 电脑鼠标挪动10字线显示信息情况
 */

2. init方式和canvas画布的旋转

RenderKLine.prototype.init = function () {
  let cBox = 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 = '您确当前访问器不适用HTML5 canvas';
  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();
  // base settings
  this.ctx.lineWidth = LINE_WIDTH*this.sharpness;
  this.ctx.font = `${12*this.sharpness}px Arial`;
  // 复原以前翻转的间距
  this.ctx.translate(-this.movingRange * this.sharpness, 0);
  // console.log(this.movingRange);
}

init方式原始化了1个canvas,enlargeCanvas是1个取代clearRect的方式,在其中必须留意的是 transformOrigin 这个方式,由于一切正常的canvas原点座标在座上角,可是大家必须绘图的图象是从右边刚开始绘图的,因此我这里以便便捷制图,把全部canvas做了1次变换,原点座标转到了右上角部位。

// 切换座标系走向 (原点在左上角 or 右上角)
RenderKLine.prototype.transformOrigin = function () {
  this.ctx.translate(this.canvas.width, 0);
  this.ctx.scale(⑴, 1);
}

这里有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;
  function mosueMoveEvent (e) {
    if (!_this.dataArr.length) return;
    _this.event = e || event;
    _this.enlargeCanvas();
    _this.updateData();
  }
}

//拖拽恶性事件
RenderKLine.prototype.addMouseDrag = function () {
  let pageX, 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;
  function dragMouseMoveEvent (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;
  function wheelEvent (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(⑴); //向内
        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 方式,绘图图象便可。会启用下面方式画出10字线。

function drawCrossLine () {
  if (!this.crossLineStatus || !this.event) return;
  let cRect = this.canvas.getBoundingClientRect();
  //layerX 有适配性难题,应用clientX
  let x = this.canvas.width - (this.event.clientX - cRect.left - this.movingRange) * this.sharpness;
  let y = (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的变换复原回家,避免文本旋转形变
  let str = (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(⑶*this.sharpness, y⑴0*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  
 *      ⑶ >> this.blockWidth - 3*2
 * 以便确保放缩的实际效果,
 * 应当以当今可视性地区的管理中心为标准放缩
 * 因此放缩前后左右两侧的长度在总长度中所占有率例应当1样
 * 公式:(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;
  let oldTotalLen = this.totalWidth;
  this.blockWidth += scaleTimes*2;
  this.processParams();
  this.computeTotalWidth();
  let newRange = (this.movingRange*this.sharpness*this.totalWidth+this.canvas.width/2*this.totalWidth-this.canvas.width/2*oldTotalLen)/oldTotalLen/this.sharpness;
  let diffRange = 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);
  let maxMovingRange =  (this.totalWidth - this.canvas.width) / this.sharpness + this.blockWidth;
  if (this.totalWidth <= this.canvas.width || this.movingRange <= 0) {
    this.movingRange = 0;
  } else if (this.movingRange >= maxMovingRange) {
    this.movingRange = maxMovingRange;
  }
  this.enlargeCanvas();
  this.updateData();
}

4. 关键方式 updateData

全部的绘图全过程全是在这个方式中进行的,这样不管要想甚么实际操作,都可以以根据此方式重绘canvas来完成,必须做的只是更改原形上的1些特性罢了,例如要想上下挪动,只必须把 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);
  // 刚开始绘图烛炬图
  let item, col;
  let lineWidth = LINE_WIDTH * this.sharpness,
      margin = blockMargin = BLOCK_MARGIN*this.sharpness,
      blockWidth = this.blockWidth*this.sharpness;//乘上清楚度系数后的间隔、块宽度
  let blockHeight, lineHeight, blockYPoint, lineYPoint; //单1方块、单1正中间线的高宽比、y座标点
  let realTime, realTimeYPoint; //即时(最终)报价及y座标点
  for (let i=0; i<this.dataArr.length; i++) {
    item = this.dataArr[i];
    if (item[START_PRICE_INDEX] > item[END_PRICE_INDEX]) {
      //跌了 sell
      col = this.sellColor;
      blockHeight = (item[START_PRICE_INDEX] - item[END_PRICE_INDEX])*this.perPricePixel;
      blockYPoint = (this.maxPrice - item[START_PRICE_INDEX])*this.perPricePixel;
    } else {
      //涨了 buy
      col = 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⑴*this.sharpness)/2, 0, margin+(blockWidth⑴*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⑴*this.sharpness)/2), this.centerSpace + 12*this.sharpness, undefined, 'center', 'top');
      
      this.ctx.restore();
    }
    this.drawRect(margin+(blockWidth⑴*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 (!data instanceof Array || !data.length) return;
  this.dataArr = data;
  this.dataType = type;
  this.updateData(true);
}

6. 启用demo

<div id="myCanvasBox" style="width: 1000px; height: 500px;"></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,
      horizontalCells: 10
    };
    let kLine = new RenderKLine("myCanvasBox", options);
    //升级历史时间报价
    kLine.updateHistoryQuote(data);
    //仿真模拟即时报价
    let realTime = `{
      "time": 1575858840, 
      "open_price": "1476.96", 
      "high": "1482.12", 
      "low": "1470.96", 
      "close": "1476.96"
    }`;
    setInterval(() => {
      let realTimeCopy = JSON.parse(realTime);
      realTimeCopy.time = parseInt(new Date().getTime()/1000);
      realTimeCopy.close = (1476.96 - (Math.random() * 4 - 2)).toFixed(2);
      kLine.updateRealTimeQuote(realTimeCopy);
     }, parseInt(Math.random() * 1000 + 500))
</script>

7. 实际效果图

 

4. 总结

这个作用都还没做完,也有许多别的作用和1些细节上必须开发设计,例如贝塞尔曲线图的绘图,初次载入的Loading,更多历史时间报价载入这些。如今只是简易总结1下这次遇到的难题,和1些获得,等下1环节健全后再做详尽纪录。

这是我第1次应用canvas绘图1个详细的新项目,全部全过程還是很有获得的,我想之后还要尝试别的不一样的物品,例如手机游戏。

  • canvas特性十分高,实际上现动漫的全过程,便是不断的重绘。
  • 要学会变换座标系,这对绘图图象很有协助。
  • 要用好ctx.save 和 ctx.restore
  • 数学课很关键...

以上便是本文的所有內容,期待对大伙儿的学习培训有一定的协助,也期待大伙儿多多适用脚本制作之家。

上一篇:html2canvas截图空白难题的处理 返回下一篇:没有了