React使用Canvas繪制大數(shù)據(jù)表格的實(shí)例代碼
之前一直想用Canvas做表格渲染的,最近發(fā)現(xiàn)了一個(gè)很不錯(cuò)的Canvas繪圖框架Leafer,api很友好就試著寫了一下。
表格渲染主要分為四個(gè)部分,1、表頭渲染,2、表格渲染,3、滾動(dòng)條渲染,4、滾動(dòng)條與表格的聯(lián)動(dòng)。
1、表頭渲染
表頭的通過 JSON 格式來設(shè)置的,主要包括每列的名稱、對(duì)應(yīng)的數(shù)據(jù)的鍵值、寬度、是否需要對(duì)數(shù)據(jù)進(jìn)行二次渲染。
首先需要解決的是表頭的正確渲染,這里分為兩種情況:
1、表格列都沒有設(shè)置寬度
2、表格列有設(shè)置寬度
1.1、表格列都沒有設(shè)置寬度
1.1.1、計(jì)算表格每列的寬度
這里已知的是表格的寬度,表格的列數(shù)及表格列的名稱,解決方案如下:
文本與表格寬度比率 = 表格寬度 / 表格列文本總寬度
每列寬度 = 每列表格文本寬度 * 文本與表格寬度比率
獲取文本寬度方法:
const getTextWidth = (leafer: Leafer, text: string) => { return leafer.canvas.measureText(text).width; };
1.1.2、計(jì)算表格每列的開始坐標(biāo)
初始化表格列數(shù)據(jù)結(jié)構(gòu),給表格列添加 width 字符
const thList = columns.map((item) => { return { ...item, width: item.width ? item.width : Math.floor(getTextWidth(leafer, item.title) * widthRatio), }; });
循環(huán)遍歷表格列 渲染表頭
const group = new Group({ x, y, id: "tableHeader" }); thList.forEach((th, index) => { const midLength = thList.slice(0, index).reduce((acc, cur) => { return acc + cur.width; }, 0); const x = index === 0 ? 0 : midLength - index; const rect = new Rect({ x, y: 0, width: th.width, height: initParams.headerHeight, fill: "#417A77", stroke: "#b4c9fb", }); group.add(rect); const text = new Text({ x, y, width: th.width, textAlign: "center", height: headerHeight, verticalAlign: "middle", fill: "#000000", text: th.title, fontSize, }); group.add(text); });
到這里為止 表頭就可以正常渲染出來了
1.2、表格列有設(shè)置寬度
與沒有設(shè)置表格列的渲染類似
文本與表格寬度比率 = (表格寬度 - 表格設(shè)置列的總寬度) / 表格列文本總寬度
沒有設(shè)置寬度的列寬度 = 每列表格文本寬度 * 文本與表格寬度比率
const noSetWidthColWidth = columns.reduce((acc, cur) => { if (cur.width) { return ""; } return acc + cur.title; }, ""); const textWidth = getTextWidth(leafer, noSetWidthColWidth); const setColWidthSum = columns.reduce((acc, cur) => { if (cur.width) { return acc + cur.width; } return acc; }, 0); const widthRatio = (width - setColWidthSum) / textWidth;
渲染方式同上,最后掛載到 leafer 中完成渲染
2、滾動(dòng)條渲染
在表格渲染之前要先解決表格滾動(dòng)條和表格聯(lián)動(dòng)的問題,根據(jù)滾動(dòng)條滾動(dòng)的距離計(jì)算表格顯示的內(nèi)容,因?yàn)槭亲岳L制表格,所以滾動(dòng)條部分不能利用瀏覽器的滾動(dòng)條。
2.1、創(chuàng)建滾動(dòng)條
滾動(dòng)條的本質(zhì)還是一個(gè) Rect,使 Rect 模擬滾動(dòng)條的行為。
const rect = new Rect({ x: width - scrollBar.width, y: initParams.headerHeight, width: scrollBar.width - scrollBar.margin * 2, height: scrollBar.height, fill: "rgba(133,117,85, 0.8)", cornerRadius: 10, id: "scrollBar", zIndex: scrollBar.zIndex, });
2.2、計(jì)算滾動(dòng)條的高度、位置、樣式
2.2.1、計(jì)算滾動(dòng)條的高度
根據(jù)數(shù)據(jù)量的大小,需要調(diào)整滾動(dòng)條渲染的高度,計(jì)算方式如下:
每條數(shù)據(jù)對(duì)應(yīng)滾動(dòng)條高度 = (表格總高度 - 表頭高度) / 數(shù)據(jù)長(zhǎng)度
滾動(dòng)條高度 = 滾動(dòng)條最小高度 + 視圖內(nèi)顯示行數(shù) * 每條數(shù)據(jù)對(duì)應(yīng)滾動(dòng)條高度
const computedScrollBarHeight = ( leafer: Leafer, dataSource: Record<string, string>[], jumpIndex = 0 ) => { const { height } = leafer; const { viewHeight, viewCapacity } = getViewInfo(leafer); const unitLength = (height - initParams.headerHeight) / dataSource.length; if (jumpIndex) { return initParams.scrollBar.height; } const targetHeight = initParams.scrollBar.height + viewCapacity * unitLength; // 小數(shù)據(jù)量做臨時(shí)處理 return targetHeight < viewHeight ? Math.ceil(targetHeight) : viewHeight - 10; };
2.2.2、滾動(dòng)條的位置
滾動(dòng)條的 X 軸位置 = 表格的寬度 - 滾動(dòng)條區(qū)域的寬度
滾動(dòng)條的 Y 軸滾動(dòng)需要添加鼠標(biāo)滾輪和拖拽事件的監(jiān)聽,對(duì)滾動(dòng)條拖拽事件的監(jiān)聽是通過監(jiān)聽滾動(dòng)條本身,鼠標(biāo)滾輪的監(jiān)聽需要對(duì)表格本身添加監(jiān)聽事件
leafer.on(MoveEvent.MOVE, function (e) { setScroll(leafer, rect, e, dataSource, -0.1, scrollParams); }); rect.on(DragEvent.DRAG, function (e) { setScroll(leafer, rect, e, dataSource, 1, scrollParams); });
滾動(dòng)條的最大滾動(dòng)高度 = 表格的高度 - 滾動(dòng)條高度
滾動(dòng)條的渲染是從設(shè)置的坐標(biāo)點(diǎn)開始 + 滾動(dòng)條的高度,保證滾動(dòng)條在可視區(qū)域內(nèi),需要減去滾動(dòng)條的高度。
當(dāng)滾動(dòng)或拖拽計(jì)算值超過最大高度時(shí),為最大高度;當(dāng)滾動(dòng)或拖拽計(jì)算值小于表頭高度時(shí),為表頭高度,其他情況為滾動(dòng)條在 Y 軸方向的偏移值 + 鼠標(biāo)滾輪滾動(dòng)的距離或拖拽的距離
const setScroll = ( leafer: Leafer, rect: Rect, e: MoveEvent | DragEvent, dataSource: Record<string, string>[], val = 1, scrollInfo: ScrollInfo ) => { const { scrollMaxHeight, headerHeight, height, scrollBar, viewCapacity, unitLength, } = scrollInfo; leafer.children = leafer.children.filter((item) => fixedGroup.includes(item.id ?? "") ); /** * 鼠標(biāo)滾輪的滾動(dòng)向上滾動(dòng)是正值,向下是負(fù)值 * 這與滾動(dòng)條位置是相反的,需要在獲取滾動(dòng)距離時(shí) * -1 * */ rect.y = rect.y + e.moveY * val >= scrollMaxHeight ? scrollMaxHeight : rect.y + e.moveY * val < headerHeight ? headerHeight : rect.y + e.moveY * val; };
2.2.3、滾動(dòng)條的樣式
滾動(dòng)條 = 滾動(dòng)條本身 + 左右邊距
滾動(dòng)條本身寬度 = 滾動(dòng)條寬度 - 邊距 * 2
鼠標(biāo)移入移出滾動(dòng)條時(shí)會(huì)有顯隱效果,通過對(duì) Rect 添加移入移出事件來修改透明度
rect.on(PointerEvent.ENTER, (e) => { e.target.fill = "rgba(133,117,85, 1)"; }); rect.on(PointerEvent.LEAVE, (e) => { e.target.fill = "rgba(133,117,85, 0.8)"; });
2.3、滾動(dòng)條是否顯示
當(dāng)數(shù)據(jù)長(zhǎng)度小于可視區(qū)域內(nèi)的行數(shù)時(shí),此時(shí)不需要出滾動(dòng)條,在初始化表格調(diào)用滾動(dòng)條方法添加判斷。
export const drawCanvasTable = ( leafer: Leafer, columns: Column[], dataSource: Record<string, string>[], jumpIndex = 0 ) => { // ... dataSource.length > viewCapacity && initScrollBar(leafer, dataSource, jumpIndex); };
3、表格渲染
3.1、初始化渲染
表格的渲染類似于表頭的渲染,表格的渲染是按照行來渲染,每行的列坐標(biāo)、寬度是和表頭一樣的,可以在表格渲染的部分保存一份。
thList.forEach((th, index) => { const midLength = thList.slice(0, index).reduce((acc, cur) => { return acc + cur.width; }, 0); const x = index === 0 ? 0 : midLength - index; tableHeaderInfo[th.dataIndex] = { x, width: th.width, }; // ...省略渲染部分... });
3.2、獲取渲染的范圍
表格渲染內(nèi)容的起始位置是通過滾動(dòng)條位置來計(jì)算的,并通過滾動(dòng)條位置的變化來重新渲染表格。
滾動(dòng)距離等于最大滾動(dòng)距離時(shí),渲染的起始位置為數(shù)據(jù)總長(zhǎng)度 - 視圖可顯示的行數(shù)。
滾動(dòng)距離小于最大滾動(dòng)距離時(shí):
滾動(dòng)條偏移范圍內(nèi)需要渲染的數(shù)據(jù)單位長(zhǎng)度 = (表格高度 - 表格頭高度 - 滾動(dòng)條高度) / (數(shù)據(jù)長(zhǎng)度 - 視圖可顯示行數(shù))
渲染的起始位置 = (滾動(dòng)條位置 - 表格頭高度) / 滾動(dòng)條偏移范圍內(nèi)需要渲染的數(shù)據(jù)單位長(zhǎng)度
const setScroll = ( leafer: Leafer, rect: Rect, e: MoveEvent | DragEvent, dataSource: Record<string, string>[], val = 1, scrollInfo: ScrollInfo ) => { // ...計(jì)算滾動(dòng)條位置代碼... from = rect.y === scrollMaxHeight ? dataSource.length - viewCapacity : Math.ceil((rect.y - headerHeight) / unitLength); initTableBody(); };
當(dāng)數(shù)據(jù)大于表格視圖行數(shù)時(shí),表格結(jié)束范圍 = 起始位置 + 表格視圖可以顯示的最大行數(shù),如果計(jì)算值大于數(shù)據(jù)最大長(zhǎng)度,則為數(shù)據(jù)長(zhǎng)度,否則表格的結(jié)束范圍 = 數(shù)據(jù)長(zhǎng)度
const computedViewBoundary = ( i: number, start: number, viewCapacity: number, dataSource: Record<string, string>[] ) => { if (dataSource.length > viewCapacity) { return (i < start + viewCapacity && start + viewCapacity <= dataSource.length); } else { return i < dataSource.length; } };
3.3、計(jì)算表格 Y 軸方向的偏移
因?yàn)橛?jì)算視圖內(nèi)可以顯示的表格行時(shí),會(huì)有小數(shù)的存在,這里是采用向下取整,這樣顯示的行數(shù)總高度會(huì)超過表格的可視區(qū)域高度,這時(shí)候需要對(duì)表格進(jìn)行部分偏移,使其在滾動(dòng)到底部時(shí)能夠正常顯示
- 1、數(shù)據(jù)長(zhǎng)度小于可視區(qū)域行數(shù)時(shí),不需要偏移
- 2、數(shù)據(jù)長(zhǎng)度大于可視區(qū)域行數(shù)時(shí)
- 2.1 開始位置小于需要隨滾動(dòng)條渲染的數(shù)據(jù)長(zhǎng)度時(shí),不需要偏移
- 2.2 開始位置大于等于需要隨滾動(dòng)條渲染的數(shù)據(jù)長(zhǎng)度時(shí):
- 2.2.1 如果表格行高可以被視圖高度整除,不需要偏移
- 2.2.2 如果表格行高不可以被視圖高度整除,偏移值 = 表格頭高度 - (表格行高 - 視圖高度 % 行高)
const computedTableOffset = ( viewCapacity: number, headerHeight: number, viewHeight: number, rowHeight: number ) => { return globalDataSource.length > viewCapacity ? from < Math.floor(globalDataSource.length - viewCapacity) ? headerHeight : headerHeight - (viewHeight % rowHeight ? rowHeight - (viewHeight % rowHeight) : 0) : headerHeight; };
4、跳轉(zhuǎn)到指定位置
當(dāng)表格數(shù)據(jù)量大時(shí),需要能夠快速定位到某條數(shù)據(jù),當(dāng)接收到需要跳轉(zhuǎn)到的行時(shí),該數(shù)據(jù)為起始位置,重新執(zhí)行渲染表格一系列方法,因?yàn)樵跐L動(dòng)條初始化時(shí)修改了滾動(dòng)條的初始高度,所以在跳轉(zhuǎn)操作時(shí)不應(yīng)該修改表格行的高度
useEffect(() => { if (canvasDom.current) { const leafer = new Leafer({ view: canvasDom.current, width: 500, height: 800, move: { dragOut: false }, type: "user", }); drawCanvasTable(leafer, columns, dataSource, jumpIndex); } }, [columns, dataSource, jumpIndex]);
if (jumpIndex) { return initParams.scrollBar.height; }
代碼地址:
https://stackblitz.com/edit/vitejs-vite-emryft?file=src%2FApp.tsx
以上就是React使用Canvas繪制大數(shù)據(jù)表格的詳細(xì)內(nèi)容,更多關(guān)于React Canvas繪制表格的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在Ant Design Pro登錄功能中集成圖形驗(yàn)證碼組件的方法步驟
這篇文章主要介紹了在Ant Design Pro登錄功能中集成圖形驗(yàn)證碼組件的方法步驟,這里的登錄功能其實(shí)就是一個(gè)表單提交,實(shí)現(xiàn)起來也很簡(jiǎn)單,具體實(shí)例代碼跟隨小編一起看看吧2021-05-05使用react-beautiful-dnd實(shí)現(xiàn)列表間拖拽踩坑
相比于react-dnd,react-beautiful-dnd更適用于列表之間拖拽的場(chǎng)景,本文主要介紹了使用react-beautiful-dnd實(shí)現(xiàn)列表間拖拽踩坑,感興趣的可以了解一下2021-05-05React特征學(xué)習(xí)Form數(shù)據(jù)管理示例詳解
這篇文章主要為大家介紹了React特征學(xué)習(xí)Form數(shù)據(jù)管理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09react拖拽組件react-sortable-hoc的使用
本文主要介紹了react拖拽組件react-sortable-hoc的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02淺談react.js中實(shí)現(xiàn)tab吸頂效果的問題
下面小編就為大家?guī)硪黄獪\談react.js中實(shí)現(xiàn)tab吸頂效果的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09React使用setState更新數(shù)組的方法示例(追加新數(shù)據(jù))
在?React?中,setState?是管理組件狀態(tài)的核心方法之一,然而,當(dāng)我們需要更新狀態(tài)中的數(shù)組時(shí),如何高效且安全地操作變得尤為關(guān)鍵,本文將詳細(xì)解析以下代碼的實(shí)現(xiàn)邏輯,幫助你掌握在?React?中追加數(shù)組數(shù)據(jù)的最佳實(shí)踐,需要的朋友可以參考下2025-03-03