H5用canvas画股票分时图

最近公司要开发一个关于模拟股票走势的项目,本想着用echarts开发,但考虑到echarts有些不能自定义设置,灵活性不是很方便,所以决定用canvas来开发。

如下图所示:

所要考虑的问题:canvas宽高动态设置,移动端适配,股票坐标封装,最高值最低值获取,黄色平均线坐标封装,十字游离坐标封装

//一:canvas初始化

由于一个是股票走势,一个是十字游离坐标,所以这边用了两个canvas绘图,分层操作

html结构

  <div id="kChart">
      <canvas id="canvas1"></canvas> 
      <canvas id="canvas2"></canvas> 
  </div>
js:
//首先获取元素
//画折线图
var canvas = document.getElementsByTagName("canvas")
 
var canvas1= document.getElementById('canvas1")
var ctx1= canvas1.getContext('2d')
//画十字线
var canvas2= document.getElementById("canvas2")
var ctx2= canvas2.getContext('2d')
 
var dpr = window.devicePixelRatio;//获取设备dpr,高清适配
//动态设置canvas宽高 这里设置的高度可视设备可视窗口高度的一半
function setCanvasWH(canvasId) {
  canvasId.width = document.documentElement.clientWidth * dpr;
  canvasId.height = document.documentElement.clientHeight / 2 * dpr;
  canvasId.style.width = canvasId.width / dpr + 'px';
  canvasId.style.height = canvasId.height / dpr + 'px';
}
setCanvasWH(canvas)//动态设置折线图宽高
setCanvasWH(canvas3)//动态设置画十字线宽高
canvas.width = canvas1.width;
canvas.height = canvas1.height;
var canvasWidth = canvas.width 
var canvasHeight = canvas.height;
// 由于画布扩大,canvas的坐标系也跟着扩大,如果按照原先的坐标系绘图内容会缩小  所以需要将绘制比例放大
ctx1.scale(dpr, dpr)
 
//二:定义所需参数
// 公共参数
var result;       //分时数据结果
var closeP = '57.38';//昨日收盘价;,这个是要动态获取的
var minP;       //最低
var maxP;      //最高
var singleScase;  //定义单像素刻度 动态
var xyZb = [];    //分时图坐标集合
var avgxyZb = [];   //平均线坐标集合
var dWidth = canvasWidth / 240;//单个数据宽度 股票每周一到周五9:30开盘,上午9:30-11:30 120分钟,下午13:00:15:00也是120分钟,每一分钟一条数据,一条总共240条数据
var maxAbs;//比较数组中的最大值与买卖点的最大值
var reserveH = 12.5//保留高度
var realH = canvas.height / dpr - reserveH * 2; //真实高度canvas内容可视高度
var priceXY = []  // 定义坐标数组 
var cz;  //平均高度单个
 
//首先知道昨天的收盘价格,每一条数据由昨日收盘价来定义上下的高度,昨天收盘价Y坐标在整个canvas高度的一半
var closeH = parseFloat(canvas.height / 2)//昨日收盘价高度
//三:定义定时器,3s请求次股票数据
var t = setInterval(() => {
  requestData({
    code: '000000"//股票代码
    market: '00'  //股票市场
  })
}, 3000)
//请求股票数据
function requestData(data) {
  var xhr = new XMLHttpRequest();
  xhr.open('get',"url","true")
  xhr.send();
  //成功回调
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        var res = JSON.parse(xhr.responseText).root    
  assembleData(res); // 组装最新数据格式 根据数据组装坐标
   drawLine(res);// 画分时图
   drawCoordinate(res);// 画十字线坐标
      }
    }
  }
}
 
//四:组装最新数据格式 封装坐标
var assembleData = function (data) {
//设置涨跌幅
  for (var i = 0; i < data.length; i++) {
    if (data[i].preClose == 0 || data[i].price == 0) {
      data[i].zzf = 0;
    } else {
      data[i].zzf = ((data[i].price - data[i].preClose) / data[i].preClose * 100).toFixed(2) + "%";
    }
  }
  result = data;
  //黄色的均线数据组装
  loopAvg(result)
  loop_Zero(result);
  loop_Zero2(result);
  getTimeMax(result)
  xyCoordinate(result);
}
//最小为0往前 拿10次数据
function loop_Zero(data) {
  for (var i = 0; i < data.length; i++) {
    for (var j = 0; j < 10; j++) {
      if ((i - j) <= 0) {
        result[i].low = data[i].low;
      } else {
        if (data[i - j].low != 0) {
          result[i].low = data[i - j].low;
          break;
        }
      }
    }
  }
}
//最大为0往前 拿10次数据
function loop_Zero2(data) {
  for (var i = 0; i < data.length; i++) {
    for (var j = 0; j < 10; j++) {
      if ((i - j) <= 0) {
        result[i].high = data[i].high;
      } else {
        if (data[i - j].high != 0) {
          result[i].high = data[i - j].high;
          break;
        }
      }
    }
  }
}
//获取最大价/最低价
// 定义此刻交易最大/最小数据
var getTimeMax = function (result) {
  var newMax = [];
  var newMin = [];
  result.map((max) => {
    newMax.push(max.high);
    newMin.push(max.low);
  })
  maxP = Math.max.apply(Math, newMax)
  minP = Math.min.apply(Math, newMin)
  //获取设置买点,卖点最大值 
    maxAbs = Math.max(Math.abs((maxP - parseFloat(closeP))), Math.abs(minP - parseFloat(closeP)))
      .toFixed(2);
    singleScase = parseFloat(maxAbs) / (canvasHeight / 2 - reserveH)
    console.log('定义单像素刻度', singleScase)
    //赋值最大值
    maxP = (parseFloat(closeP) + parseFloat(maxAbs)).toFixed(2)
    //赋值最小值
    minP = (parseFloat(closeP) - parseFloat(maxAbs)).toFixed(2)
 
  cz = realH / (maxP - minP);
}
 
//黄色的均线数据组装 //动态获取的数据
function loopAvg(data) {
  for (var i = 0; i < data.length; i++) {
    for (var j = 0; j < 10; j++) {
      if ((i - j) <= 0) {
        result[i].avgPrice = data[i].avgPrice;
      } else {
        if (data[i - j].high != 0) {
          result[i].avgPrice = data[i - j].avgPrice;
          break;
        }
      }
    }
  }
}
//组装折线图坐标xy数据 
function xyCoordinate(that) {
  xyZb = [];
  avgxyZb = [];
  closeAvg = [];
  //分时坐标
  for (var i = 0; i < result.length; i++) {
    var obj = {};
    obj.xZb = (dWidth / 2 + dWidth * i) / dpr;
    obj.yZb = ((maxP - result[i].price) * cz + reserveH)
    xyZb.push(obj)
    //均线数据坐标
    var obj2 = {};
    obj2.xZb = (dWidth / 2 + dWidth * i) / dpr;
    obj2.yZb = (maxP - result[i].avgPrice) * cz + reserveH;
    avgxyZb.push(obj2)
  };
}
 
