前端圖表可視化的應用實踐總結
- 2019 年 12 月 4 日
- 筆記
本文作者:IMWeb IMWeb團隊 原文出處:IMWeb社區 未經同意,禁止轉載
需求簡介
騰訊企鵝輔導在學生上課結束後推送「學習報告」,是課程所提供的一項重要服務。家長在「學習報告」中能查看孩子上課時間及互動情況,答題及掌握知識點,作業考試分數,班級排名等諸多數據,繼而讓學生家長及時掌握孩子的學習情況。
此次改版升級是針對舊學習報告的的數據和展示進行的一次優化:增加考試模組、知識點採用更簡單的表達形式、在視覺交互上更加年輕活潑、並運用了更多數據圖表可視化在其數據展示中。

考試模組-數據圖表可視化的應用
1.數據可視化組件庫的選擇及應用
在考試模組中,需要展示學生成績變化趨勢的曲線圖,而這需要用到第三方的可視化組件庫,繼而快速回憶起比較知名的幾款:國外的HighChart,百度家的Echart,阿里的AntV(移動端F2)等。當然也希望騰訊有一天也能有同樣知名好用的的可視化組件庫。
在選擇可視化組件庫時,我們主要考慮以下幾點:1.能夠良好支援移動端且輕量。2.支援React。3.具備足夠自由的可訂製化配置樣式的能力。
其中第三點尤其重要,因為這裡要準確還原交互視覺(不得不說我們交互和視覺給的要求很高)。根據經驗,縱使強大的可視化組件庫配置非常繁多,但往往可配置的內容太多,根本找不到用什麼配置項達到目的,而且一些配置相互影響,變化很多。
最終我們發現並使用了Recharts。它是一個使用React和D3構建的Redefined圖表庫。具備以下特性:
- 支援React組件,聲明式的標籤,寫圖表和寫 HTML 一樣簡單。
- 原生SVG支援,依賴於輕量級的 D3 子模組構建 SVG 元素。
- 介面式的 API,解決各種個性化的需求。
以下是部分需求程式碼,展示了其用法和特性:
<ResponsiveContainer width="100%" height={200}> <LineChart data={data}> <CartesianGrid horizontal={false} strokeDasharray="2 3" /> <Line type={lineStyle} dataKey="avgScore" stroke="#CCCCCC" fill="#CCCCCC" label={ <CustomizedLabel direction="down" data={data} relateKey="actualScore"/> } isAnimationActive={false} /> <Line type={lineStyle} dataKey="actualScore" stroke="#08CB6A" fill="#08CB6A" label={<CustomizedLabel data={data} relateKey="avgScore" />} isAnimationActive={false} /> <Legend align="left" verticalAlign="top" iconSize={4} iconType="rect" height={36} formatter={(value) => { return { actualScore: '我的成績', avgScore: '班級平均分' }[value]; }} wrapperStyle={{ left: -13, fontSize: 12, }} /> <XAxis dataKey="name" padding={{ left: padding, right: padding }} axisLine={false} tickLine={false} /> <YAxis domain={[-8, 108]} hide /> </LineChart> </ResponsiveContainer>除了樣式配置項外,還提供了諸如「strokeDasharray」貼近原生SVG的配置項。對於熟悉SVG的同學就能能很準確寫圖形樣式了。
2. 如何畫好一根曲線[貝塞爾曲線]
說道貝塞爾曲線,前端的同學很容易想到的是CSS transition中的cubic-bezier,一般是起始點和兩個控制點 來生成兩點間的一條曲線,也就是常用三階貝塞爾曲線。 關於貝塞爾曲線就不再贅述了,其原理和SVG中Path中貝塞爾曲線的使用,可查閱下面兩篇文章。 貝塞爾曲線原理 SVG Path 曲線

