原文作者:IMWeb团队。未经同意,禁止转载。

Recharts 是一款图表处理的类库,利用 React 的特性,重新定义了图表的配置和组合方式,大大地提高了图表自定义样式的灵活度。本文记录了使用 Recharts 结合 SVG 开发自定义样式图表的踩坑历程。

背景

ABCmouse 学校版 为老师们提供了孩子学习情况反馈的模块,其中有一部分数据需要以图表的方式直观展示。

这也涉足到了数据可视化的领域。这个领域细节繁多,靠个人力量难以考虑周全,便需要依赖第三方组件库。结合这一个需求,在数据可视化组件库的选择上,主要考虑两点:

  1. 支持 React
  2. 支持灵活自定义样式

经过一番调研,选择用 Recharts[1] 实现上述的图表。

1. 关于 Recharts

Recharts 是一个处理图表的类库,re 的含义除了 "React" 外,还代表 "Redifined",重新定义图表各元素的组合和配置的方式。它基于 React 和 D3 构建,具有以下特点:

  1. 声明式的标签,让写图表和写 HTML 一样简单
  2. 贴近原生 SVG 的配置项,让配置项更加自然
  3. 接口式的 API,解决各种个性化的需求

下面是一个输出的例子,Recharts 的代码也十分地简洁明了,避免了新学习一套配置和 API 带来的额外负担。

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

可以说一个个痛点都被它戳中了,更具体的介绍可以参考作者的介绍文章:组件化可视化图表 - Recharts[2]

本文接下来的部分,记录使用它在实现饼图条形图中,遇到的细节问题和实现的过程。

2. 饼图的实现

如图,这里的饼图的圆环部分,使用了 PieChart 组件,中间的文字和图例则直接使用 HTML 渲染,不依赖 Recharts。

这里简单地介绍一下 Recharts 实现放大的圆环部分引导线Label 的过程,为你带来一个对 Recharts 直观印象。

2.1 实现圆环部分放大

Recharts 提供的 Pie 组件可以实现基本的圆环部分。需要自定义颜色的情况下,通过 Cell 组件把饼图每一份的颜色传入。

<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>

得到圆环:

接下来需要实现一个鼠标 Hover 状态下,放大鼠标对应的 Sector、再显示虚线引导线和 label 的效果。

参考 官网例子[3],实现 Hover 状态下放大的 Sector,<Pie /> 提供了一个 ActiveShape 属性,往里面传入一个自定义的 React 组件,重新渲染需要的那一份,然后再传入一个 activeIndex 指明哪一份需要重新渲染,另外还需要一个 onMouseEnter 函数,更新 activeIndex

