H5用canvas畫股票分時圖

最近公司要開發一個關於模擬股票走勢的項目,本想着用echarts開發,但考慮到echarts有些不能自定義設置,靈活性不是很方便,因此決定用canvas來開發。html

以下圖所示:canvas

 

 

 

 

 

 

 

 

 

 

所要考慮的問題:canvas寬高動態設置,移動端適配,股票座標封裝,最高值最低值獲取,黃色平均線座標封裝,十字遊離座標封裝數組

//一:canvas初始化app

因爲一個是股票走勢,一個是十字遊離座標,因此這邊用了兩個canvas繪圖,分層操做echarts

html結構oop

  <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();
  })
}
這裏只是提供個畫圖思路,具體狀況還項目須要,大體就這樣,代碼也許有誤,最好不要所有複製用,能夠用這個思路來畫圖
相關文章
相關標籤/搜索