h5行情k線開發


前言

        由於公司項目需要,要做港股行情的H5版本,經過分析需求,大致有兩塊難點: 一是行情的推送接收,二是行情K線的生成及相關操作。本文章主要分析行情K線的相關實現,由於我們前端團隊之前是沒有相關的工作經驗的,所以我們第一反應就是去網上搜現成的插件或者相關文檔。經過查找我們發現其實網上這方面的資料不多,相關插件也是比較少,比較符合的相關插件有tradingView以及百度團隊開發的ECharts, 但是兩者插件體積比較大而且在H5移動端的處理並不是特別好。經過討論我們決定自研開發。

線上效果

   下面是我們H5線上行情系統的實際操作圖, 也可以掃碼體驗。

    ​                                 

關鍵點分析

       開發這套行情的K線圖表,關鍵點主要有兩點,其一是K線圖,其二是手勢的處理。K線圖難度不是很大在熟悉Canvas畫圖基礎的情況下注意不同區域划分和層級即可,重點在於數據的一些計算和判斷;手勢的處理就比較麻煩了需要考慮到長按,滑動,放大縮小,慣性滑動,觸底加載,橫屏等場景。下面就這些關鍵點進行逐一分析。

具體實現

一,K線圖基礎

1、K線圖基於Z軸(可以理解成css樣式中的z-index)分成了三層:

     第一層畫坐標軸的各種文字和線條包括邊框線,XY軸分割線,X軸時間和日期,Y軸價格和成交量成交額等數據文本;

    

     第二層畫主體數據圖包括分時的走勢線,分時的均線,日K的柱狀圖,MA5,MA10,MA20走勢線,最高價,最低價,成交量或成交額的柱狀圖等K線主體數據圖或文本;

    

     第三層畫長按K線時出現的十字線及十字線的數據文本。

    

     最后將三層相對定位在同一坐標即可。

2、Z軸每一層基於Y軸分成了三部分:

     第一部分畫上方走勢圖的線條,圖形,文本;

     第二部分畫中間時間或日期文本;

     第三部分畫下方成交量或成交額的線條,圖形,文本。

    

     以上canvas的顏色、大小、線條粗細寫成插件配置形式即可。

3、幾個需要注意的圖形畫法

      3.1、分時圖的畫法邏輯:從第一個數據點的坐標(x0, y0)畫筆開始(beginPath)移動到(moveTo)下一個點的坐標(x1, y1)依次移動到最后一個點的坐標(xn, yn),到最后一點的坐標后移動到第一部分的最右下方點(width, height)然后再移動到第一部分最左下方點(0, height)最后回到起點(x0, y0)形成閉合填充(fill)漸變色(createLinearGradient)關閉畫筆(closePath)。

      3.2、K線柱狀圖:這里首先介紹下柱狀圖(可能有點多余)

      轉換成canvas畫圖角度其實就是線條和矩形的結合,線條的畫法比較容易,中心柱狀圖需要注意的地方是如果是空心柱狀圖需要疊加兩層矩形第一層的背景色和你主背景色一致第二層畫邊框矩形邊框顏色畫對應的漲跌顏色(這里是紅漲綠跌)所以邊框顏色設置成紅色。下面是一段偽代碼:

var rectConf = {
    xAxis: 10,  // 矩形框x軸坐標
    yAxis: 20,  // 矩形框y軸坐標
    width: 5,   // 矩形框寬度
    height: 30, // 矩形框高度
}

if (!this.isSolidCandle) { // 如果不是實心蠟燭圖
    this.ctx.fillStyle = this.COLORS.MAINBG; // 主背景色
    this.ctx.fillRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
    this.ctx.strokeRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
    this.ctx.fillStyle = lineColor; // 線條顏色
} else {
    this.ctx.fillStyle = lineColor; // 線條顏色
    this.ctx.fillRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
}

      其它可能存在難點的地方更多是涉及到計算,例如最高點最低點坐標位置,第二部分時間文本坐標位置及寬度等,這里就不一一介紹了,有問題可以下方留言。

二,手勢事件處理  

1、長按事件:

      我們知道js中是沒有這個事件的,但是是有觸摸事件,所以這里利用觸摸事件來模擬長按事件。定義從觸摸開始超過200ms不動即為長按,可以在觸摸事件中使用setTimeout定時器超過200ms即執行長按事件,並且設置長按標識,但是這里需要注意的是在滑動事件中清除這個定時器,如果長按事件已經執行那么清除了也不會有影響,如果還沒執行說明還沒到達長按的條件,利用這個特性就能模擬長按事件了,下面是一段偽代碼:

 

// 觸摸開始事件
touchStartEvent(e) {
   this.longTapTimeout = setTimeout(() => {
      this.longTapFlag = true;
      this.longTap(touchOne);
   }, 200);
},

