0%

ECharts径向条形图(玉玦图)实例分析

径向条形图(Radial Bar Chart),就是把普通笛卡尔二维坐标系中的条形图转为极坐标系下展示,使用弧度来表示数值大小。

如下图所示,在本文中解析一个 ECharts Gallery 中的径向条形图实例,顺带学习 ECharts 中极坐标系、颜色渐变和自定义系列的用法。

附带说一下,有时候也把径向条形图叫做玉玦图。古人曾经云过:“满者为环,缺者玦”,玉环都知道是什么样子,缺一块的玉环叫玉玦,用玉玦来称呼径向条形图真的是挺形象。

基本结构

首先把基本结构如下,给 ECharts 准备一个 DOM 容器用来初始化实例,自定义一组数据 data 和配置项 option,最后给实例 chart 设置 option

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/echarts@4.6.0/dist/echarts.js"></script>
<title>ECharts径向条形图(Radial Bar Chart)</title>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
div#container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
let chart = echarts.init(document.getElementById('container'));
// 指定图表的配置项和数据
// 把数据按照从大到小的顺序排列
let data = [
{ name: '张三', value: 88 },
{ name: '李四', value: 250 },
{ name: '王五', value: 5438 },
{ name: '赵六', value: 8848 },
{ name: '陈七', value: 9527 },
{ name: '朱八', value: 10086 }
].sort((a, b) => a.value > b.value);

let option = {
// ......
// ......
// ......
};

// 使用刚指定的配置项和数据显示图表。
chart.setOption(option);
</script>
</body>
</html>

数据 data 提前进行了排序,排列出来的径向条形图更美观。

坐标系配置

这个实例中由于使用的是极坐标系,所以需要专门配置,本节中需要用的配置项如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let option = {
backgroundColor: '#000',
tooltip: {},
polar: {
radius: ['10%', '80%']
},
angleAxis: {
show: false,
// 最小值是0°处的数值,最大值是360°处的数值,避免出现弧度为0和2PI的数据
min: value => (value.min >= 1 ? value.min - value.max / 3 : 0),
max: value => (value.max * 4) / 3
},
radiusAxis: {
type: 'category',
axisLabel: {
interval: 0,
color: '#2df',
fontSize: 16
},
axisLine: {
show: false
},
axisTick: {
show: false,
alignWithLabel: true
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(17, 51, 68, 0.8)'
}
},
data: data.map(item => item.name)
},
series: [
// ......
// ......
// ......
]
};

大部分的配置都是一些细节样式上的调整,最关键的三项是 polorangleAxisdata。设置 polor 属性后自动变为径向坐标系,并加了一个最小半径 10%和最大半径 80%
angleAxis 是角度轴的设置,minmax 指的是的是 360° 代表的数值。在这里为了避免数值过小的数据显示不明显,同时不让数值过大的数据把圆环填满,故意把 minmax 从数据的大小极限向外拓展了一段,形成一个比较美观的玉玦效果。
radiusAxis 是径向轴的设置,radiusAxis.data 属性返回的是数据中的每一项的 name,目的是显示张三李四这一系列的数据名。

条形图配置

系列中 bar 条形图配置的部分比较普通,本节中需要用的配置项如下:

关于条形图部分的配置代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let option = {
// ......
// ......
// ......
series: [
{
type: 'bar',
coordinateSystem: 'polar',
barWidth: 15,
itemStyle: {
// // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord 为 `true`,则该四个值是绝对的像素位置
color: {
type: 'linear',
x: 1,
y: 0,
x2: 0,
y2: 0.5,
colorStops: [
{ offset: 0, color: 'rgba(50, 120, 150, 0.5)' },
{ offset: 0.5, color: 'rgba(80, 190, 210, 0.7)' },
{ offset: 1, color: 'rgba(255, 255, 255, 0.9)' }
]
},
barBorderRadius: 10,
shadowBlur: 20,
shadowColor: 'rgba(255, 255, 255, 0.8)'
},
emphasis: {
itemStyle: {
shadowColor: 'rgba(255, 255, 255, 1)'
}
},
data: data
}
// ......
// ......
// .....
]
};

颜色渐变

