Canvas 高性能K線圖的架構(gòu)方案
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
前言證券行業(yè),最難的前端組件,也就是k線圖了。 1、H5 K線圖,支持無(wú)限左右滑動(dòng)、樣式可隨意定制; 滑動(dòng)K線圖組件 Github Page 預(yù)覽地址 股票詳情頁(yè)源碼 Github Page 預(yù)覽地址 注意:以上的 demo 還有一些 bug, 沒(méi)時(shí)間修復(fù), 預(yù)覽地址是直接在 github 上部署的, 所以最好通過(guò) vpn科學(xué)上網(wǎng),否則可能訪問(wèn)不了,然后再在移動(dòng)端打開(kāi)頁(yè)面。 另外, 上面的股票詳情頁(yè), 還沒(méi)有做自適應(yīng),等我有時(shí)間再改。 一、先看最終的效果1、GIF動(dòng)圖如下
2、支持樣式自定義用可以屏幕取色器,獲取東方財(cái)富的配色 codeinword.com/eyedropper 圖一、圖二,是參考東方財(cái)富黑白皮膚的配色, 圖三是參考騰訊自選股的配色。
二、canvas 注意事項(xiàng)1、整數(shù)坐標(biāo),會(huì)導(dǎo)致模糊canvas 在畫(huà)線段, 通常會(huì)出現(xiàn)以下代碼:
假設(shè)上面的兩個(gè)點(diǎn)是(1,10)和(5,10),那么畫(huà)出來(lái)的實(shí)際上是一條橫線, 但是 canvas 不是這樣處理的, canvas 默認(rèn)線條會(huì)與整數(shù)對(duì)齊, 并且由于粗度被拉伸,顏色也會(huì)被淡化,那怎么解決這個(gè)問(wèn)題呢? 處理方式也很簡(jiǎn)單, 通過(guò) cxt.translate(0.5, 0.5) 將坐標(biāo)往下移動(dòng) 0.5 個(gè)像素, 典型的代碼如下:
在我的代碼中, 也體現(xiàn)了類似的處理。 2、如何處理高像素比帶來(lái)的模糊設(shè)備像素比越高,理論上應(yīng)該越清晰,因?yàn)樵瓉?lái)用一個(gè)小方塊來(lái)渲染1px, 現(xiàn)在用2個(gè)(dpr=2的情況)小方塊來(lái)渲染,應(yīng)該更清晰才對(duì),但是canvas不是這樣的。 例如,通過(guò)js獲取父容器 div 的寬度是 width, 這時(shí)候如果設(shè)置 canvas.width = width,在設(shè)備像素比為2的時(shí)候, canvas 畫(huà)出來(lái)的寬度為css對(duì)應(yīng)寬度的一半, 如果強(qiáng)制通過(guò) css 將 canvas 寬度設(shè)置為 width, 則 canvas 會(huì)被拉長(zhǎng)一倍, 導(dǎo)致出現(xiàn)鋸齒模糊。 注意了嗎?上面所說(shuō)的 canvas.width=width 與 css 設(shè)置的 #canvas { width: width } 起到的效果是不一樣的。不要隨便通過(guò) css 去設(shè)置 canvas 的寬高, 容易被拉伸變形或者導(dǎo)致模糊。 通用的處理方式是:
三、樣式配置為了方便樣式自定義, 我獨(dú)立出一個(gè)默認(rèn)的配置對(duì)象 defaultKlineConfig, 參數(shù)的含義如下圖所示,其實(shí)下圖這個(gè)風(fēng)格的標(biāo)注, 是通過(guò) excalidraw 這個(gè)軟件畫(huà)的, 也是 canvas 做的開(kāi)源軟件, 可見(jiàn) canvas 在前端可視化領(lǐng)域的重要性, 這個(gè)扯遠(yuǎn)了,打住。 如上圖, 整個(gè)canvas 畫(huà)板, 分成 5 部分, 十字交叉線的顏色, X軸 與 Y軸 的 tooltip 背景色、字體大小的參數(shù)如下: 四、均線計(jì)算從上面的圖可以看出, 需要畫(huà) 5日均線、10日均線、20日均線, 成交量快線(10日)、成交量慢線(20日) 但是, 接口沒(méi)有給出當(dāng)日的均線值, 需要自己計(jì)算。 5日均線 = (過(guò)去4個(gè)成交日的收盤價(jià)總和 + 今日收盤價(jià))/ 5 10日均線 = (過(guò)去9個(gè)成交日的收盤價(jià)總和 + 今日收盤價(jià))/ 10 20日均線 = (過(guò)去19個(gè)成交日的收盤價(jià)總和 + 今日收盤價(jià))/ 20 成交量快線 = (過(guò)去9日成交量 + 今日成交量)/ 10 成交量慢線 = (過(guò)去19日成交量 + 今日成交量)/ 20 所以, 當(dāng)獲取 lmt(一屏的蠟燭圖個(gè)數(shù))個(gè)數(shù)據(jù)時(shí), 為了計(jì)算均線, 需要至少將前 19 個(gè)(我的代碼寫(xiě)20)數(shù)據(jù)都獲取到。當(dāng)前一個(gè)均線已經(jīng)獲取到, 下一個(gè)均線就不需要再累加20個(gè)值再得平均數(shù), 可以省一點(diǎn)計(jì)算: 今日20日均線值 = (昨日均線值 * 20 - 前面第20個(gè)的收盤價(jià) + 今日收盤價(jià))/ 20; 五、分層渲染為了減少重繪,提高性能,可以將K線圖做分層渲染。那分幾層合適?我認(rèn)為是三層。
不動(dòng)層首先, 網(wǎng)格是固定的, 也就是說(shuō),當(dāng)頁(yè)面拖拽、或者長(zhǎng)按出現(xiàn)十字交叉的時(shí)候,底部的網(wǎng)格線是不變的,如果每次拖拽,都需要重繪網(wǎng)格,那這個(gè)其實(shí)是沒(méi)有必要的開(kāi)銷,可以將網(wǎng)格放在最底層(例如 z-index:0),一次性繪制后,就不要再重繪。 變動(dòng)層由于拖拽的時(shí)候,蠟燭柱體,均線,Y軸刻度, X軸刻度, 都需要重繪, 這一塊是無(wú)法改變的事實(shí), 所以, 變動(dòng)層放在中間層(例如 z-index:1),也是最繁忙的一層,并且該層不響應(yīng)觸摸事件。 交互層最后, 交互層由于要捕捉用戶的觸摸行為, 所以,這一層要在最上層(例如 z-index:2)。 交互層監(jiān)聽(tīng)觸摸事件:當(dāng)頁(yè)面快速滑動(dòng), 則響應(yīng)拖拽事件, 即K線圖的時(shí)間線會(huì)左右滑動(dòng);當(dāng)用戶長(zhǎng)按之后才滑動(dòng), 則出現(xiàn)十字交叉浮層。 交互層的好處是, 當(dāng)響應(yīng)十字交叉浮層時(shí), 只需要繪制橫線、豎線、對(duì)應(yīng)X軸和Y軸的值,而不需要重繪蠟燭柱體和均線, 可以減少重繪,最大程度減少渲染壓力。 六、基礎(chǔ)幾何繪制網(wǎng)格線首先計(jì)算出主圖的高度 this.mainChartHeight, 將主圖從上到下等分為4部分,再在左右兩邊畫(huà)出豎線,形成主圖的網(wǎng)格,副圖是成交量圖, 只需畫(huà)一個(gè)矩形邊框即可,用 strokeRect 即可畫(huà)出。
畫(huà)各類均線1、首先計(jì)算出一屏的股價(jià)最大值 max , 股價(jià)最小值 min ,成交量最大值 maxAmount。 2、當(dāng)某一個(gè)點(diǎn)的均線為 value, 根據(jù)最大值、最小值、索引index, 計(jì)算出坐標(biāo)點(diǎn)(x, y), 畫(huà)均線的時(shí)候, 第一個(gè)點(diǎn)用 moveTo(x0, y0),其他點(diǎn)用 lineTo(xn yn), 最后 stroke 連起來(lái)即可。 3、當(dāng)然, 每一條線設(shè)置下顏色, 即 stokeStyle。
畫(huà)出蠟燭柱體
當(dāng)收盤價(jià)大于等于開(kāi)盤價(jià), 選用上面左邊紅色的樣式; 當(dāng)收盤價(jià)小于開(kāi)盤價(jià), 選用上面右邊綠色的樣式。 以紅色蠟燭為例, 最高點(diǎn) A(x0, y0),最低點(diǎn)是 B(x1, y1), 為了畫(huà)出紅色蠟燭, 先后順序別搞混:
按照上面這個(gè)順序, 豎線會(huì)被覆蓋掉一部分,同時(shí),矩形內(nèi)部的白色填充不會(huì)擠壓矩形的紅色邊框, 如果先 stroke 再 fill,容易出現(xiàn)白色填充覆蓋紅色邊框,矩形可能會(huì)變模糊,或者使得紅色變淡,極其不友好,所以按照我上面的順序,可以減少不必要的麻煩。 畫(huà)出文字canvas 畫(huà)出文字, 典型的代碼如下
注意textBaseline 默認(rèn)對(duì)齊方式是 alphabetic, 但 middle 往往更好用, 能實(shí)現(xiàn)垂直居中,但我發(fā)現(xiàn)垂直居中也不是很居中,所以會(huì)特意加減1、2個(gè)像素; 當(dāng)然還有個(gè)textAlign, 能實(shí)現(xiàn)水平對(duì)齊方式, 左右對(duì)齊都可以, 例如上圖最左、最右的時(shí)間標(biāo)簽。 七、交互設(shè)計(jì)根據(jù)上面的GIF動(dòng)圖, 可以知道, 本次做的移動(dòng)端 K 線圖, 最重要的兩個(gè)交互是:
上面的交互,其實(shí)是比較復(fù)雜的,所以需要先設(shè)計(jì)一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu):
當(dāng)用戶往右快速拖拽時(shí), startIndex 根據(jù)用戶拖拽的距離, 適當(dāng)變?。?當(dāng)用戶往左快速拖拽時(shí), startIndex 根據(jù)用戶拖拽的距離, 適當(dāng)變大。 那 arrayList 到底多長(zhǎng)合適, 因?yàn)楣善笨赡苡惺畮啄甑臄?shù)據(jù), 甚至上百年的數(shù)據(jù), 我不能一次性拉取這個(gè)股票的所有數(shù)據(jù)吧? 當(dāng)然,站在軟件性能、消耗等角度,也不應(yīng)該一次性拉取所有的數(shù)據(jù), 我的答案是 arraylist 最多保存5屏的數(shù)據(jù)量,用戶看到的屏幕, 應(yīng)該是接近中間這一屏,也就是第3屏的數(shù)據(jù), 左右兩邊各保存2屏數(shù)據(jù),這樣,用戶拖拽的時(shí)候,可以比較流暢,而不是每次拖拽都要等拉取數(shù)據(jù)再去渲染。 那什么時(shí)候拉取新的數(shù)據(jù)呢? 用戶觸摸完后,當(dāng)startIndex左邊的數(shù)據(jù)少于2屏,開(kāi)始拉取左邊的數(shù)據(jù); 用戶觸摸完后,當(dāng)startIndex右邊的數(shù)據(jù)少于2屏,開(kāi)始拉取右邊的數(shù)據(jù); 那如果用戶一直往右拖拽, 是不是就一直往左邊添加數(shù)據(jù), 這個(gè) arraylist 是不是會(huì)變得很長(zhǎng)? 當(dāng)然不是,例如,當(dāng)我往 arraylist 的左邊添加數(shù)據(jù)的時(shí)候,startIndex 也會(huì)跟著變動(dòng), 因?yàn)橛脩艨吹降牡谝粭l柱體,在 arraylist 的索引已經(jīng)變了。當(dāng)我往 arraylist 的某一邊添加數(shù)據(jù)后, arraylist 的另一邊如果數(shù)據(jù)超過(guò) 2 屏, 要適當(dāng)裁掉一些數(shù)據(jù), 這樣 arraylist 的總數(shù), 始終保持在 5 屏左右,就不會(huì)占用太多的存放空間。 總體思想是, 從 startIndex 開(kāi)始渲染屏幕的第一條柱體, 當(dāng)前屏幕的左右兩邊, 都預(yù)留2屏數(shù)據(jù),防止用戶拖拽頻繁調(diào)用接口, 導(dǎo)致卡頓; 同時(shí)也控制了 arraylist 的長(zhǎng)度, 這是虛擬列表的變形,這樣設(shè)計(jì),可以做一個(gè) 高性能 的k線圖。 八、觸摸事件解耦根據(jù)上面的分析:
以上兩種拖拽,都在 touchmove 事件中觸發(fā), 那怎么區(qū)分開(kāi)呢? 典型的 touchstart、 touchmove 、 touchend 解耦如下:
根據(jù)上面的框架, 再詳細(xì)補(bǔ)充下代碼就可以了。 然后再在 touchend 事件中, 新增或減少 arraylist 的數(shù)據(jù)量。 九、性能優(yōu)化其實(shí), 做到上面的設(shè)計(jì),性能已經(jīng)很好了,可以監(jiān)控幀率來(lái)看下滑動(dòng)的流暢程度。 總結(jié)下我做了什么操作,來(lái)提高整體的性能: 1、分層渲染將K線圖畫(huà)在3個(gè)canvas上。
2、離屏渲染當(dāng)需要在K線上標(biāo)注一些icon時(shí), 這些 icon 可以先離屏渲染, 需要的時(shí)候, 再copy到變動(dòng)層對(duì)應(yīng)的位置,這樣比臨時(shí)抱佛腳去畫(huà),要省很多時(shí)間,也能提高新能。 3、設(shè)置數(shù)據(jù)緩沖區(qū)就是屏幕只渲染一屏數(shù)據(jù), 但是在當(dāng)前屏的左右兩邊,各緩存了2屏數(shù)據(jù), 超過(guò)5屏數(shù)據(jù)的時(shí)候,及時(shí)裁掉多余的數(shù)據(jù), 這樣arraylist的數(shù)據(jù)量始終保持在5屏, 控制了數(shù)據(jù)量,有效的控制了占用空間。 4、節(jié)流防抖touchmove 會(huì)很頻繁觸發(fā), 可通過(guò)節(jié)流來(lái)控制,減少不必要的渲染。 十、部署到GitHub Pages1、安裝gh-pages包
2、package.json 添加如下配置注意, Stock 這個(gè)需要對(duì)應(yīng)github的倉(cāng)庫(kù)名
3、運(yùn)行部署命令
最后, 訪問(wèn)上面的鏈接 這樣, github pages 部署成功, 訪問(wèn)上面鏈接, 可以看到如下效果。 github page 的部署需要將倉(cāng)庫(kù)設(shè)置為 public, 這個(gè)我挺反感的, 可以用 vercel 部署, 也就是將 github 賬號(hào)與 vercel 關(guān)聯(lián)起來(lái), 項(xiàng)目的 package.js 的 homepage 設(shè)置為 “.” , 然后 vercel 可以點(diǎn)擊一下, 一鍵部署, 常見(jiàn)的命令行如下:
轉(zhuǎn)自https://juejin.cn/post/7556154928059334666 該文章在 2025/10/10 11:31:22 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |