Building Interactive Exam Charts with Recharts, SVG and CSS Mask in React
This article details how to redesign a learning report by selecting a lightweight React‑compatible visualization library, implementing exam score line charts with Recharts, customizing SVG labels and Bezier curves, creating a carousel bar chart, and using CSS‑mask techniques to render non‑continuous status bars.
Demand Overview
Penguin Tutor pushes a "Learning Report" after each class, allowing parents to view attendance, interaction, quiz results, homework scores, class ranking and other data. The redesign optimizes data and presentation by adding an exam module, simplifying knowledge point expressions, using a younger visual style, and incorporating more data visualizations.
Exam Module – Data Visualization
1. Choosing a Visualization Library
To display student score trends, a third‑party chart library is needed. Options considered include HighChart, ECharts, and AntV (F2). The selection criteria were mobile support, lightweight, React compatibility, and high customizability.
Recharts was chosen because it is built with React and D3, supports declarative components, native SVG, and an API that handles personalized requirements.
Supports React components with simple declarative syntax.
Native SVG support via lightweight D3 sub‑modules.
API allows extensive customization.
Below is a sample usage of Recharts:
<code><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 data={data} relateKey="avgScore"/>}>
<Line type={lineStyle} dataKey="actualScore" stroke="#08CB6A" fill="#08CB6A" label={<CustomizedLabel data={data} relateKey="actualScore"/>}>
<Legend align="left" verticalAlign="top" iconSize={4} iconType="rect"/>
<XAxis dataKey="name" padding={{left: padding, right: padding}} axisLine={false} tickLine={false}/>
<YAxis domain={[-8,108]} hide/>
</LineChart>
</ResponsiveContainer></code>Additional SVG‑like configuration options such as
strokeDasharraymake it easy for developers familiar with SVG to fine‑tune styles.
2. Drawing a Bezier Curve
To create a smooth curve between two points, a custom
BezierLineShapeextends D3’s shape utilities. Control points are calculated based on parameters, and the curve is rendered using
bezierCurveToon the canvas context.
<code>BezierLineShape.prototype = {
lineStart() { /* set initial points */ },
lineEnd() { /* set end points */ },
point(x, y) { /* calculate control points and draw */ }
};</code>A
CustomizedLabelcomponent is defined to render SVG text labels with dynamic direction and styling based on data values.
<code>export default class CustomizedLabel extends React.PureComponent {
static defaultProps = { direction: 'up', stroke: '#777' };
render() {
const { x, y, value, direction } = this.props;
const dy = direction === 'up' ? -10 : 18;
return <text x={x} y={y} dy={dy} fill={stroke} fontSize={14} textAnchor="middle">{value}</text>;
}
}</code>Final effect:
Learning Review – Carousel Bar Chart
The learning review module allows horizontal swipe or click on bar charts to switch courses. A carousel component was chosen to host simple DOM‑based bar charts, avoiding complex chart libraries while meeting customization needs.
To handle edge cases where the first and last items should not show extra dashed axes, pseudo‑elements are added before and after the carousel container, preserving layout calculations.
<code>.time-chart-item:nth-of-type(1)::after { content: ''; width: pxToRem(116); height: pxToRem(144); position: absolute; background: url(./img/dashline.png) pxToRem(29) no-repeat; top: pxToRem(28); left: -pxToRem(116); }</code>This Class – CSS Mask for Status Bar
A simple status bar shows online time. To make the green bar overlay the gray border and handle non‑continuous segments, the
-webkit-maskproperty is used with a tiny PNG mask and calculated positions.
<code>const maskArray = [];
InClassState.map(item => {
const { start, 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)}% 0%`);
maskArray.push(`${width * 100}% 0%`);
});
style.WebkitMask = maskArray.join(',');</code>This technique replaces multiple layered divs with a single element, achieving the desired visual effect efficiently.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.