<PieactiveIndex={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>

renderActiveShape 的实现,首先返回一个内径更小,外径更大的 Sector 。根据 render 函数返回的信息填充到 Sector 组件上,cx, cy 为 Sector 所在圆环对应圆心的坐标。

function renderActiveShape(props) {const innerOffset = 2; // 内缩const outerOffset = 4; // 外扩const {cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill} = props;return (<Sectorcx={cx} cy={cy}innerRadius={innerRadius - innerOffset}outerRadius={outerRadius + outerOffset}startAngle={startAngle} endAngle={endAngle}fill={fill}/>);
}

完成圆环部分放大的效果:

2.2 实现引导线和标签

找了一圈 Recharts 的文档没有发现引导线的组件, 官网例子 的引导线是一段嵌套了 svg 元素的代码,作者在做这个需求之前还没仔细研究过 svg 图形。怎么办呢?学!

开始一波网上冲浪,找到了 MDN 的 SVG 教程[4],过了一遍,有了个基础印象。在引导线的实现上用了 <path> 元素。

2.2.1 关于 <path> 元素

<path> 元素提供一个名为 d 属性,意思是 "Path Data",包含了路径的所有数据,数据的格式是一系列的命令,和命令所需要的参数序列。命令与参数之间用空白字符分开。

简单梳理一下文档中涉及的基本命令和接受的参数:

M x y 画笔移动到 (x, y),作为起点
L x y 画一条直线到 (x, y)
H x     水平划线到横坐标 x
V y   水平划线到纵坐标 y
Z     闭合路径回到起点(用于创建一个形状)

它还可以画贝塞尔曲线和弧形,用到下方的命令:

C x1 y1, x2 y2, x y   三次贝塞尔曲线
Q x1 y1, x y          二次贝塞尔曲线
A rx ry x-axis-rotation large-arc-flag sweep-flag x y 绘制弧形

关于 d 属性,本文涉及到的命令都已经列出来了,这里不再赘述。

<path> 还提供了 strokefill 属性,分别对应着边框和填充的颜色,path 本质上是一个闭合路径形成的形状,我们画的图本质上属于边框,因此颜色设置上也是需要用 stroke 来做,具体参考 MDN 关于 Stroke 和 Fill 的介绍[5]

设计同学需要虚线的引导线,SVG 提供了 stroke-dasharray 实现这个需求,它接受一组逗号分隔的数字,这个数字代表着线长和空白的长度的组合。

到这里,绘制图形需要的原料基本梳理清楚了。

2.2.2 生成 Path Data

我们的目标是在 renderShapeData 里输出一个这样的 Sector + 引导线 + Label,需要通过接收原本只交给 Sector 的输入,自己生成相应的绘图数据 d。观察发现我们需要一个先往外延伸一段,再往水平方向折过去的折线。也就是说我们需要确定一个起点,一个中间偏折的参考点,还有最后的终点。配合边框的颜色样式,我们可以得到如下代码。 (这是上述官网的 renderActiveShape 例子的实现思路,我这里做的也是理解和修改的工作)

<pathd={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}stroke={fill}strokeDasharray="1,3"fill="none"
/>

确立三个点的坐标不难,首先需要确定渲染 activeShape 时的 props 各个属性在图形中的含义,这里用到的有:

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

涉及到的圆心坐标、角度、半径等参数的含义如图:

这不就是初中学过的「直角三角形」吗?用三角函数可以很快把三个点的坐标分别计算出来。

接下来把这一切转换成代码的表达。需要考虑角度弧度转换、方向等问题。

const RADIAN = Math.PI / 180;
const innerOffset = 2; // 内缩
const outerOffset = 4; // 外扩
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;

这时我们渲染出了想要的引导线:

2.2.3 label 的生成

这一步比较简单,用 SVG 的 <text> 元素处理就好,把上一步引导线用的 (ex, ey) 作为文字的起始坐标,再考虑一下 textAnchor 保证对齐方向即可。

最终的饼图效果。

3. 条形图的实现

如图,这里我们需要做这样的一个条形图,涉及到的元素有两块,X轴、一系列的柱子,各一个 React 组件。

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

得到如下效果:

到了这一步,我们距离最终目标还差条形图的标签,渐变和圆角的顶部。

3.1 渐变的实现

首先我们解决渐变的问题,查找MDN 关于渐变的文档[6],发现实现其实很简单,只需要往 <defs> 元素插入一个 <linearGradient> 节点,然后再在需要应用渐变的元素的 fill 属性(填充)设为 url(#渐变节点的id属性值) 即可。

Recharts 文档没有说到 <defs> 元素,看 SVG 里面所有渐变、CSS 等定义都集中在了文件开头的 <defs> 里面。脑洞:我直接在组件里面写 <defs> 是否能出现在最终生成的 <svg> 里面呢?试着写了下,还真可以!说明这个脑洞是可行的。

看,加入渐变后的 JSX 代码,还是那么简洁:

<BarChartwidth={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><XAxisdataKey="scene"tickLine={false}axisLine={{ stroke: "#5dc1fb" }}tick={{ fill: "#999" }}/>...
</BarChart>

So easy~

3.2 顶部改为圆角

接下来我们实现圆角的顶部,它本质上是一个封闭的 <path>,我们只需要画一个顶部为圆角的矩形就可以了。

这里我们用到 <Bar> 组件提供的 shape 属性,传入一个自定义组件 <CustomBar> 处理。

<BardataKey="time"fill="url(#abc-bar-gradient)"barSize={32}shape={<CustomBar />}
/>

接下来我们的关注点和精力都放在如何实现这个 <CustomBar /> 上,填充 fill 就用上级继承过来的,核心的问题在于如何计算这个 d

实现代码如下,搞清楚 x, y, width, height 的含义以后,一切都变得十分简单。

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} />);
}

(x, y) 指的是柱子左上角的坐标。

加上圆角后的效果:

3.3 设置剪切

上面的实现是数据比较均衡的情况,当数据差异悬殊的情况下,便暴露出一个让人心态炸裂的问题,不多说,看下图。