条形图的配置比较普通,但是上述代码中明显不普通的地方是 itemStyle.color,这里的颜色使用的是渐变。虽然在 ECharts 文档中并没有对这里的颜色能渐变进行说明,但是在其他的配置中找到了对渐变的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比。
// 如果 globalCoord 为 `true`,则该四个值是绝对的像素位置
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'red' // 0% 处的颜色
}, {
offset: 1, color: 'blue' // 100% 处的颜色
}],
global: false // 缺省为 false
}
// 径向渐变,前三个参数分别是圆心 x, y 和半径,取值同线性渐变
color: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [{
offset: 0, color: 'red' // 0% 处的颜色
}, {
offset: 1, color: 'blue' // 100% 处的颜色
}],
global: false // 缺省为 false
}
// 纹理填充
color: {
image: imageDom, // 支持为 HTMLImageElement, HTMLCanvasElement,不支持路径字符串
repeat: 'repeat' // 是否平铺, 可以是 'repeat-x', 'repeat-y', 'no-repeat'
}

所以在玉玦图的代码中渐变的意思就是从[100%, 0%]的右上角到[0%, 50%]的左中位置的线性渐变,并在中间加了颜色停顿点

需要说明的是,ECharts 还有另一种渐变方式,大家用的非常多但是至今没有文档说明:echarts.graphic.LinearGradient,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
type: 'bar',
itemStyle: {
normal: {
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{offset: 0, color: '#000'},
{offset: 0.5, color: '#888'},
{offset: 1, color: '#ddd'}
]
)
}
}
}

其中前四个参数依次对应右、下、左、上四个方位,比如 0,0,0,1 代表从正上方往正下方渐变,0,1,1,0 代表从左下方往右上方渐变,颜色停止点的理解与上述配置式的颜色停止点相同。

自定义系列

在玉玦图中,还有一个非常重要需要理解的地方就是每条 bar 后面都跟着一个数字 value,这里使用自定义系列加上去的。自定义序列是 ECharts 为了方便开发者能够跳出固定配置项,按照自己的想法自由渲染图表而推出的一个配置。

本节中需要查看的配置项如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
series: [
// ......
// ......
// ......
{
type: 'custom',
coordinateSystem: 'polar',
renderItem: (params, api) => {
// 给定维度上的值
let values = [api.value(0), api.value(1)];
// 对于polar坐标系,还会包含其他信息:polar: [x, y, radius, angle]
let coord = api.coord(values);
return {
type: 'text',
position: [3 * Math.sin(coord[3]), 3 * Math.cos(coord[3])],
rotation: coord[3] + Math.PI / 2,
origin: [coord[0], coord[1]],
style: {
text: api.value(1),
fill: '#ac6',
fontSize: 16,
textAlign: 'right',
textVerticalAlign: 'middle',
x: coord[0],
y: coord[1]
}
};
},
data: data
}
];

自定义序列的 typecustom,又给赋予了一个 polar 极坐标系和数据 data,最关键的地方是 renderItem自定义渲染逻辑)。

renderItem 函数有两个参数 paramsapi,第一个 params 主要包含以下内容,基本上全是坐标系和数据的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
context: // {Object} 一个可供开发者暂存东西的对象。生命周期只为:当前次的渲染。
seriesId: // {string} 本系列 id。
seriesName: // {string} 本系列 name。
seriesIndex: // {number} 本系列 index。
dataIndex: // {number} 数据项的 index。
dataIndexInside: // {number} 数据项在当前坐标系中可见的数据的 index(即 dataZoom 当前窗口中的数据的 index)。
dataInsideLength: // {number} 当前坐标系中可见的数据长度(即 dataZoom 当前窗口中的数据数量)。
actionType: // {string} 触发此次重绘的 action 的 type。
coordSys: // 不同的坐标系中,coordSys 里的信息不一样,含有如下这些可能:
coordSys: {
type: 'cartesian2d',
x: // {number} grid rect 的 x
y: // {number} grid rect 的 y
width: // {number} grid rect 的 width
height: // {number} grid rect 的 height
},
coordSys: {
type: 'calendar',
x: // {number} calendar rect 的 x
y: // {number} calendar rect 的 y
width: // {number} calendar rect 的 width
height: // {number} calendar rect 的 height
cellWidth: // {number} calendar cellWidth
cellHeight: // {number} calendar cellHeight
rangeInfo: {
start: // calendar 日期开端
end: // calendar 日期结尾
weeks: // calendar 周数
dayCount: // calendar 日数
}
},
coordSys: {
type: 'geo',
x: // {number} geo rect 的 x
y: // {number} geo rect 的 y
width: // {number} geo rect 的 width
height: // {number} geo rect 的 height
zoom: // {number} 缩放的比率。如果没有缩放,则值为 1。例如 0.5 表示缩小了一半。
},
coordSys: {
type: 'polar',
cx: // {number} polar 的中心坐标
cy: // {number} polar 的中心坐标
r: // {number} polar 的外半径
r0: // {number} polar 的内半径
},
coordSys: {
type: 'singleAxis',
x: // {number} singleAxis rect 的 x
y: // {number} singleAxis rect 的 y
width: // {number} singleAxis rect 的 width
height: // {number} singleAxis rect 的 height
}
}

