G2Plot柱状图和环形图
柱状图
横坐标是时间,比如'2015-12-10 10:00:00', 可能占据空间太大了, 中间加一个\n 竟然能换行 "2015-12-10 \n 10:00:00"
import { useEffect } from "react";
import { Column } from '@antv/g2plot';
const data = [
{label: '2015-12-10 10:00:00', value: 38},
{label: '2015-12-11 \n 10:00:00', value: 50 },
{label: '2015-12-12 \n 10:00:00', value: 20 }
];
export function LabelColumnChart() {
useEffect(() => {
const columnPlot = new Column('container', {
data,
xField: 'label',
yField: 'value',
});
columnPlot.render();
return () => { columnPlot.destroy()}
}, [])
return<div id="container" style={{ width: 300, height: 200 }}></div>
}
柱子的宽度默认是由columnWidthRatio决定的,当设置1时,所有柱子占满整个画柱子的区域,每一个柱子的宽度就是这个区域的宽度除以柱子的个数,比如区域宽度是150px,有3个柱子,那么每一个柱子的宽度就是150/3= 50px.

columnWidthRatio设为0.8以后,柱子之间有间距,那是相对于1而言的,在1的基础上乘以0.8。比例是1时,每一个柱子是50px,那0.8时,每一个柱子是50*0.8=40px,没有占据的空间(50-40=10px)在柱子两边平分,各10/2 =5px,柱子和柱子之间的间距就是2 * 5 = 10px。当然可以手动设置柱子的宽度,minColumnWidth和minColumnWidth,当最大值和最小值相等时,比如都给一个值20,就是柱子的宽度。
上面说的是画柱子的区域,而不是整个画图区域,因为整个画图区域,除了柱子,还有坐标轴,图例之类的,这也是padding的由来。padding是画柱子区域和canvas画布之间的距离,好让这些内容展示出来。默认是auto,让组件自动计算padding。但有时候,还需要额外的padding,比如手动在顶部添加文字,g2plot并不知道,自动计算的padding可能不够,此时可设置appendpadding,在g2plot自动计算的padding的基础上,再添加padding。当不显示坐标轴的时候,padding: auto可能计算有问题,此时可手动设置paddidng。
y轴的配置,要么是false,要么是一个配置对象,当不显示坐标轴时,又需要设置最大值和最小值,可以把y轴的其他配置项,全部设为null,
yAxis: {
label: null,
grid: null,
min: 10
}
柱子的颜色可以配置,有以下数据,
const data = [ {label: '0ms', value: 0, flag: 0}, {label: '100ms', value: 50, flag: 0},{label: '200ms', value: 100, flag: 0}, {label: '300ms', value: 70, flag: 1}, {label: '400ms', value: 20, flag: 1},{label: '500ms', value: 8, flag: 2} ];
横轴从大到小排列,超过某个值就给个flag标识,每种标识1个颜色,这种简单有规律的就可以使用数组配置各个颜色,然后用seriesField指定用flag的值匹配颜色
seriesField: 'flag', // 部分图表用colorField,但柱状图用seriesField color: ['green', 'yellow', 'red'], legend: false // 设置了seriesField 有了legend
但这种配置存在一个问题,如果删除data最后一项,没有flag: 2,flag: 0和flag: 1的颜色竟然是color数组的第一项和第三项,flag: 0是green,flag: 1是'red'。这时还判断,flag只有两种情况的时候,
const flagSet = new Set(data.map(item => item.flag)) color: flagSet.size === 2 ? ['green', 'yellow'] : ['green', 'yellow', 'red'],
但当用value值来判断填充什么颜色时,数组就无能为力了,因为value值太多了,此时可以使用函数,函数的参数就是seriesField指定的字段,
seriesField: 'value', color: (data) => { console.log(data) // data只有seriesField指定的属性(value) return data.value <= 60 ? 'green' : (data.value >=80 ? 'red': 'yellow') },
自定义toolTip,先看下tooltip的组成部分,

tooltip默认显示title,name和value,name,value 加上前面的marker组成一个itemTpl。鼠标hover到柱子上时,

当把整个颜色配置去掉,

可以发现,title对应的是xField指定的字段的值(x轴坐标的值),name和value是对应是y轴,如果有seriesField,那么name的值就是seriesField指定字段的值,value就是该字段对应的值。如果没有seriesField,name就是yField指定的字段,value就是该字段对应的值。它们都从data数据每一项取值,每一个柱子对应的就是data中的每一项。没有seriesField时好处理,value换成中文,只需配置一下meta,给value一个别名
meta: { value: { alias: '数量' } }
有seriesField时,name和value通过formatter函数进行配置,也是返回name和value属性,
tooltip: { formatter: (datum) => ({ name: '数量', value: Math.round(datum.value) })
title也可以配置,显示和隐藏用showTitle控制,title还接受一个函数,对内容进行配置,
tooltip: { // showTitle: false, title: (title, datum) => title + '~' + (parseInt(title) + 100) + 'ms', }
要显示其他信息,就要手动配置了itemTpl。首先设置fields属性,指定要显示数据源(data数组中每一项)中的哪些字段。其次在formatter函数中把这些字段对应的值返回出去,最后itemTpl从fomatter函数的返回值中读取数据展示出来。
tooltip: { fields: ['label', 'value', 'flag'], formatter: (datum) => { const tip = ['better', 'good', 'bad'] return { name: datum.label, value: datum.value, flag: tip[datum.flag] } }, // 自定义 item 模板:每项记录的默认模板,自定义模板时必须包含各个 dom 节点的 class。 itemTpl: ` <li class="g2-tooltip-list-item"> <span class="g2-tooltip-marker" style="background-color: {color};"></span> <span class="g2-tooltip-name">{name}</span>: <span class="g2-tooltip-value">{value}<span style="margin-left: 16px; color: #ccc">{flag}</span></span> </li> ` }
itemTpl模板中 {xxx} 对应的是formatter函数返回对象的key,formatter中可以返回任何内容,甚至是样式,比如color,display,
tooltip: { fields: ['label', 'value', 'flag'], formatter: (datum) => { const tipColor = [['better', 'green'], ['good', 'yellow'], ['bad', 'red']] return { name: datum.label, value: datum.value, flag: tipColor[datum.flag][0], flagColor: tipColor[datum.flag][1], // flag的颜色 display: datum.value > 0 ? 'block' : 'none' // 值为0,不显示itemTel } }, itemTpl: ` <li class="g2-tooltip-list-item" style="display: {display};"> <span class="g2-tooltip-marker" style="background-color: {color};"></span> <span class="g2-tooltip-name">{name}</span>: <span class="g2-tooltip-value">{value}<span style="margin-left: 16px; color: {flagColor}">{flag}</span></span> </li> ` }
如果tooltip比较简单,就显示一个值,用不到itemTpl,可以使用customContent
tooltip: { customContent: (title, data) => { console.log(data) return `<div>${title}</div>`; } }
添加图表标注(Annotation),添加最多的是辅助线。线就要确定起点和终点(start和end),一个点需要横纵坐标,因此start和end是一个数组,数组的第一项是横坐标,数组的第二项是纵坐标。添加y轴的水平线比较简单,因为y轴通常是数字,想画那个值的水平线,就让那个值是纵坐标。起点在最左侧,终点在最右侧。起点和终点可以使用G2提供的预设数据点,max, min. start, end. min是最小值,对应x轴来说,它的最小值是第一个坐标刻度,max最大值就是最后一个坐标刻度。画水平线,就是从第一个刻度画到最后一个刻度。start和end 是0,1是,整个画图区域。start: ['start', 60]. end: ['end', 60], 就是y值等于60的地方画一条水平线
annotations: [ { type: 'line', start: ['start', 60], end: ['end', 60], text: { content: '中位线', position: 'right', offsetY: 6, style: { textAlign: 'right', }, }, style: { lineWidth: 0.5, lineDash: [4, 4] }, }, ]
x轴上画一条竖线,那就比较复杂了。因为x轴是分段的,不连续,需要手动计算出,它在x轴那两个label之间。它的格式有三种,一个是lable名,比如start: ['20ms', min],end:["30ms", max], 直接把竖线画在label上。一个是数字,1,2,3,就是表示第几个label,3.1,3.2 就表示偏移了。第三种取值是百分比字符串, 比如['35%', '20%'], 画柱子的区域宽度乘以35%,高度乘以20%。比如在180ms处画垂直
function getAnnotation(rate: string) { const index = data.findIndex(item => parseInt(rate) <= parseInt(item.label)) const label = data[index].label; const diff = parseInt(rate) - parseInt(label); const perceentage = diff / 100 // 每个柱子或横坐标直接的间距是100msreturn { type: 'line', start: [index + perceentage, 'min'], end: [index + perceentage, 'max'], text: { content: rate, position: 'end', autoRotate: false, // 手动开启下面的旋转时,这个要设为false。 rotate: Math.PI, offsetX: -10, offsetY: -6 } } } annotations: [ {...getAnnotation('180ms')} ],
这时线画出来了,但180ms没有显示从出来,就padding不够,设置appendPadding: [20, 0, 0, 0],
添加纯文本,比如给纵坐标添加一个单位ms,这时可以用百分比,
annotations: [ { type: 'text', content: 'ms', position: ['0%', '-10%'], // y: 10%是自上而下,画柱子的区域最上边下来10%,-10%,就是最上边上去10%, 10%相相对于画布的高度 style: { textAlign: 'center', fill: 'rgba(0,0,0,0.85)', }, }, ],
画柱子区域的最上边,就是下图中的红色

环形图
有个需求,画一个如下的环形图

环形图有几个分类,每个分类占比不同,需要中间有间隔来区分。同时每一个分类又有两种情况(机会和评分),分别用不同的颜色表示,中间显示总得分。接口的数据是这样的,
const data = { appScore: '90', categoryList: [ { type: '分类一', weight: 0.5, score: 20, opp: 80 }, { type: '分类二', weight: 0.3, score: 39, opp: 61 }, { type: '分类三', weight: 0.2, score: 98, opp: 2 }, ] }
g2plot的环形图比较简单,数组中有几项,就画几个环,环与环之间没有空隙,每一个环的大小是由数组项中的value来决定。第一想法是,按分类画三个环,然后每一个环再自定义形状进行分成两部分,没有实现。然后又想到,既然每一个环是数组的每一项,可以对数据重新组装,分成六份(分类一Score,分类一Opp,分类二Score,分类二Opp,分类三Score,分类三Opp),这样每一个环的颜色都不一样,实现了大部分功能,就是间隙不能实现。每一个分类的value是多少呢?每一个分类都是一个占比,加起来是1,那就按100来,分类一的value就是100 * 0.5 = 50. score 和opp 加起来是100,也就是说score占50的20/100, opp占50的80/100, 分类二和分类三同理。
const categoryValueList = data.categoryList.map(item => ({ ...item, value: 100 * item.weight }))
const subCategoryList = []
categoryValueList.forEach(item => {
subCategoryList.push({
type: item.type + 'Score',
value: (item.value) * item.score / 100
})
subCategoryList.push({
type: item.type + 'Opp',
value: (item.value) * item.opp / 100
})
})
实现中间的间隙需要自定义形状,官网的自定义现状正好是环形,把draw的path生成改成如下
let path = []; path.push(['M', points[0].x, points[0].y]); path.push(['L', points[1].x, points[1].y -0.01]); //0.01就是间隙 path.push(['L', points[2].x, points[2].y-0.01]); path.push(['L', points[3].x, points[3].y]); path.push('Z'); // 将0-1空间的坐标转换为画布坐标 path = this.parsePath(path);
但在项目中,0.01这种设置方式效果不好,到后面发现-0.01的逻辑是g2plot已经给每一个环形生成了一个坐标,-0.01就是这一块不要了,就产生了间隙。当时就想,生成环形的时候,为什么不多生成一个环形,然后多生成的这个环不要了,这样也能控制间隙的大小。那么每一个分类就要分为三部分,score,opp 和gap,gap是固定值,用来控制间隙,score和opp在计算value的时候,要用分类的value减去gap,再根据比例计算。
const gap = 2; categoryValueList.forEach(item => { subCategoryList.push({ type: item.type + 'Score', value: (item.value - gap) * item.score / 100 }) subCategoryList.push({ type: item.type + 'Opp', value: (item.value - gap) * item.opp / 100 }) subCategoryList.push({ type: item.type + 'Gap', value: gap }) })
怎么去掉gap这个环?搜索发现,只要环的填充色和背景色一样,或使用透明度,就看不见了,也算达到目的。g2plot中,pieStyle可以自定义
pieStyle: ({type}) => {
return type.includes('Gap') ? ({fill: '#fff'}) : undefined
}
还剩下label和tooltip需要处理,它们都和大的分类有关,在画好的小的分类的图形中,不太好处理。可以再画一个环形图,盖在小的分类的环形图上,只要不填充颜色就看不见。tooltip中的颜色和score和opp的颜色一致,需要统一颜色。
const colorMap = { "分类一Score": '#0043cc', "分类一Opp": '#528bff', "分类二Score": '#009a97', "分类二Opp": '#34ddcf',"分类三Score": '#4d3c99', "分类三Opp": '#a598f5'}
在React中,封装一个组件,
import { Pie } from "@antv/g2plot";
import { useEffect } from "react";
type Props = { appScore: string; categoryList: Array<{ type: string; weight: number; score: number; opp: number }> }
const colorMap = { "分类一Score": '#0043cc', "分类一Opp": '#528bff', "分类二Score": '#009a97', "分类二Opp": '#34ddcf', "分类三Score": '#4d3c99', "分类三Opp": '#a598f5' }
type colorKey = keyof typeof colorMap
export function CategoryPie({data}: {data: Props}) {
const { appScore, categoryList } = data;
const categoryValueList = categoryList.map(item => ({ ...item, value: 100 * item.weight }))
const subCategoryList: { type: string; value: number }[] = []
const gap = categoryList.length === 1 ? 0 : 2; // 只有一个分类,就不要区分了,间隙为0
categoryValueList.forEach(item => {
subCategoryList.push({ type: item.type + 'Score', value: (item.value - gap) * item.score / 100 })
subCategoryList.push({ type: item.type + 'Opp', value: (item.value - gap) * item.opp / 100 })
subCategoryList.push({ type: item.type + 'Gap', value: gap })
})
useEffect(() => {
const piePlotBottom = new Pie('containerBottom', {
appendPadding: 10,
data: subCategoryList,
angleField: 'value',
colorField: 'type',
radius: 1,
innerRadius: 0.8,
legend: false, // 所有小分类的表现特征都设为false
label: false,
tooltip: false,
statistic: undefined,
pieStyle: ({ type }) => type.includes('Gap') ? ({ fill: '#fff' }) : ({ fill: colorMap[type as colorKey] })
});
piePlotBottom.render();
const piePlotTop = new Pie('containerTop', {
appendPadding: 10,
data: categoryValueList,
angleField: 'value',
colorField: 'type',
radius: 1,
innerRadius: 0.6,
legend: false,
label: {
labelLine: null,
content: (data) => data.type // data是list数据集中的整个一项
},
tooltip: {
fields: ['score', 'opp', 'type'],
// @ts-ignore
formatter: (datum) => {
return {
opp: datum.opp, score: datum.score,
scoreColor: colorMap[datum.type + 'Score' as colorKey],
oppColor: colorMap[datum.type + 'Opp' as colorKey]
}
},
// 返回单个根节点。
itemTpl: `
<ul>
<li class="g2-tooltip-list-item">
<span class="g2-tooltip-marker" style="background-color: {oppColor};"></span>
<span class="g2-tooltip-name">机会</span>:
<span class="g2-tooltip-value">{opp}</span>
</li>
<li class="g2-tooltip-list-item">
<span class="g2-tooltip-marker" style="background-color: {scoreColor};"></span>
<span class="g2-tooltip-name">评分</span>:
<span class="g2-tooltip-value">{score}</span>
</li>
</ul>
`
},
statistic: {
title: false,
content: { content: appScore },
},
pieStyle: { fill: '#fff', fillOpacity: 0 }
});
piePlotTop.render();
// 当数据发生变化时,先销毁组件,再重新创建。清理函数中销毁组件。
return () => {
piePlotBottom.destroy()
piePlotTop.destroy()
}
}, [data])
return (
<>
<div id="containerBottom" style={{ width: 300, height: 200 }}></div>
<div id="containerTop" style={{ width: 300, height: 200, marginTop: -200 }}></div>
</>
)
}

浙公网安备 33010602011771号