我们想实现一个圆角矩形,但 (x, y) 实际上是位于半圆的左边空白部分的左上角。当这个点太接近坐标轴,加上圆角半径以后,圆角的起点的纵坐标便超出范围,导致了这种诡异的情况。能不能把它隐藏起来呢?

怎么能不可以!继续网上冲浪,找到 SVG 的剪切功能[7],恰好 recharts 生成的 SVG 也有 <clipPath> 元素的存在,想必作者有考虑过这一点。

也就是说,我直接在柱子里面引用这里带的 clipPath 就好了,但它的前缀带着一个仿佛是个 id,这个 id 看起来似乎是全局统一自增的。怎么获取到确切的 id 呢?

深入 recharts 源码,找到了这里提到的 clipPath 的 id 的定义[8],原来我们需要在最外层的 <BarChart /> 传入一个固定的 id 属性。

<BarChartwidth={520}height={280}data={data}id={uniqueId}
>...
</BarChart>

<CustomBar /> 里面渲染的 <path> 传入一个带着一个我们可控的 id 组合之后得到的 clipPath,问题解决。

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)`}/>);
}

3.4 Label 的实现

同样的思路,我们直接在 <Bar> 组件提供的 label 属性定义一个 <CustomLabel /> 组件。

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

代码与修改思路也类似,有问题用 DevTools 跟踪一波,再给文字自定义格式化一下(这里抽象成了 getStudyTime 函数)。

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

3.5 最终效果

总结与感想

关于 SVG 与 React

在做这个需求时也开始直接入门了 SVG,掌握了新的一门控制视觉展示的技术,满满的收获~

React 直接渲染 SVG 也进一步打开了我的眼界,原来她不仅可以渲染 HTML 元素,也可以直接撸 SVG,在实现了适配层的情况下,我们还可以搞 canvas、Native 渲染,甚至嵌入式设备的液晶屏也可以用[9]。通过 React 实现一套代码在不同的平台上构造许多复杂的 UI 逻辑,让我实实在在地感受到了这样的抽象的威力所在。

“抽象”与图表框架的选型

假期看了 SICP 课程[10],它讨论了许多关于“抽象”的话题。我们为一些复杂的事情建立抽象屏障,避免了我们的精力被各种重复的琐事给占据。

抽象的目的在于隐藏背后的复杂,创造抽象屏障的本质上也同时创造出一种新的沟通方式,某种意义上可以说是一种“语言”。

让人新把握一门“语言”实际会给人带来负担,但一般情况下我们察觉不到。当这样的抽象复杂到了一定程度,这样的负担便开始显现出来。往往我们的需求并不能被一层抽象满足,而经常去跨越一层层的抽象屏障。

跨越多层抽象屏障,也就意味着需要同时把握更多的“语言”以及它们之间的千丝万缕关系,导致复杂度大大增加,无形中就带来了许多的坑。

想以抽象的方式去概括复杂的现实,设计上必然会有所侧重。这是个矛盾的问题,类似 ECharts 这样侧重于简单配置的图表可视化组件,如果尝试去做精细的定制改造,难度将会非常大;Recharts 更侧重于定制化,它为我们提供了能直接触及到最终 UI 展现的方式,借助于 React,定制的过程也足够简单。我们做组件库选型的时候,得考虑目标在不同维度之下的比较和权衡,根据需求在其中的侧重之处,做最合适的选择。

参考资料

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

[2] 组件化可视化图表 - Recharts: https://zhuanlan.zhihu.com/p/20641029

[3] 官网自定义 ActiveShape 例子: http://recharts.org/en-US/examples/CustomActiveShapePieChart

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

[5] MDN 关于 Stroke 和 Fill 的介绍: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Fills_and_Strokes

[6] MDN 关于渐变的文档: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Gradients

[7] SVG 的剪切功能: https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Clipping_and_masking

[8] clipPath 的 id 的定义: https://github.com/recharts/recharts/blob/master/src/chart/generateCategoricalChart.tsx#L172

[9] 将 React 渲染到嵌入式液晶屏: https://juejin.im/post/5dbb729e51882524c101ffe1

[10] Bilibili Learning-SICP 课程: https://www.bilibili.com/video/av8515129/

svg path绘制心形_SVG 菜鸟的 Recharts 自定义图表实战相关推荐

  1. android绘制心形_Android自定义View系列(一)——打造一个爱心进度条

    写作原因:Android进阶过程中有一个绕不开的话题--自定义View.这一块是安卓程序员更好地实现功能自主化必须迈出的一步.下面这个系列博主将通过实现几个例子来认识安卓自定义View的方法.从自定义 ...

  2. Flutter:如何使用 CustomPaint 绘制心形

    作为程序员其实也有浪漫的一幕,今天我们一起借助CustomPaint和CustomPainter绘制心形,本文将带您了解在 Flutter 中使用CustomPaint和CustomPainter绘制 ...

  3. python画出心形图-python如何绘制心形

    python绘制心形的方法:利用matplotlib和numpy画心形,代码为[init = np.arange(-np.pi, np.pi, 0.001);plt.fill_between(x, y ...

  4. css 绘制心形图案

    CSS3 transform-origin 属性设置旋转元素的基点位置. 注释:该属性必须与 transform 属性一同使用. (1)首先,绘制背景: <!doctype html> & ...

  5. python合成心形_python如何绘制心形

    python绘制心形的方法:利用matplotlib和numpy画心形,代码为[init = np.arange(-np.pi, np.pi, 0.001);plt.fill_between(x, y ...

  6. 学生用计算机如何弄心形,电脑画图软件内如何绘制心形

    电脑画图软件内如何绘制心形 随着科技的发展,电脑已经成为人们日常生活中必不可少的工具,当我们在使用电脑中的画图软件时,如果想要画一颗心形的话,应如何操作呢?接下来就由小编来告诉大家. 具体如下: 1. ...

  7. OpenGL绘制心形函数

    OpenGL绘制心形函数 用的最后一个 r =(float) (r_beishu*(Math.sin(Math.PI*i/180f)*Math.sqrt(Math.abs(Math.cos(Math. ...

  8. python心脏线绘制代码_C++和Java命令行绘制心形图代码分享

    C++和Java命令行绘制心形图案 心形线 心形线,是一个圆上的固定一点在它绕着与其相切且半径相同的另外一个圆周滚动时所形成的轨迹,因其形状像心形而得名. 心脏线亦为蚶线的一种.在曼德博集合正中间的图 ...

  9. Linux下操纵CPU曲线绘制心形

    不久之前看了「编程之美」,里面有在windows下操纵CPU绘制正弦曲线的示例程序.思路很简单,但是需要知道几个windows的API函数. 刚开始我想尝试在windows下绘制心形,不过没能做到,原 ...

最新文章

  1. 《C陷阱与缺陷》一导读
  2. c语言变量值与数组元素值交换,编写一个交换变量值的函数,利用该函数交换数组a和数组b中的对应元素值。要求尽量用指针的方法实现。数组a...
  3. Hibernate关联关系映射-----双向一对多/多对一映射配置
  4. mxf高速发展和数字电影母版制作技术
  5. 浙江义乌计算机中专学校,浙江义乌有没有中专学校?
  6. maven 指定jdk版本打包
  7. UI实用素材模板|可临摹学习的控制面板
  8. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8
  9. JavaWeb项目 打开首页就跳转debug模式的解决方法
  10. 脚本加密http://www.datsi.fi.upm.es/~frosal/sources/
  11. 区块链java开发教程,JAVA区块链项目实战视频课程
  12. linux老自动重启原因,【重启】查询linux自动重新启动原因
  13. 黑龙江省大学计算机学校排名2015,2015黑龙江省大学排行榜 哈工大第一
  14. 微信公众号-服务器配置(token验证)
  15. 使用DistrbutedDataParallel时,nvdiai-smi显示每个进程都占用GPU:0
  16. 程序员为什么要学算法?
  17. 重温数据结构(C语言版)(第二版)
  18. iOS 系统分享UIActivityViewController,自定义分享预览UI
  19. 笔记本电脑搜索不到wifi,只有飞行模式
  20. 互联网常用的网络用语

热门文章

  1. go get golang.org/x 包下载失败问题
  2. docker 学习记录
  3. 100行Python代码实现一款高精度免费OCR工具
  4. 多线程-非共享数据(python 版)
  5. LVQ,Learning Vector Quantization,学习向量量化
  6. “发明在商业上获得成功”对专利法22条第三款有关创造性规定的影响
  7. DEBERTA(Decoding-enhanced BERT with disentangled attention) 论文笔记
  8. Linux和Windows下计算文件的Hash值
  9. leetcode - 55. 跳跃游戏
  10. 面向消费者的自动文本分析(Automated Text Analysis for Consumer Research) 2017 JCR 论文阅读