OK,根據需求,我們考試成績已經確定兩個點了,那麼這根曲線到底具被怎樣的「性格」,彎一點還是平滑一點?但是這需要和視覺的同學反覆調整得出一個讓她滿意的「參數」。當然如果要做到完全滿意,可能還要針對不同情況計算不同的參數。
下面程式碼為:通過D3 shape(可視化的圖形基元),除了終點,兩個控制點的x值通過參數設置。將其實例作為props 的type值傳入Recharts中的<Line/>中,即可得到想要的曲線。
BezierLineShape.prototype = { lineStart() { this._x0 = this._x1 = this._y0 = this._y1 = this._t0 = NaN; this._point = 0; console.log('lineStart', this._line, this._point); }, lineEnd() { console.log('lineEnd', this._line, this._point); }, point(x, y) { console.log('point', x, y, this._line); (x = +x), (y = +y); if (x === this._x1 && y === this._y1) { return; } switch (this._point) { case 0: { this._point = 1; this._x1 = x; this._y1 = y; this._context.moveTo(x, y); break; } case 1: { const mint = (x - this._x1) * 0.35;//此為控制點位置參數 const x1 = this._x1 + mint; const y1 = this._y1; const x2 = x - mint; const y2 = y; this._x1 = x; this._y1 = y; console.log('bezierCurveTo', x1, y1, x2, y2, x, y); this._context.bezierCurveTo(x1, y1, x2, y2, x, y); break; } default: break; } }, };最後效果圖:

基於SVG做的客制化修改
Scalable Vector Graphics,意思為可縮放的矢量圖形,它基於XML,是一種開放標準的矢量圖形語言。recharts提供基於react組件的寫法,去寫可訂製化svg圖形。比如下面:用組件svg 來訂製的Label的位置樣式。
export default class CustomizedLabel extends React.PureComponent { static defaultProps = { direction: 'up', // stroke: '#777', }; render() { const { x, y, stroke, value, direction, index, relateKey, data } = this.props; let settedDirect = direction; try { const relateValue = data[index][relateKey]; if (value > relateValue) { settedDirect = 'up'; } else if (value < relateValue) { settedDirect = 'down'; } } catch (e) { // BJ_Report } const dy = settedDirect === 'up' ? -10 : 18; return ( <text x={x} y={y} dy={dy} fill={stroke} fontSize={14} textAnchor="middle"> {value} </text> ); } }
學習回顧-輪播柱狀圖結合實現
在學習回顧模組,用戶可以左右滑動/點擊柱狀圖,來切換不同課程資訊展示。 很顯然可以通過一個輪播組件來實現,但是這個模組還具備柱狀圖的展示。要選擇一個兼具輪播和圖表的組件,還要保證兩者的功能和樣式都可按需求訂製。顯然這並不容易,即便存在這樣組件也要花上不少時間去尋找和篩選。

這時就要權衡,到底是在一個輪播組件添加圖表,還是改造圖表組件為輪播。這裡我選擇基於輪播組件來寫裡面的柱狀圖的這個方案。原因是:這裡的柱狀圖並不複雜,可以用dom+css樣式來實現,並且正好實現樣式訂製化的需求。雖然圖表組件(比如antV的F2)也提供類似滑動圖表的功能,但是由於輪播不是它主要特性,諸如多item展示以及居中item選中等特性,改起來也不容易。
確定在輪播組件實現柱狀圖方案後,發現在實現仍有難點:第一個item的左邊和最後一個item的右邊仍有虛線軸。最開始想到通過添加空item來實現,但實際需求是在滑至第一個和最後一個是不允許繼續滑動的,所以不能直接添加空item。

那怎麼辦呢?我們都知道輪播都是由視窗加container組成,通過計算定位container的位置來輪播。我不能在container裡面直接添加DOM元素,否則會影響輪播組件的計算。但是我們可以在container前後添加偽元素,這樣就不會妨礙輪播定位的計算了。
.time-chart-item:nth-of-type(1):after { content: ''; width: pxToRem(116); height: pxToRem(144); display: block; position: absolute; background: url(./img/dashline.png) pxToRem(29) 0 no-repeat, url(./img/dashline.png) pxToRem(29+58) 0 no-repeat; top: pxToRem(28); left: pxToRem(-116); }這裡有個平時很少用到的background的都多背景方法,由於左右兩邊最多有兩根虛線展示,backgound設置兩個虛線圖片即可。
本次上課-如何用CSS mask實現狀態條
當看到視覺稿 學生在線時間狀態條的時候,一眼看去ok完全沒有難度,不就一個簡單的狀態條嗎,只不過不連續罷了。寫個div,overflow-hidden,只需計算綠色塊的width值和left值即可,擼起袖子就是干,十分鐘搞定。

可是設計走查時,卻逃不過視覺設計同學的火眼金睛:「這裡的綠色應該是覆蓋灰色邊框上面的!」 接下來為了滿足視覺同學的要求可花費了不少功夫。 因為在線狀態條及其相關計算已經寫好,最開始沒有使用圖表組件,因為我覺得這很簡單,不需要殺雞用牛刀,直接CSS可以實現。

寫來改寫程式碼,為了讓綠色在線條覆蓋背景border,我將綠色狀態條覆蓋在上層,但這又出現另外一個問題。 綠色條塊左右兩側由於不被父級overflowhiden遮住,在值未達到極值時,無法做到圓角轉直線的效果。
傳統的辦法
在外面再套一層div,position設置為relative,設置圓角和overflow hidden,綠色塊相對於這一層div定位,如果溢出就會被裁剪。
css遮罩
css 有一個 -webkit-mask 屬性。它所提供類似於遮罩到的能力,讓原本CSS無法實現的shape通過圖片也能做到。看了下面這個圖就清楚了。

那麼怎麼應用-webkit-mask來實現不連續的狀態條呢?其實只需要一個非透明的極小的png圖,計算好寬度以及位置,再進行樣式設置即可。
這裡的-webkit-mask和所有background的多背景圖使用是一樣的,需要注意的是,這裡的第一個參數值不要把它誤會成是的x值,而是圖片的x%與容器x%的重合點,這裡很容易出錯。 以下是計算的程式碼和生成的css樣式:
const maskArray = []; InClassState.map((item) => { const { start_inclass: start, end_inclass: end } = item; const left = (start - lessonBeginTime) / allTime; const width = (end - start) / allTime; const maskLeft = left / (1 - width); maskArray.push(`url(${maskimg}) no-repeat ${maskLeft.toFixed(2) * 100}% 0/ ${width * 100}% 100%`); }); style.WebkitMask = maskArray.join(',');-webkit-mask: url(//fudao.qq.com/block_0bb81cb….png) 0% 0px / 60% 100% no-repeat, url(//fudao.qq.com/block_0bb81cb….png) 76% 0px / 15% 100% no-repeat, url(//fudao.qq.com/block_0bb81cb….png) 106% 0px / 15% 100% no-repeat;
只需要通過一個元素。