第二个参数 api,返回一些方法,这里只用到的两个:valuecoord

1
2
3
4
5
6
7
8
9
10
// value得到给定维度的数据值:
@param {number} dimension 指定的维度(维度从 0 开始计数)。
@param {number} [dataIndexInside] 一般不用传,默认就是当前数据项的 dataIndexInside。
@return {number} 给定维度上的值。

// coord将数据值映射到坐标系上:
@param {Array.<number>} data 数据值。
@return {Array.<number>} 画布上的点的坐标,至少包含:[x, y]
对于polar坐标系,还会包含其他信息:
polar: [x, y, radius, angle]

所以在代码中,先用 value()获取了当前 item 的数据值,再传给 coord()计算得到了该点在极坐标系中的位置[x, y, radius, angle]

接下来在 return 时,返回了一个 text 类型。text 类型中的 origin 指定旋转、缩放和平移的基准点,在这个实例中中就是 xyposition 指定平移多少距离,在这里使用 sin(angle)cos(angle)计算当前点的切线方向,将文字平移了 3pxrotation 使用 angle 加上了 90°,将文字延切线摆放。returns.style 中给文字设定了文本值xy 坐标和其他的一些样式,完成了文本类型的返回。

全部代码

全部代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/echarts@4.6.0/dist/echarts.min.js"></script>
<title>ECharts径向条形图(Radial Bar Chart)</title>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
div#container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
let chart = echarts.init(document.getElementById('container'));
// 指定图表的配置项和数据
// 把数据按照从大到小的顺序排列
let data = [
{ name: '张三', value: 88 },
{ name: '李四', value: 250 },
{ name: '王五', value: 5438 },
{ name: '赵六', value: 8848 },
{ name: '陈七', value: 9527 },
{ name: '朱八', value: 10086 }
].sort((a, b) => a.value > b.value);

let option = {
backgroundColor: '#000',
tooltip: {},
polar: {
radius: ['10%', '80%']
},
angleAxis: {
show: false,
// 最小值是0°处的数值,最大值是360°处的数值,避免出现弧度为0和2PI的数据
min: value => (value.min >= 1 ? value.min - value.max / 3 : 0),
max: value => (value.max * 4) / 3
},
radiusAxis: {
type: 'category',
axisLabel: {
interval: 0,
color: '#2df',
fontSize: 16
},
axisLine: {
show: false
},
axisTick: {
show: false,
alignWithLabel: true
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(17, 51, 68, 0.8)'
}
},
data: data.map(item => item.name)
},
series: [
{
type: 'bar',
coordinateSystem: 'polar',
barWidth: 15,
itemStyle: {
// // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord 为 `true`,则该四个值是绝对的像素位置
color: {
type: 'linear',
x: 1,
y: 0,
x2: 0,
y2: 0.5,
colorStops: [
{ offset: 0, color: 'rgba(50, 120, 150, 0.5)' },
{ offset: 0.5, color: 'rgba(80, 190, 210, 0.7)' },
{ offset: 1, color: 'rgba(255, 255, 255, 0.9)' }
]
},
barBorderRadius: 10,
shadowBlur: 20,
shadowColor: 'rgba(255, 255, 255, 0.8)'
},
emphasis: {
itemStyle: {
shadowColor: 'rgba(255, 255, 255, 1)'
}
},
data: data
},

{
type: 'custom',
coordinateSystem: 'polar',
renderItem: (params, api) => {
// 给定维度上的值。
let values = [api.value(0), api.value(1)];
// 对于polar坐标系,还会包含其他信息:polar: [x, y, radius, angle]
let coord = api.coord(values);
return {
type: 'text',
position: [3 * Math.sin(coord[3]), 3 * Math.cos(coord[3])],
rotation: coord[3] + Math.PI / 2,
origin: [coord[0], coord[1]],
style: {
text: api.value(1),
fill: '#ac6',
fontSize: 16,
textAlign: 'right',
textVerticalAlign: 'middle',
x: coord[0],
y: coord[1]
}
};
},
data: data
}
]
};
// 使用刚指定的配置项和数据显示图表。
chart.setOption(option);
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