作为一名CSDN的前端领域优质创作者,时常有一些读者向我咨询前端问题。最近就有一个读者看了一些我之前写的数据可视化文章,向我请教如何制作一个比较复杂的散点图,由于目前做的是大数据项目,在数据可视化也做过一些成绩,尤其是数据分析,数据血缘链路。最常用的是AntV图表库和Echarts。 于是我就用AntV实现了他的需求,由于这个图表比较复杂,借着这次AntV的案例征文来给大家详细分享一下。
先说一下需求背景 某个学校需要统计本区域内学校的成绩,并显示自己在该区域中的位置,设计了这样一个散点图,以x轴为学校成绩的标准差,y轴为学校的平均成绩,两个轴都是数值。点的类型一共有四类。 在图表的四个角分别有辅助注释,分别是
除此之外在图表中有两个特殊的点,这两个点附近使用特殊的图标显示。 以达到快速区分,寻找的效果 一个是“本校”, 一个是“全体”,其中"全体"这个点,以该点的(x,y) 画两条蓝色线,两条蓝线将图表划分成了四象限。
总结而言,相对于一般最基础的散点图,该图表有以下难点
先看一下图表的最终效果
这个图我是使用G2Plot
来实现的,官网地址:https://g2plot.antv.antgroup.com/。
它是一个开箱即用的图表库,
并且易于配置、并且定位是一个通用统计图表库。
由于是散点图,所以使用的是G2Plot中Scatter
模块。
import { Scatter } from '@antv/g2plot';
fetch('https://gw.alipayobjects.com/os/antfincdn/j5ADHaMsZx/scatter.json')
.then(data => data.json())
.then(data => {
const scatterPlot = new Scatter('container', {
data,
xField: 'x',
yField: 'y',
size: 5,
pointStyle: {
fill: '#5B8FF9',
},
});
scatterPlot.render();
});
使用的数据为
[
{ "x": 1, "y": 4.181 },
{ "x": 2, "y": 4.665 },
{ "x": 3, "y": 5.296 },
{ "x": 4, "y": 5.365 },
{ "x": 5, "y": 5.448 },
.....
]
其中有几个比较关键的配置data
,xField
与 yField
data
数组或对象, 设置图表数据源。数据源为对象集合,例如:[{ time: ‘1991’,value: 20 }, { time: ‘1992’,value: 20 }]。
xField
一个字符串, 图形在 x 方向对应的数据字段名,一般是横向的坐标轴对应的字段。比如:要看不同班级的人数情况,那么班级字段就是对应的 xField。
yField
一个字符串, 图形在 y 方向对应的数据字段名,一般是纵向的坐标轴对应的字段。比如:要看不同班级的人数情况,那么人数字段就是对应的 yField。
四个方位的图表标注是使用 Annotations
来实现的, 图形标注,Annotation,作为图表的辅助元素,主要用于在图表上标识额外的标记注解。https://g2plot.antv.antgroup.com/api/components/annotations
在一个图表中你可以添加多个图表标注,而且标注的类型也是多种多样的。可以是文字,图片,html,辅助线,弧线。
在该例子中,我们只需要这样配置就可以在四个角添加标注
annotations: [
{
type: 'text',
position: ['18%', '5%'],
content: '高水平高均衡',
style: {
textAlign: 'right',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['18%', '95%'],
content: '低水平高均衡',
style: {
textAlign: 'right',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['83%', '5%'],
content: '高水平低均衡',
style: {
textAlign: 'left',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['83%', '95%'],
content: '低水平低均衡',
style: {
textAlign: 'left',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
],
其中position表示标注的位置,可以使用百分比,也可以使用一些特殊位置的枚举值,如position: ['median', 'median'],
在这个散点图的统计图中,有两个特殊的点,就是“本校”和“全体”
这是为了实现区分,方便自我定位,按照上面这个图,本校的点,更接近“高水平低均衡” 所以说这说明这个学校的成绩基本是高水平,但并不太稳定。比全体水平好一些。
在特殊点这里,使用的是label
配置项。使用label 可以定义某个点的文本图形属性样式。
官方配置文档 https://g2plot.antv.antgroup.com/api/plots/scatter
由于图标上还要显示文字,嫌麻烦的话可以直接将文字放到图片上,我这里是拆开的,在label中加了一个图标,一个文字。 配置如下:
label:{
formatter: item => {
return labels.includes(item.type) ? item.type : ''
},
offsetY: 0,
content: item => {
if (labels.includes(item.type)) {
const group = new G.Group({})
group.addShape({
type: 'image',
attrs: {
x: 0,
y: 0,
width: 40,
height: 50,
img: 'https://gw.alipayobjects.com/zos/rmsportal/oeCxrAewtedMBYOETCln.png',
},
})
group.addShape({
type: 'text',
attrs: {
x: 20,
y: 20,
text: item.type,
textAlign: 'center',
textBaseline: 'top',
fill: '#ffffff',
},
})
return group
} else {
return ''
}
},
labelLine: true,
},
首先过滤一些点,点的属性中有type
属性的 才需要显示label。content
属性是用来配置label的内容,样式,及定位的。
这个图表细节要使用散点图的quadrant
属性来实现,在散点图中给一个y值和x值就能以该点画出一个四象限,并且能够配置每个区域的颜色,和线的颜色。
quadrant 的配置参数
细分配置 | 类型 | 功能描述 |
---|---|---|
xBaseline | number | x 方向上的象限分割基准线,默认为 0 |
yBaseline | number | y 方向上的象限分割基准线,默认为 0 |
lineStyle | object | 配置象限分割线的样式,详细配置参考绘图属性 |
regionStyle | object[] | 象限样式,详细配置参考绘图属性 |
labels | object[] | 象限文本配置,详细配置参考绘图属性 |
我们的配置
quadrant: {
xBaseline: item.standardDeviation,
yBaseline: item.average,
lineStyle: {
stroke: '#1890ff',
opacity: 0.8,
lineWidth: 3,
},
regionStyle: [
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
],
},
item 是"全体"点。配置线的颜色为蓝色 lineStyle.stroke = '#1890ff'
至此该图表所有的细节都已实现。
<template>
<div>
<div id="container" style="height: 500px; width: 500px"></div>
</div>
</template>
<script>
import { Scatter, G2 } from '@antv/g2plot'
const G = G2.getEngine('canvas')
export default {
data() {
return {}
},
methods: {},
mounted() {
const data = [
{ standardDeviation: 8, average: 95, category: 'I类', type: '本校' },
{ standardDeviation: 8, average: 25, category: 'I类' },
{ standardDeviation: 3, average: 26, category: 'I类' },
{ standardDeviation: 0, average: 98, category: 'I类' },
{ standardDeviation: 7, average: 58, category: 'I类' },
{ standardDeviation: 6, average: 7, category: 'I类' },
{ standardDeviation: 10, average: 54, category: 'I类' },
{ standardDeviation: 5, average: 84, category: 'I类' },
{ standardDeviation: 6, average: 21, category: 'I类' },
{ standardDeviation: 9, average: 93, category: 'I类' },
{ standardDeviation: 9, average: 47, category: 'I类' },
{ standardDeviation: 5, average: 26, category: 'I类' },
{ standardDeviation: 10, average: 17, category: 'I类' },
{ standardDeviation: 2, average: 50, category: 'I类' },
{ standardDeviation: 12, average: 3, category: 'I类' },
{ standardDeviation: 10, average: 75, category: 'II类', type: '全体' },
{ standardDeviation: 3, average: 95, category: 'II类' },
{ standardDeviation: 2, average: 75, category: 'II类' },
{ standardDeviation: 10, average: 27, category: 'II类' },
{ standardDeviation: 7, average: 40, category: 'II类' },
{ standardDeviation: 3, average: 81, category: 'II类' },
{ standardDeviation: 2, average: 94, category: 'II类' },
{ standardDeviation: 0, average: 93, category: 'II类' },
{ standardDeviation: 9, average: 68, category: 'II类' },
{ standardDeviation: 10, average: 12, category: 'II类' },
{ standardDeviation: 12, average: 30, category: 'II类' },
{ standardDeviation: 5, average: 10, category: 'II类' },
{ standardDeviation: 4, average: 60, category: 'II类' },
{ standardDeviation: 7, average: 24, category: 'II类' },
{ standardDeviation: 6, average: 28, category: 'II类' },
{ standardDeviation: 11, average: 14, category: 'III类' },
{ standardDeviation: 7, average: 10, category: 'III类' },
{ standardDeviation: 0, average: 13, category: 'III类' },
{ standardDeviation: 4, average: 74, category: 'III类' },
{ standardDeviation: 1, average: 24, category: 'III类' },
{ standardDeviation: 10, average: 4, category: 'III类' },
{ standardDeviation: 11, average: 90, category: 'III类' },
{ standardDeviation: 0, average: 90, category: 'III类' },
{ standardDeviation: 2, average: 99, category: 'III类' },
{ standardDeviation: 11, average: 24, category: 'III类' },
{ standardDeviation: 6, average: 65, category: 'III类' },
{ standardDeviation: 8, average: 0, category: 'III类' },
{ standardDeviation: 5, average: 19, category: 'III类' },
{ standardDeviation: 12, average: 7, category: 'III类' },
{ standardDeviation: 2, average: 69, category: 'III类' },
{ standardDeviation: 12, average: 37, category: 'IV类' },
{ standardDeviation: 8, average: 56, category: 'IV类' },
{ standardDeviation: 5, average: 70, category: 'IV类' },
{ standardDeviation: 5, average: 1, category: 'IV类' },
{ standardDeviation: 0, average: 37, category: 'IV类' },
{ standardDeviation: 4, average: 9, category: 'IV类' },
{ standardDeviation: 11, average: 69, category: 'IV类' },
{ standardDeviation: 7, average: 20, category: 'IV类' },
{ standardDeviation: 9, average: 77, category: 'IV类' },
{ standardDeviation: 1, average: 83, category: 'IV类' },
{ standardDeviation: 5, average: 68, category: 'IV类' },
{ standardDeviation: 3, average: 39, category: 'IV类' },
{ standardDeviation: 1, average: 8, category: 'IV类' },
{ standardDeviation: 10, average: 38, category: 'IV类' },
{ standardDeviation: 11, average: 18, category: 'IV类' },
]
const cateMap = {
I类: { color: '#299999', shape: 'circle' },
II类: { color: '#f6c022', shape: 'triangle' },
III类: { color: '#ff99c3', shape: 'square' },
IV类: { color: '#74cbed', shape: 'diamond' },
}
const labels = ['本校', '全体']
const item = data.filter(x => x.type === '全体')[0]
const scatterPlot = new Scatter('container', {
padding: [30, 30, 50, 50],
data,
xField: 'standardDeviation',
yField: 'average',
colorField: 'category',
color: ({ category }) => {
return cateMap[category].color
},
size: 5,
legend: {
layout: 'horizontal',
position: 'top',
},
shapeField: 'category',
shape: ({ category }) => {
return cateMap[category].shape
},
pointStyle: {
fillOpacity: 1,
},
label: {
formatter: item => {
return labels.includes(item.type) ? item.type : ''
},
offsetY: 0,
content: item => {
if (labels.includes(item.type)) {
const group = new G.Group({})
group.addShape({
type: 'image',
attrs: {
x: 0,
y: 0,
width: 40,
height: 50,
img: 'https://gw.alipayobjects.com/zos/rmsportal/oeCxrAewtedMBYOETCln.png',
},
})
group.addShape({
type: 'text',
attrs: {
x: 20,
y: 20,
text: item.type,
textAlign: 'center',
textBaseline: 'top',
fill: '#ffffff',
},
})
return group
} else {
return ''
}
},
labelLine: true,
},
annotations: [
{
type: 'text',
position: ['18%', '5%'],
content: '高水平高均衡',
style: {
textAlign: 'right',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['18%', '95%'],
content: '低水平高均衡',
style: {
textAlign: 'right',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['83%', '5%'],
content: '高水平低均衡',
style: {
textAlign: 'left',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
{
type: 'text',
position: ['83%', '95%'],
content: '低水平低均衡',
style: {
textAlign: 'left',
fontWeight: '500',
fill: 'rgb(92, 92, 92)',
},
},
],
meta: {
average: {
alias: '平均分',
},
standardDeviation: {
alias: '标准差',
},
category: {
alias: '类别',
},
},
yAxis: {
nice: true,
line: {
style: {
stroke: '#aaa',
},
},
title: {
text: '平均分',
position: 'end',
offset: 30,
// autoRotate: false,
},
},
xAxis: {
title: {
text: '标准差',
position: 'end',
},
grid: {
line: {
style: {
stroke: '#eee',
},
},
},
line: {
style: {
stroke: '#aaa',
},
},
},
quadrant: {
xBaseline: item.standardDeviation,
yBaseline: item.average,
lineStyle: {
stroke: '#1890ff',
opacity: 0.8,
lineWidth: 3,
},
regionStyle: [
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
{ fill: '#ffffff', opacity: 0.2 },
],
},
})
scatterPlot.render()
},
}
</script>
具体来说,使用了 Scatter 组件创建了一个散点图,并传入了以下配置项:
最后,调用 render() 方法将散点图渲染到 HTML 元素上。