騰訊企鵝輔導(dǎo)在學(xué)生上課結(jié)束后推送“學(xué)習(xí)報(bào)告”,是課程所提供的一項(xiàng)重要服務(wù)。家長(zhǎng)在“學(xué)習(xí)報(bào)告”中能查看孩子上課時(shí)間及互動(dòng)情況,答題及掌握知識(shí)點(diǎn),作業(yè)考試分?jǐn)?shù),班級(jí)排名等諸多數(shù)據(jù),繼而讓學(xué)生家長(zhǎng)及時(shí)掌握孩子的學(xué)習(xí)情況。
此次改版升級(jí)是針對(duì)舊學(xué)習(xí)報(bào)告的的數(shù)據(jù)和展示進(jìn)行的一次優(yōu)化:增加考試模塊、知識(shí)點(diǎn)采用更簡(jiǎn)單的表達(dá)形式、在視覺(jué)交互上更加年輕活潑、并運(yùn)用了更多數(shù)據(jù)圖表可視化在其數(shù)據(jù)展示中。
在考試模塊中,需要展示學(xué)生成績(jī)變化趨勢(shì)的曲線圖,而這需要用到第三方的可視化組件庫(kù),繼而快速回憶起比較知名的幾款:國(guó)外的HighChart,百度家的Echart,阿里的AntV(移動(dòng)端F2)等。當(dāng)然也希望騰訊有一天也能有同樣知名好用的的可視化組件庫(kù)。
在選擇可視化組件庫(kù)時(shí),我們主要考慮以下幾點(diǎn):1.能夠良好支持移動(dòng)端且輕量。2.支持React。3.具備足夠自由的可定制化配置樣式的能力。
其中第三點(diǎn)尤其重要,因?yàn)檫@里要準(zhǔn)確還原交互視覺(jué)(不得不說(shuō)我們交互和視覺(jué)給的要求很高)。根據(jù)經(jīng)驗(yàn),縱使強(qiáng)大的可視化組件庫(kù)配置非常繁多,但往往可配置的內(nèi)容太多,根本找不到用什么配置項(xiàng)達(dá)到目的,而且一些配置相互影響,變化很多。
最終我們發(fā)現(xiàn)并使用了Recharts。它是一個(gè)使用React和D3構(gòu)建的Redefined圖表庫(kù)。具備以下特性:
支持React組件,聲明式的標(biāo)簽,寫(xiě)圖表和寫(xiě) HTML 一樣簡(jiǎn)單。
原生SVG支持,依賴(lài)于輕量級(jí)的 D3 子模塊構(gòu)建 SVG 元素。
接口式的 API,解決各種個(gè)性化的需求。
以下是部分需求代碼,展示了其用法和特性:
<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: '我的成績(jī)', avgScore: '班級(jí)平均分' }[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>
除了樣式配置項(xiàng)外,還提供了諸如“strokeDasharray”貼近原生SVG的配置項(xiàng)。對(duì)于熟悉SVG的同學(xué)就能能很準(zhǔn)確寫(xiě)圖形樣式了。
說(shuō)道貝塞爾曲線,前端的同學(xué)很容易想到的是CSS transition中的cubic-bezier,一般是起始點(diǎn)和兩個(gè)控制點(diǎn) 來(lái)生成兩點(diǎn)間的一條曲線,也就是常用三階貝塞爾曲線。關(guān)于貝塞爾曲線就不再贅述了,其原理和SVG中Path中貝塞爾曲線的使用,可查閱下面兩篇文章。貝塞爾曲線原理
SVG Path 曲線
OK,根據(jù)需求,我們考試成績(jī)已經(jīng)確定兩個(gè)點(diǎn)了,那么這根曲線到底具被怎樣的“性格”,彎一點(diǎn)還是平滑一點(diǎn)?但是這需要和視覺(jué)的同學(xué)反復(fù)調(diào)整得出一個(gè)讓她滿意的“參數(shù)”。當(dāng)然如果要做到完全滿意,可能還要針對(duì)不同情況計(jì)算不同的參數(shù)。
下面代碼為:通過(guò)D3 shape(可視化的圖形基元),除了終點(diǎn),兩個(gè)控制點(diǎn)的x值通過(guò)參數(shù)設(shè)置。將其實(shí)例作為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;//此為控制點(diǎn)位置參數(shù) 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; } },};
Scalable Vector Graphics,意思為可縮放的矢量圖形,它基于XML,是一種開(kāi)放標(biāo)準(zhǔn)的矢量圖形語(yǔ)言。recharts提供基于react組件的寫(xiě)法,去寫(xiě)可定制化svg圖形。比如下面:用組件svg 來(lái)定制的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> ); }}
在學(xué)習(xí)回顧模塊,用戶可以左右滑動(dòng)/點(diǎn)擊柱狀圖,來(lái)切換不同課程信息展示。很顯然可以通過(guò)一個(gè)輪播組件來(lái)實(shí)現(xiàn),但是這個(gè)模塊還具備柱狀圖的展示。要選擇一個(gè)兼具輪播和圖表的組件,還要保證兩者的功能和樣式都可按需求定制。顯然這并不容易,即便存在這樣組件也要花上不少時(shí)間去尋找和篩選。
這時(shí)就要權(quán)衡,到底是在一個(gè)輪播組件添加圖表,還是改造圖表組件為輪播。這里我選擇基于輪播組件來(lái)寫(xiě)里面的柱狀圖的這個(gè)方案。原因是:這里的柱狀圖并不復(fù)雜,可以用dom+css樣式來(lái)實(shí)現(xiàn),并且正好實(shí)現(xiàn)樣式定制化的需求。雖然圖表組件(比如antV的F2)也提供類(lèi)似滑動(dòng)圖表的功能,但是由于輪播不是它主要特性,諸如多item展示以及居中item選中等特性,改起來(lái)也不容易。
確定在輪播組件實(shí)現(xiàn)柱狀圖方案后,發(fā)現(xiàn)在實(shí)現(xiàn)仍有難點(diǎn):第一個(gè)item的左邊和最后一個(gè)item的右邊仍有虛線軸。最開(kāi)始想到通過(guò)添加空item來(lái)實(shí)現(xiàn),但實(shí)際需求是在滑至第一個(gè)和最后一個(gè)是不允許繼續(xù)滑動(dòng)的,所以不能直接添加空item。
那怎么辦呢?我們都知道輪播都是由視窗加container組成,通過(guò)計(jì)算定位container的位置來(lái)輪播。我不能在container里面直接添加DOM元素,否則會(huì)影響輪播組件的計(jì)算。但是我們可以在container前后添加偽元素,這樣就不會(huì)妨礙輪播定位的計(jì)算了。
.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);}
這里有個(gè)平時(shí)很少用到的background的都多背景方法,由于左右兩邊最多有兩根虛線展示,backgound設(shè)置兩個(gè)虛線圖片即可。
當(dāng)看到視覺(jué)稿 學(xué)生在線時(shí)間狀態(tài)條的時(shí)候,一眼看去ok完全沒(méi)有難度,不就一個(gè)簡(jiǎn)單的狀態(tài)條嗎,只不過(guò)不連續(xù)罷了。寫(xiě)個(gè)div,overflow-hidden,只需計(jì)算綠色塊的width值和left值即可,擼起袖子就是干,十分鐘搞定。
可是設(shè)計(jì)走查時(shí),卻逃不過(guò)視覺(jué)設(shè)計(jì)同學(xué)的火眼金睛:“這里的綠色應(yīng)該是覆蓋灰色邊框上面的!”接下來(lái)為了滿足視覺(jué)同學(xué)的要求可花費(fèi)了不少功夫。因?yàn)樵诰€狀態(tài)條及其相關(guān)計(jì)算已經(jīng)寫(xiě)好,最開(kāi)始沒(méi)有使用圖表組件,因?yàn)槲矣X(jué)得這很簡(jiǎn)單,不需要?dú)㈦u用牛刀,直接CSS可以實(shí)現(xiàn)。
在外面再套一層div,position設(shè)置為relative,設(shè)置圓角和overflow hidden,綠色塊相對(duì)于這一層div定位,如果溢出就會(huì)被裁剪。
css 有一個(gè) -webkit-mask 屬性。它所提供類(lèi)似于遮罩到的能力,讓原本CSS無(wú)法實(shí)現(xiàn)的shape通過(guò)圖片也能做到??戳讼旅孢@個(gè)圖就清楚了。
那么怎么應(yīng)用-webkit-mask來(lái)實(shí)現(xiàn)不連續(xù)的狀態(tài)條呢?其實(shí)只需要一個(gè)非透明的極小的png圖,計(jì)算好寬度以及位置,再進(jìn)行樣式設(shè)置即可。
這里的-webkit-mask和所有background的多背景圖使用是一樣的,需要注意的是,這里的第一個(gè)參數(shù)值不要把它誤會(huì)成是的x值,而是圖片的x%與容器x%的重合點(diǎn),這里很容易出錯(cuò)。以下是計(jì)算的代碼和生成的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;
只需要通過(guò)一個(gè)元素。
聯(lián)系客服