Author: IMWeb Team. Reprint without permission is prohibited.


Recharts is a class library for chart processing. It uses the React feature to redefine the configuration and combination of charts, greatly improving the flexibility of chart customization. This article documents the development of custom style charts using Recharts in conjunction with SVG.

background

ABCmouse School edition provides teachers with a module that provides feedback on a child’s learning performance, some of which is presented visually in a graphic format.

Visual draft

This also moves into the realm of data visualization. This is an area that is too detailed to be considered by one person, and relies on third-party component libraries. Combined with this requirement, there are two main considerations in the selection of data visualization component library:

  1. Support the React

  2. Flexible custom styles are supported

After some research, Recharts[1] was selected to realize the above charts.

1. About Recharts

Recharts is a library for processing charts, and re stands for “redi25”, besides “React”, redefining the way the elements of a chart are combined and configured. It is built on React and D3 and has the following features:

  1. Declarative tags make writing charts as easy as writing HTML

  2. Configuration items that are close to native SVG make configuration items more natural

  3. Interface API, to solve a variety of personalized needs

Here is an example of the output. The Code for Recharts is also very concise, avoiding the extra burden of learning a new set of configurations and apis.

<BarChart width={520} height={280} data={data}>  <XAxis    dataKey="scene" tickLine={false}    axisLine={{ stroke: "#5dc1fb" }} tick={{ fill: "# 999" }}  />  <Bar    dataKey="time"isAnimationActive={! isEmpty} fill="#8884d8" barSize={32}    shape={<CustomBar />}    label={<CustomLabel />}  >    {data.map((entry, index) => (      <Cell key={index.toString()} />    ))}  </Bar></BarChart>Copy the code

It can be said that each pain point is hit by it. For more detailed introduction, please refer to the author’s introduction article: Componentized Visual Charts – Recharts[2].

In the following part of this paper, record the detailed problems and implementation process of using it in the realization of pie charts and bar charts.

2. Pie chart implementation

Custom bar chart

As shown in the figure, the circle part of the PieChart here uses the PieChart component, while the text and legend in the middle are rendered directly using HTML without relying on Recharts.

To give you a sense of Recharts, here’s a quick look at how Recharts implements enlarged circles, guides, and labels.

2.1 Partially enlarge the ring

The Pie component provided by Recharts implements the basic circle part. If you need to customize the color, pass in the color of each pie chart through the Cell component.

<PieChart width={480} height={400}>  <Pie data={data} dataKey="value"    cx={200} cy={200}    innerRadius={58} outerRadius={80} paddingAngle={0}    fill="#a08bff" stroke="none"  >    {data.map((entry, index) => (      <Cell key={`cell-${index}`} fill={entry.color} />    ))}  </Pie></PieChart>Copy the code

Get the ring:

Next, it is necessary to realize the effect of amplifying the corresponding Sector with a mouse in Hover state, and then displaying the dotted guide line and label.

To implement the Hover state for a Sector, provides an ActiveShape property to which a custom React component can be added. It then passes in an activeIndex to indicate which copy needs to be rendered again, and an onMouseEnter to update the activeIndex.

<Pie  activeIndex={this.state.activeIndex}  activeShape={renderActiveShape}  data={data} dataKey="value" cx={200} cy={200}  innerRadius={58} outerRadius={80} paddingAngle={0}  fill="#a08bff" stroke="none"  onMouseEnter={this.onPieEnter}>  {data.map((entry, index) => (    <Cell key={`cell-${index}`} fill={entry.color} />  ))}</Pie>Copy the code

RenderActiveShape implementation first returns a Sector with a smaller inner diameter and a larger outer diameter. Fill the Sector component with the information returned by the Render function. Cx and cy are the coordinates of the center of the circle corresponding to the Sector.