// 觸摸移動事件
touchMoveEvent(e) {
   if (this.longTapTimeout) {
      clearTimeout(this.longTapTimeout);
      this.longTapTimeout = null;
   }
   if (this.longTapFlag) {
      // 長按滑動事件(即十字線滑動事件)
      this.touchMove();
   } else {
      // k線滑動事件
      this.swipe();
   }
} 

 

2、放大縮小事件:

這個事件是雙指事件,在js中是可以通過event.targetTouches的長度來判斷的。實現放大縮小大體思路是:

      step1:計算兩指中間坐標點;

      step2:計算兩指間的直線距離;

      step3:根據直線距離以及上一次的直線距離計算需要放大或縮小的刻度;

      step4:計算刻度不變時中間坐標點對應K線數組的索引index1;

      step5:計算刻度變動后中間坐標點對應K線數組的索引index2;

      step6:變動前后的索引值相減可以獲得變動的柱狀圖條數,重新渲染圖形即可。

這里有幾個點需要注意:1. 縮小時左邊數據已經到底則需要加載更多數據,2.刻度粗細應該設置上下限在達到上下限的時候避免再次渲染圖形。下面是部分計算代碼:

// 放大縮小刻度
const scale = (touchDistance - this.nextTouchDistance) / this.nextTouchDistance;

// 放大縮小事件
this.zoomIn(centerX, scale);

step1:計算兩指中間坐標點

// 兩指x軸方向距離
const xLen = Math.abs( e.targetTouches[1].pageX - e.targetTouches[0].pageX, ); // 兩指y軸方向距離(橫屏需要) const yLen = Math.abs( e.targetTouches[1].pageY - e.targetTouches[0].pageY, ); // canvas內容區矩形的邊框信息 const clientRect = this.container.getBoundingClientRect(); // 相對於屏幕的中心點 let center; // 相對於canvas圖層的中心點 let centerX; center = e.targetTouches[1].clientX - (e.targetTouches[1].clientX - e.targetTouches[0].clientX) / 2; centerX = center - clientRect.left;
step2:計算兩指間的直線距離;
// 兩指間距離 (根據勾股定理計算)
const touchDistance = Math.sqrt(xLen * xLen + yLen * yLen);

step3:根據直線距離以及上一次的直線距離計算需要放大或縮小的刻度;

// 需要放大縮小刻度
const scale = (touchDistance - this.nextTouchDistance) / this.nextTouchDistance;

// 放大縮小處理
this.zoomIn(centerX, scale);

// 記住本次兩指間距離
this.nextTouchDistance = touchDistance;

step4:計算刻度不變時中間坐標點對應K線數組的索引index1;step5:計算刻度變動后中間坐標點對應K線數組的索引index2;

step6:變動前后的索引值相減可以獲得變動的柱狀圖條數,重新渲染圖形即可。

zoomIn(centerX, scale) {
    // 中心刻度
    const centerScale = Math.ceil(centerX / this.cellWidth); // this.cellWidth為圖中柱狀圖寬度
    // K線數組索引(刻度不變時中間坐標點)
    const centerIndex = Math.max(Math.min(this.kData.length - 1, centerScale - 1), 0);
    const originalScale = this.scale;
    this.scale += scale;
    if (this.scale <= 0.5) {
      this.scale = 0.5;
    } else if (this.scale > 4) {
      this.scale = 4;
    }
    
    if (originalScale !== this.scale) {
      // K線條目數
      this.count = Math.floor(60 / this.scale)
      this.cellWidth = this.canvasWidth / this.count;
      // 計算刻度變動后中間坐標點
      const centerScale1 = Math.ceil(centerX / this.cellWidth );
      const centerIndex1 = Math.max(Math.min(this.kData.length - 1, centerScale1 - 1),0);
      const scaleDiff = centerIndex1 - centerIndex;

      let index = this.indexStart - scaleDiff;
      index = Math.min(index, Math.max(this.allData.length - this.count, 0));
      index = Math.max(index, 0);
      if (index === 0 && this.loadMore === false) {
        this.loadMore = true;
        this.loadMoreCallback();
        return;
      }
      this.indexStart = index;
      this.indexStartTemp = index;
      const data = this.allData.slice(index, index + this.count) || [];
      // 重新渲染圖形
      this.drawKLine();
   }
},

以上代碼有些方法沒有體現出來,因為代碼比較長所以只粘貼相應的代碼,如果有迷惑的地方,可以下方留言。

