本文作者:IMWeb IMWeb团队 原文出处:IMWeb社区 未经同意,禁止转载
需求简介
腾讯企鹅辅导在学生上课结束后推送“学习报告”,是课程所提供的一项重要服务。家长在“学习报告”中能查看孩子上课时间及互动情况,答题及掌握知识点,作业考试分数,班级排名等诸多数据,继而让学生家长及时掌握孩子的学习情况。
此次改版升级是针对旧学习报告的的数据和展示进行的一次优化:增加考试模块、知识点采用更简单的表达形式、在视觉交互上更加年轻活泼、并运用了更多数据图表可视化在其数据展示中。
考试模块-数据图表可视化的应用
1.数据可视化组件库的选择及应用
在考试模块中,需要展示学生成绩变化趋势的曲线图,而这需要用到第三方的可视化组件库,继而快速回忆起比较知名的几款:国外的HighChart,百度家的Echart,阿里的AntV(移动端F2)等。当然也希望腾讯有一天也能有同样知名好用的的可视化组件库。
在选择可视化组件库时,我们主要考虑以下几点:1.能够良好支持移动端且轻量。2.支持React。3.具备足够自由的可定制化配置样式的能力。
其中第三点尤其重要,因为这里要准确还原交互视觉(不得不说我们交互和视觉给的要求很高)。根据经验,纵使强大的可视化组件库配置非常繁多,但往往可配置的内容太多,根本找不到用什么配置项达到目的,而且一些配置相互影响,变化很多。
最终我们发现并使用了Recharts。它是一个使用React和D3构建的Redefined图表库。具备以下特性:
- 支持React组件,声明式的标签,写图表和写 HTML 一样简单。
- 原生SVG支持,依赖于轻量级的 D3 子模块构建 SVG 元素。
- 接口式的 API,解决各种个性化的需求。
以下是部分需求代码,展示了其用法和特性:
代码语言:javascript复制<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的位置样式。
代码语言:javascript复制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前后添加伪元素,这样就不会妨碍轮播定位的计算了。
代码语言:javascript复制.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样式:
代码语言:javascript复制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(',');
代码语言:javascript复制-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;
只需要通过一个元素。