functionrenderActiveShape(props) { const innerOffset = 2; // Inner const outerOffset = 4; Const {cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill} = props;return( <Sector cx={cx} cy={cy} innerRadius={innerRadius - innerOffset} outerRadius={outerRadius + outerOffset} startAngle={startAngle} endAngle={endAngle} fill={fill} /> ); }Copy the code

Complete the circle part of the zoom effect:

2.2 Implementing boot Cables and Labels

I searched the document of Recharts and did not find the component of the bootline. The bootline of the example on the official website is a piece of code with embedded SVG elements. The author had not studied SVG graphics carefully before making this requirement. What to do? To learn!

I started a wave of Internet surfing and found the SVG tutorial of MDN [4]. After going through it, I had a basic impression. The element is used in the bootline implementation.

2.2.1 About Elements

The element provides a property called d, which stands for “path Data” and contains all the Data for a path in the form of a series of commands and the sequence of arguments required by the command. Commands and parameters are separated by white space characters.

A quick review of the documentation covers basic commands and accepted parameters:

M x y brush move to (x, y) as the starting point L x y Draw a line to (x, y)H x horizontally crossed to x xV y horizontally crossed to y y Close path back to the starting point (used to create a shape)Copy the code

It can also draw Bezier curves and arcs using the command below:

C x1 y1, x2 y2, x y Cubic Becselle curve Q x1 y1, x y quadratic Becselle curve A rx ry x-axis-rotation large-arc-flag sweep-flag x yCopy the code

For the D attribute, the commands covered in this article are already listed and will not be repeated here.

also provides stroke and fill attributes, which correspond to the color of the border and fill respectively. Path is essentially a shape formed by a closed path. The graph we draw is essentially a border, so stroke is also required for color Settings. For details, please refer to MDN’s introduction of Stroke and Fill [5].

Design students need a dashed guide line, and SVG provides stroke-Dasharray to implement this requirement, which accepts a set of comma-separated numbers that represent a combination of line length and blank length.

Here, the raw materials needed to draw graphics are basically combed out.

2.2.2 Generating Path Data

Our goal was to output a Sector + bootstrap + Label in renderShapeData, generating the corresponding drawing data D by receiving input originally given only to the Sector. The observation is that we need a fold that goes out a little bit and then bends horizontally. That is to say we need to identify a starting point, a point of reference that is deflected in the middle, and a final destination. With the color style of the border, we get the following code. (This is how the renderActiveShape example on the official website was implemented, and what I have done here is to understand and modify it.)

<path  d={`M${sx}.${sy}      L${mx}.${my}      L${ex}.${ey}`}  stroke={fill}  strokeDasharray="1, 3"  fill="none"/>Copy the code

To determine the coordinates of the three points, we need to determine the meaning of the props properties when rendering activeShape.

const {  cx, cy, innerRadius, outerRadius, startAngle, endAngle, midAngle,  fill, value, name} = props;Copy the code

The meanings of parameters such as center coordinates, Angle and radius involved are shown in the figure below:

Isn’t this the “right triangle” I learned in junior high school? The coordinates of the three points can be quickly calculated separately by using trigonometric functions.

Now turn all of this into code. Need to consider the Angle radian conversion, direction and other issues.

const RADIAN = Math.PI / 180; const innerOffset = 2; // Inner const outerOffset = 4; Const {cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, value, name,} = props; const {cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, value, name,} = props; const sin = Math.sin(-RADIAN * midAngle); const cos = Math.cos(-RADIAN * midAngle); const sx = cx + (outerRadius - innerOffset) * cos; const sy = cy + (outerRadius + outerOffset) * sin; const mx = cx + (outerRadius + outerOffset + 30) * cos; const my = cy + (outerRadius + outerOffset + 35) * sin; const ex = mx + (cos >= 0 ? 1: -1) * 80; const ey = my;Copy the code

At this point we render the desired bootline:

2.2.3 Generation of label

This step is relatively simple, just use the

element of SVG, use (ex, ey) as the starting coordinate of the text, and consider textAnchor to ensure alignment.

Final pie chart effect.

3. Bar chart implementation

The bar chart

Here, we need to make such a bar chart, involving two elements: the X-axis, a series of columns, and a React component each.

<BarChart width={520} height={280} data={data}>  <XAxis    dataKey="scene" tickLine={false}    axisLine={{ stroke: "#5dc1fb" }}    tick={{ fill: "# 999" }}  />  <Bar dataKey="time" fill="#8884d8" barSize={32} /></BarChart>Copy the code

The following results are obtained:

At this point, we’re still short of the bar chart’s labels, gradients, and rounded corners at the top.

3.1 Implementation of gradient

First of all, we solved the problem of gradients. We searched the MDN document about gradients [6] and found that the implementation is actually very simple. Just insert a node into the < DEFS > element. Then set the fill attribute of the element to which the gradient should be applied to the URL (the id attribute of the # gradient node).

The Recharts document does not mention the

element, see that all gradients, CSS, etc. in SVG are grouped in

at the beginning of the file. Imagination: if I write

directly inside the component, can it appear in the final generated < SVG >? Tried to write down, really! It shows that the imagination works.


Look, the JSX code with the gradient is still simple:

<BarChart  width={520}  height={280}  data={data}>  <defs>    <linearGradient x1="0" x2="0" y1="0" y2="1">      <stop offset="0%" stopColor="#00ddee" />      <stop offset="100%" stopColor="#5dc1fb" />    </linearGradient>  </defs>  <XAxis    dataKey="scene"    tickLine={false}    axisLine={{ stroke: "#5dc1fb" }}    tick={{ fill: "# 999"}} / >... </BarChart>Copy the code

So easy~

3.2 Change the top to rounded corners

Next we implement the top of the rounded corner, which is essentially a closed , and we simply draw a rectangle with the rounded corner at the top.

Here we use the shape property provided by the

component and pass in a custom component

to handle it.

<Bar  dataKey="time"  fill="url(#abc-bar-gradient)"  barSize={32}  shape={<CustomBar />}/>Copy the code

Next, we will focus on how to implement the
. The core problem is how to calculate the D.

The implementation code is as follows. Once you understand the meaning of x, y, width, height, everything is very simple.

function CustomBar(props) {  const { fill, x, y, width, height } = props;  const radius = width / 2;  const d = `M${x}.${y + height}    L${x}.${y + radius}    A${radius}.${radius} 0 0 1 ${x + width}.${y + radius}    L${x + width}.${y + height}    Z`;  return (    <path d={d} stroke="none"fill={fill} /> ); }Copy the code

(x, y) refers to the coordinates of the top left corner of the column.

With rounded corners:

3.3 Setting the Shear

The above implementation is the case of relatively balanced data, when the data is very different, it will expose a psychological explosion problem, not to say more, see the figure below.

Look at the lower left corner = =

We want to implement a rounded rectangle, but (x, y) is actually in the upper left corner of the left margin of the semicircle. When this point is too close to the coordinate axis, plus the radius of the corner, the y-coordinate of the beginning of the corner is out of range, resulting in this weird situation. Is it possible to hide it?

How can you not! I continued to surf the Internet and found the clipping function of SVG [7]. It happened that SVG generated by Recharts also had

elements, which must have been considered by the author.

Predefined clipPath

That is, I could have just referred to clipPath in the column, but it is prefixed with an id that seems to be globally uniform and self-incremented. How do I get the exact id?

Digging into the Recharts source code, we found the definition of clipPath’s ID mentioned here [8]. It turned out that we needed to pass in a fixed ID attribute on the outermost
.

<BarChart width={520} height={280} data={data} id={uniqueId}> ... </BarChart>Copy the code

Passing a clipPath with a combination of ids we can control to the rendered in
, problem solved.

function CustomBar(props) {  const { fill, x, y, width, height } = props;  const radius = width / 2;  const d = `M${x}.${y + height}    L${x}.${y + radius}    A${radius}.${radius} 0 0 1 ${x + width}.${y + radius}    L${x + width}.${y + height}    Z`;  return (    <path d={d} stroke="none" fill={fill}      clipPath={`url(#${uniqueId}-clip)`} /> ); }Copy the code

3.4 Implementation of Label

In the same vein, we define a
component directly in the label property provided by the

component.

<Bar isAnimationActive={! isEmpty} dataKey="time"  fill="url(#abc-bar-gradient)"  barSize={32}  shape={<CustomBar />}  label={<CustomLabel />}/>Copy the code

Use DevTools to track a wave and format the text (here abstracted as getStudyTime).

function CustomLabel(props) {  const { x, y, width, height, value } = props;  return (    <text      x={x + width / 2 - 1} y={y - 10}      width={width} height={height}      fill="# 999"      className="recharts-text recharts-label"      textAnchor="middle"> {getStudyTime(value)} </text> ); };Copy the code

3.5 Final Effect

The bar chart

Summary and Impressions

SVG and React

When making this requirement, I also started to directly learn SVG and mastered a new technology to control visual display, which was full of harvest ~

React rendering SVG directly also further opened my horizon. It turns out that it can not only render HTML elements, but also directly render SVG. With the implementation of adaptation layer, we can also use Canvas and Native rendering, and even the LCD screen of embedded devices [9]. React implemented a set of code to construct a lot of complex UI logic on different platforms, and I really felt the power of this abstraction.

“Abstraction” and the selection of chart frame

During the vacation, I saw SICP course [10], which discussed many topics about “abstraction”. We build abstract barriers around complex things to keep our energy from being consumed by repetitive trivia.

The purpose of abstraction is to hide the complexity behind it. In essence, creating an abstract barrier also creates a new way of communication, which can be said to be a “language” in a sense.

Learning a new language can actually be a burden, but we don’t usually notice it. When such abstractions are complex enough, such burdens begin to manifest themselves. Often our needs cannot be met by a layer of abstraction, and often go across layers of abstraction barriers.

A barrier of abstraction brings hierarchy

Crossing multiple layers of abstraction means that more “languages” and their relationships need to be understood at the same time, leading to a huge increase in complexity and a lot of pitfalls.

If you want to generalize complex reality in an abstract way, there will be certain emphasis in design. This is a paradox. A graphical visualization component like ECharts, which focuses on simple configurations, can be very difficult if you try to do subtle customization. Recharts is more focused on customization, which provides us with a way to directly touch the final UI presentation. With React, the customization process is easy enough. When we do component library selection, we should consider the comparison and tradeoff of objectives under different dimensions, and make the most appropriate choice according to the emphasis of requirements.

The resources

[1] 
Recharts: http://recharts.org/


[2] 
Modular visualization – Recharts: https://zhuanlan.zhihu.com/p/20641029


[3] 
Website custom ActiveShape example: http://recharts.org/en-US/examples/CustomActiveShapePieChart


[4] 
SVG tutorial: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial


[5] 
MDN about Stroke and the Fill is introduced: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Fills_and_Strokes


[6] 
MDN document about the gradient: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Gradients


[7] 
SVG shear function: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Clipping_and_masking


[8] 
The definition of clipPath id: https://github.com/recharts/recharts/blob/master/src/chart/generateCategoricalChart.tsx#L172


[9] 
Render will React to the embedded LCD screen: https://juejin.cn/post/6844903984998809614


[10] 
Bilibili Learning – SICP course: https://www.bilibili.com/video/av8515129/