3、慣性滑動事件:

      慣性滑動在移動端是個很好的體驗,什么時候會觸發慣性呢,兩次滑動的間隔時間小於一個設定值既可觸發慣性滑動。慣性需要考慮加速度,靈敏度等因素。這里慣性是用的js中requestAnimationFrame方法,存在兼容性問題可以用setTimeout模擬這里不多做兼容處理的介紹,因為大部分機型是兼容的。具體實現為:在滑動開始時記錄時間,在觸摸結束事件中判斷時間間隔是否小於100ms,如果小於100ms則執行慣性滑動事件,根據滑動最后時間和滑動開始時間計算滑動速度,然后根據設置的靈敏度來計算加速度,執行慣性動畫,然后每執行一次慣性事件減少速度直到速度為0停止慣性事件。以下為部分代碼:

// 觸摸結束事件
touchendEvent(e) {
   this.touchEndTime = new Date().getTime();
   // 開始移動事件和觸摸結束事件時間間隔
   const intervalTime = this.touchEndTime - this.startMoveTime;
   // 最后一次滑動結束和開始滑動時間間隔
   let timeStamp = this.endMoveTime - this.startMoveTime;
   timeStamp = timeStamp > 0 ? timeStamp : 8;
   // 停頓時間超過100ms不產生慣性滑動;
   if (intervalTime < 100) {
     this.speed = Math.abs((this.startX - this.currentX) / timeStamp); // 計算速度
     this.acceleration = this.speed / this.sensitivity; // 根據靈敏度(sensitivity)計算加速度
     this.frameStartTime = new Date().getTime(); // 動畫開始時間
     this.inertiaFrame = requestAnimationFrame(this.moveByInertia);
   }
}

// 慣性滑動時間
moveByInertia() {
    this.frameEndTime = new Date().getTime(); // 每次動畫結束時間
    this.frameTime = this.frameEndTime - this.frameStartTime; // 動畫執行時間
    if (this.currentX < this.startX) {
      // 向左慣性滑動;
      this.roll.dir = 1;
    } else {
      // 向右慣性滑動;
      this.roll.dir = -1;
   }
   this.speed = Math.max(this.speed - this.acceleration * this.frameTime, 0); // 逐漸降低速度
   if (this.speed === 0) {
     cancelAnimationFrame(this.inertiaFrame);
     this.touchEnd();
     return;
   }
   this.roll.len += this.speed;
   this.swipe(this.roll); // 執行滑動K線方法
   this.frameStartTime = this.frameEndTime;
   this.inertiaFrame = requestAnimationFrame(this.moveByInertia);
}

 

這里也有幾點代碼中沒有寫進去的如需要判斷是否已經觸及邊緣、橫屏的處理等等。

3、觸底回彈事件:

       觸底回彈主要是需要判斷是否已經到左右兩側的點,設置到達臨界點后允許滑動的K線條數結束滑動后進行慣性回彈至臨界點,慣性回彈類似慣性滑動的處理。以下為主要邏輯代碼:

// 慣性回彈
 springbackByInertia() {
    // 設置到達臨界點后允許滑動K線條數為5根
    const roll = { dir: 1, len: 5 };
    // indexStartTemp是k線數組開始的標識位
    // 如果this.indexStartTemp > this.allData.length - this.count則判斷到達了最左側的臨界點
    // 如果this.indexStartTemp < 0則判斷達到了最右側的境界點
    if (
      this.indexStartTemp > this.allData.length - this.count ||
      this.indexStartTemp < 0
    ) {
      this.swipe(roll);
      this.springInertiaFrame = requestAnimationFrame(this.springbackByInertia);
    } else {
      cancelAnimationFrame(this.springInertiaFrame);
      this.swipe(roll);
    }
},

4、橫屏事件:

       這里處理橫屏事件是通過輕擊事件來觸發的,如何判斷是否為輕擊事件呢。在觸摸結束事件中判斷觸摸時間小於100ms且移動距離小於5px即視為輕擊事件來觸發橫屏事件。下面為判斷輕擊事件的代碼:

 // 是否為輕擊事件(觸摸時間小於100ms且移動距離小於5px)
isTap() {
  if (this.touchEndTime - this.touchStartTime < 100) {
     if (!this.currentX && !this.currentY) {
     return true;
   }
     const xScale = Math.abs(this.currentX - this.startX);
     const yScale = Math.abs(this.currentY - this.startY);
     if (xScale < 5 && yScale < 5) {
       return true;
     }
   }
   return false;
},

 

橫屏的實現這里是利用css3的旋轉屬性對canvas進行90度旋轉,旋轉后需要注意的是滑動的時候X軸與Y軸要和豎屏的時候替換來處理。

三,結言

     以上是我們自研行情的K線圖部分的處理,難免有些地方存在不足,也希望讀者能給予意見和指導。由於以上代碼大都為代碼片段,所以會存在變量或方法名沒有定義的情況,望多諒解。我們的行情推送是使用websocket推送結合protobuf數據格式來完成的,如果有需要可以另外介紹。

 

注:本文和CSDN文章h5行情k線開發為同一作者

*轉載請附出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM