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線開發 為同一作者
*轉載請附出處。