// 画分时图
var drawLine = function (data) {
 //每次需要清除画布
  ctx1.clearRect(0, 0, canvas.width, canvas.height)
  ctx1.save();
  //统一字体大小
  var drawText;
  if (dpr < 3) {
    drawText = 6 * dpr + 'px'
  } else if (dpr >= 3) {
    drawText = 4 * dpr + 'px'
  }
  ctx1.beginPath();
  ctx1.lineWidth = 1;
  ctx1.fillStyle = "rgba(0,0,255,0)"
  ctx1.setLineDash([0, 0], 0);
  ctx1.strokeStyle = 'rgba(0,0,255,0)';
  ctx1.moveTo(xyZb[0].xZb, xyZb[0].yZb);
  // console.log(xyZb)
  for (var i = 1; i < xyZb.length - 1; i++) {
    ctx1.lineTo(xyZb[i].xZb, xyZb[i].yZb)
  }
  if (xyZb.length == 240) {
    ctx1.lineTo(canvas.width / dpr, xyZb[xyZb.length - 1].yZb);
    ctx1.lineTo(canvas.width / dpr, canvas.height / dpr + 1);
  } else {
    ctx1.lineTo(xyZb[xyZb.length - 1].xZb, xyZb[xyZb.length - 1].yZb);
    ctx1.lineTo(xyZb[xyZb.length - 1].xZb, canvas.height / dpr + 1);
  }
  ctx1.lineTo(0, canvas.height / dpr + 1);
  ctx1.fillStyle = 'rgba(160,210,249,.5)'
  ctx1.fill();

  //画最上边范围线
  ctx1.beginPath();
  ctx1.lineWidth = 0.5;
  ctx1.strokeStyle = 'blue'
  ctx1.moveTo(xyZb[0].xZb, xyZb[0].yZb);
  for (var i = 1; i < xyZb.length - 1; i++) {
    ctx1.lineTo(xyZb[i].xZb, xyZb[i].yZb)
  }
  ctx1.stroke();
  ctx1.restore();

  // 最低值
  ctx1.save()
  ctx1.beginPath();
  ctx1.lineWidth = 1;
  ctx1.fillStyle = "#000"
  ctx1.moveTo(dWidth / 2, canvasHeight / dpr - reserveH)
  ctx1.lineTo(canvasWidth - dWidth / 2, canvasHeight / dpr - reserveH);
  ctx1.setLineDash([0, 0], 0);
  ctx1.strokeStyle = '#ddd';
  ctx1.stroke();
  ctx1.font = `${drawText} Arial`
  ctx1.textBaseline = 'bottom'
  ctx1.fillText(minP, 0, canvasHeight / dpr - reserveH)

  // 最高
  ctx1.beginPath();
  ctx1.lineWidth = 1;
  ctx1.moveTo(dWidth / 2, reserveH);
  ctx1.lineTo(canvasWidth - dWidth / 2, reserveH)
  ctx1.font = `${drawText} Arial`
  ctx1.textBaseline = 'top'
  ctx1.fillText(maxP, 0, reserveH)
  ctx1.fillStyle = "#000"
  ctx1.setLineDash([0, 0], 0);
  ctx1.strokeStyle = '#eee'
  ctx1.stroke();

  //昨天闭市价
  ctx1.beginPath();
  ctx1.lineWidth = 0.5;

  // if (closeAvg.length == 0) {
  //   return false
  // }
  ctx1.moveTo(0, closeH / dpr)
  ctx1.lineTo(canvasWidth / dpr, closeH / dpr);
  ctx1.font = `${drawText} Arial`
  ctx1.textBaseline = 'middle'
  ctx1.fillText(closeP, 0, closeH / dpr)
  ctx1.setLineDash([2, 6], 5);
  ctx1.strokeStyle = '#000'
  ctx1.stroke();

  //平均线
  ctx1.beginPath();
  ctx1.lineWidth = 0.5;
  ctx1.setLineDash([0, 0], 0);
  ctx1.strokeStyle = 'orange'
  ctx1.moveTo(avgxyZb[0].xZb, avgxyZb[0].yZb);
  for (let i = 1; i < avgxyZb.length; i++) {
    ctx1.lineTo(avgxyZb[i].xZb, avgxyZb[i].yZb);
  }
  ctx1.stroke();

  //画底部分钟时间
  ctx1.textBaseline = 'top'
  ctx1.fillText("9:30", 0, canvasHeight / dpr - reserveH)
  ctx1.fillText("15:00", (canvasWidth / dpr - ctx1.measureText("15:00").width),
    canvasHeight / dpr -
    reserveH)
  ctx1.fillText("11:30/13:00", canvasWidth / dpr / 2 - ctx1.measureText("11:30/13:00")
    .width / 2,
    canvasHeight / dpr - reserveH)
  ctx1.stroke()
  ctx1.restore();
}
 
//画十字线

//3s清除十字线
  setInterval(() => {
    ctx2.clearRect(0, 0, canvas.width, canvas.height);
  }, 3000);

var drawCoordinate = function (result) {
  //移动时画十字坐标
//触摸滑动事件 //这里要注意层级问题,滑动层要高一层
  canvas2.addEventListener("touchmove", function (event) {
    ctx2.save()
    ctx2.clearRect(0, 0, canvas.width, canvas.height)
    event.preventDefault();
    //获取触摸坐标
    var fingerMove = event.touches[0];
    var fMoveX = fingerMove.clientX
    var fMoveY = fingerMove.clientY

    if (fMoveX < 0) {
      fMoveX = 0
    } else if (fMoveX > canvasWidth / dpr) {
      fMoveX = canvasWidth / dpr - 1
    }
    if (fMoveY < 0) {
      fMoveY = reserveH / dpr
    } else if (fMoveY > canvas.height / dpr) {
      fMoveY = canvas.height / dpr - (reserveH / dpr)
    }
    var leftText;
    if (dpr < 3) {
      leftText = 12 * dpr + 'px'
    } else if (dpr >= 3) {
      leftText = 10 * dpr + 'px'
    }
    //格式化底部时间
    var index;
    var arrY = [];
    var arrX = [];
    for (var i = 0; i < 240; i++) {
      arrX.push(window.innerWidth / 240 * i);
    }
    for (var i = 0; i < arrX.length; i++) {
      arrY.push(Math.abs(fMoveX - arrX[i]));
    }
    var min2 = Math.min(...arrY);
    index = arrY.findIndex((item) => {
      return item == min2
    })
    if (index + 570 > 690) {
      var timers = function () {
        if ((index + 570 + 90) % 60 < 10) {
          return parseInt((index + 570 + 90) / 60) + ":0" + (index + 570 + 90) % 60
        }
        return parseInt((index + 570 + 90) / 60) + ":" + (index + 570 + 90) % 60
      }();
    } else {
      var timers = function () {
        if ((index + 570) % 60 < 10) {
          return parseInt((index + 570) / 60) + ":0" + (index + 570) % 60
        }
        return parseInt((index + 570) / 60) + ":" + (index + 570) % 60
      }();
    }

    ctx2.beginPath();
    ctx2.lineWidth = 2
    ctx2.fillStyle = '#22364B' //字体颜色
    ctx2.font = `${leftText} Arial`

    // 画Y轴时间
    // 当触摸点小于这个框的宽度一半时
    if (fMoveX <= ctx2.measureText(timers).width / 4) {
      console.log('当触摸点小于这个框的宽度一半时')
      //画框
      ctx2.fillRect(0, 0, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr)
      ctx2.fillRect(0, canvas.height - reserveH * dpr, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr)
      //画线
      ctx2.moveTo(fMoveX * dpr + 1 * dpr, reserveH * dpr);
      ctx2.lineTo(fMoveX * dpr + 1 * dpr, canvas.height - reserveH * dpr);
      ctx2.setLineDash([]);
      ctx2.stroke();
      //画字
      ctx2.fillStyle = '#fff' //字体颜色
      ctx2.textBaseline = 'middle'
      ctx2.fillText(timers, ctx2.measureText(timers).width / 2 + 6 * dpr, reserveH * dpr / 2);
      ctx2.fillText(timers, ctx2.measureText(timers).width / 2 + 6 * dpr, canvas.height - reserveH * dpr / 2);

    } else if (fMoveX >= window.innerWidth - ctx2.measureText(timers).width / 2 + 2 * dpr) {
      console.log('当触摸点大于这个框的宽度一半时')
      ctx2.fillRect(canvasWidth - ctx2.measureText(timers).width * 2 + 15 * dpr, 0, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr);
      ctx2.fillRect(canvasWidth - ctx2.measureText(timers).width * 2 + 15 * dpr, canvas.height - reserveH * dpr, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr);

      ctx2.moveTo(fMoveX * dpr + 1 * dpr, reserveH * dpr);
      ctx2.lineTo(fMoveX * dpr + 1 * dpr, canvas.height - reserveH * dpr);
      ctx2.setLineDash([]);
      ctx2.stroke();

      ctx2.fillStyle = '#fff' //字体颜色
      ctx2.textBaseline = 'middle'
      ctx2.fillText(timers, canvasWidth - ctx2.measureText(timers).width / 2 - 5 * dpr, reserveH * dpr / 2);
      ctx2.fillText(timers, canvasWidth - ctx2.measureText(timers).width / 2 - 5 * dpr, canvas.height - reserveH * dpr / 2);

    } else {
      console.log('当触摸点在中间时')
      ctx2.fillRect(fMoveX * dpr - ctx2.measureText(timers).width / 2 - 6 * dpr, 0, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr);
      ctx2.fillRect(fMoveX * dpr - ctx2.measureText(timers).width / 2 - 6 * dpr, canvas.height - reserveH * dpr, ctx2.measureText(timers).width + 20 * dpr, reserveH * dpr);

      ctx2.moveTo(fMoveX * dpr + 1 * dpr, reserveH * dpr);
      ctx2.lineTo(fMoveX * dpr + 1 * dpr, canvas.height - reserveH * dpr);
      ctx2.setLineDash([]);
      ctx2.stroke();

      ctx2.fillStyle = '#fff' //字体颜色
      ctx2.fillText(timers, fMoveX * dpr + 6 * dpr, reserveH * dpr / 2);
      ctx2.fillText(timers, fMoveX * dpr + 6 * dpr, canvas.height - reserveH * dpr / 2);
    }


    // 画X轴价格与涨跌幅  计算公式
    // 每个时刻价格 = 收盘价 - ((触摸高 - 收盘高) * 单像素比例)
    var leftPrice = closeP - ((fMoveY * dpr - closeH) * singleScase).toFixed(2);

    // var xProportion = (fMoveY - reserveH) / realH;
    // var priceArr = maxP - xProportion * (maxP - minP)
    ctx2.fillStyle = '#22364B';

    //在最顶部触碰时
    if (fMoveY < reserveH / dpr) {
      //画X左上框
      ctx2.beginPath()
      ctx2.fillRect(-10 * dpr, 0, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);
      //画X右上框
      ctx2.fillRect(window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width - 20 * dpr, 0, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);

      //画左右上方文字样式  
      ctx2.fillStyle = '#fff'
      ctx2.font = `${leftText} Arial`
      ctx2.textBaseline = 'middle'
      ctx2.textAlign = "center";

      //画左右上方文字
      ctx2.fillText(leftPrice != 'NAN' ? (leftPrice).toFixed(2) : "--", ctx2.measureText((leftPrice).toFixed(2)).width / 2 + 5 * dpr, reserveH);
      ctx2.fillText(result[index] ? result[index].zzf : "--", window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width / 2 - 5 * dpr, reserveH);

    } else if (fMoveY >= canvasHeight / dpr - reserveH) {
      //当触摸点在最低点时候
      //画左下框
      ctx2.beginPath()
      ctx2.fillRect(-10 * dpr, canvasHeight - reserveH * dpr, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);
      //画右下框
      ctx2.fillRect(window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width - 20 * dpr, canvasHeight - reserveH * dpr, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);

      //画左右下方文字样式
      ctx2.fillStyle = '#fff'
      ctx2.font = `${leftText} Arial`
      ctx2.textBaseline = 'middle'
      ctx2.textAlign = "center";

      //画左右下方文字
      ctx2.fillText(leftPrice != 'NAN' ? (leftPrice).toFixed(2) : "--", ctx2.measureText((leftPrice).toFixed(2)).width / 2 + 5 * dpr, canvasHeight - reserveH);
      ctx2.fillText(result[index] ? result[index].zzf : "--", window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width / 2 - 5 * dpr, canvasHeight - reserveH);
    } else {

      ctx2.beginPath()
      ctx2.fillRect(-10 * dpr, fMoveY * dpr - 10, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);
      ctx2.fillRect(window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width - 20 * dpr, fMoveY * dpr - 10, ctx2.measureText((leftPrice).toFixed(2)).width + 20 * dpr, reserveH * dpr);

      //画中间文字样式
      ctx2.fillStyle = '#fff'
      ctx2.font = `${leftText} Arial`
      ctx2.textBaseline = 'middle'
      ctx2.textAlign = "center";
      ctx2.fillText(leftPrice != 'NAN' ? (leftPrice).toFixed(2) : "--", ctx2.measureText((leftPrice).toFixed(2)).width / 2 + 5 * dpr, fMoveY * dpr + 2 * dpr);
      ctx2.fillText(result[index] ? result[index].zzf : "--", window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width / 2 - 5 * dpr, fMoveY * dpr + 2 * dpr);

    }
    ctx2.beginPath()
    ctx2.moveTo(ctx2.measureText((leftPrice).toFixed(2)).width + 10 * dpr, parseInt(fMoveY * dpr + 5));
    ctx2.lineTo(window.innerWidth * dpr - ctx2.measureText((leftPrice).toFixed(2)).width - 20 * dpr, parseInt(fMoveY * dpr + 5));
    ctx2.stroke();
  })
}
这里只是提供个画图思路,具体情况还项目需要,大致就这样,代码也许有误,最好不要全部复制用,可以用这个思路来画图

猜你喜欢

转载自www.cnblogs.com/ForeverSky/p/11966485.html