在最近的一个项目中,需要绘制报表来清晰直观地反映业务流程的创建情况。G2 是一套基于图形语法理论的可视化底层引擎,以数据驱动,提供图形语法与交互语法。使用 G2,可以无需关注图表各种繁琐的实现细节,通过一条语句即可使用 Canvas 或 SVG 构建出各种各样的可交互的统计图表。
封装G2图表组件
G2Plot 是基于 G2 的开箱即用的图表库,我们使用 G2Plot 来绘制图表。基于 G2Plot, 封装一个适合我们自己业务使用的组件,将其命名为 chart.vue ,该组件为展示型组件,其职责仅用于渲染图表。
<style lang="scss">
.g2plot-chart {
display: flex;
align-items: center;
min-height: 100px;
}
</style>
<template>
<div class="g2plot-chart" />
</template>
<script>
import { DualAxes, Column, Pie, Line } from "@antv/g2plot";
const chartTypeClasses = {
line: Line,
column: Column,
dualAxes: DualAxes,
pie: Pie,
};
export default {
props: {
type: {
type: String,
required: false,
default: "line",
},
data: {
type: Array,
required: true,
},
options: {
type: Object,
required: true,
},
afterChartRender: {
type: Function,
required: false,
default: () => {},
},
},
data() {
return {
plot: null,
};
},
watch: {
// 监听 data 变化
data: {
deep: true,
handler(ndata, odata) {
// data 发生改变时重新渲染图表
JSON.stringify(ndata) !== JSON.stringify(odata) && this.renderChart();
},
},
},
mounted() {
this.renderChart();
},
methods: {
// 初始化图表
renderChart() {
const { data, options } = this;
if (this.plot) {
this.plot.changeData(data);
} else {
const params = Object.assign(
{},
{
padding: "auto",
autoFit: false,
},
{
...options,
data,
}
);
params.width = this.$el.offsetWidth;
params.height = this.$el.offsetHeight;
const plot = new chartTypeClasses[this.type](this.$el, params);
plot.render();
this.plot = plot;
this.$nextTick(() => {
this.afterChartRender(this.plot);
});
}
},
},
};
</script>
使用G2图表组件
定义一个名为 ReportPane 的组件,chart 组件作为子组件在 ReportPane 中使用,图表数据的请求、数据格式的转换,G2Plot的配置项都将会放在 ReportPane 组件中。
// ReportPane.vue
<template>
<div class="report-pane-container">
<div
v-loading="loading"
element-loading-text="数据加载中..."
element-loading-spinner="el-icon-loading"
class="report-pane-inner"
>
<g2plot-charts
v-if="reportData.length"
class="chart-container"
style="height: 280px"
:data="chartData.nodes"
:options="chartData.options"
:after-chart-render="setDefaultShowLegend"
/>
<div v-if="!reportData.length && !loading" class="no-data-tip">
<Icon type="ios-filing-outline" size="40" color="#ccc" /><br />
暂无数据
</div>
</div>
</div>
</template>
<script>
import g2plotCharts from "./components/G2plotCharts/chart";
export default {
components: {
g2plotCharts,
},
};
</script>
<style lang="scss">
.report-pane-container {
padding: 10px 0px;
.el-radio-button--mini .el-radio-button__inner {
padding: 6px;
}
}
.report-pane-inner {
min-height: 300px;
padding-top: 10px;
position: relative;
}
.no-data-tip {
position: absolute;
width: 50px;
height: 50px;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
text-align: center;
color: #666;
}
</style>
配置 G2Plot
我们先来熟悉一下G2图表包含的常用组件,如下图:
(图片来源于G2官网)
配置 legend
图例 legend 作为图表的辅助元素,用于标定不同的数据类型以及数据的范围,辅助阅读图表,帮助用户在图表中进行数据的筛选过滤。legend 的默认位置是在图表的左上方,我们将其配置在图表的正下方,并配置图例可进行分页:
legend: {
layout: "horizontal", // 布局方式
position: "bottom", // 图例的位置
flipPage: true, // 是否进行分页
offsetY: 6, // 图例 y 方向的偏移
label: { // 文本配置
align: "top",
},
},
配置 xAxis
xAixs 用于配置图表坐标轴的 x 轴,通过 xAxis, 可对 x 轴进行个性化配置。x 轴方向对应的数据是时间戳,需要对其进行格式化,使用 formatter 配置项,把时间戳格式化成时分:
xField: 'time', // x 轴方向对应的数据字段名
xAxis: {
// 配置 x 轴的文本标签
label: {
offset: 10, // label 在垂直方向上相对于坐标轴线(line) 的偏移量
formatter: (text) => { // 对 label 进行格式化
if (moment(Number(text)).format("HH:mm:ss") === "00:00:00") {
text = moment(Number(text)).format("MM-DD");
} else {
text = moment(Number(text)).format("HH:mm");
}
return text;
},
},
// tickInterval: 20,
nice: true, // 是否美化
},
配置 yAxis
yAixs 用于配置图表坐标轴的 y 轴,通过 yAxis, 可对 y 轴进行个性化配置。y 轴对应的数据的数值很大,需要将数值格式化为千分位:
yField: "value", // y 轴方向对应的数据字段名
yAxis: {
// 配置 y 轴的文本标签
label: {
// 数值格式化为千分位
formatter: (v) =>
`${v}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`),
},
max: yMaxValue + yMaxValue / 4, // 最大值
grid: null, // 坐标轴网格线 null 表示不展示
nice: true, // 是否美化
tickCount: 4, // 期望的坐标轴刻度数
tickLine: {}, // 坐标轴刻度线的配置
line: {}, // 坐标轴线的配置
},
配置 tooltip
当鼠标悬停在图形上时,通过提示框的形式展示该点的数据,比如该点的值,数据单位等,帮助用户快速获取图形的关键数据。在我们的项目里,通过数据字段名来配置 tooltip 的标题:
tooltip: {
title: "tooltipTitle",
},
配置 interactions
通过配置 interactions ,可以与图表进行交互。添加 brush-x 配置,仅框选 x 轴相关的数据,即可以放大查看 x 轴的数据:
interactions: [
{ type: "brush-x" },
],
数据转换
后端返回的数据格式往往不是我们绘制图表时想要的数据格式,因此需要对数据格式进行转换:
const { reportData } = this;
let nodesData = [];
let yMaxValue = 0;
// let xMaxValue = 0;
reportData.forEach((item) => {
const seriesName = generateSeriesName(item);
item.data[0].nodes.forEach((node) => {
if (parseFloat(node.value) > yMaxValue) {
yMaxValue = parseFloat(node.value);
}
if (parseFloat(node.time) > xMaxValue) {
xMaxValue = parseFloat(node.time);
}
nodesData.push({
field: item.data[0].indicator.field,
name: item.data[0].indicator.name,
time: Number(node.time),
tooltipTitle:
this.reportType === "timecompare"
? moment(Number(node.time)).add(1, "days").format(formatStr)
: moment(Number(node.time)).format(formatStr),
value: parseFloat(node.value),
seriesName,
});
});
});
数据排序
为了保证 x 轴方向上绘制的折线是一条线,需要将数据进行排序,以保证在 x 轴方向上数据是递增排列的,避免在绘制多条折线时因为 x 轴方向上的数据的顺序问题而导致绘制的折线出现回环的情况。绘制图表的数据量可能会很大,因此我们选择归并排序算法对数据进行排序:
nodesData = mergeSort(nodesData, "time");
G2Plot 默认没有对数据进行排序,为了避免在绘制多条折线图时出现回环的情况,我们需要自己对数据进行排序,以保证数据是以递增方式排列的
// 归并排序
export const mergeSort = (arr, field = "") => {
// 采用自上而下的递归方法
const len = arr.length;
if (len < 2) {
return arr;
}
const middle = Math.floor(len / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
return merge(mergeSort(left, field), mergeSort(right, field), field);
};
function merge(left, right, field) {
const result = [];
while (left.length && right.length) {
let leftEle = left[0];
let rightEle = right[0];
if (isObject(leftEle) && isObject(rightEle) && field) {
leftEle = left[0][field];
rightEle = right[0][field];
}
if (leftEle <= rightEle) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length) {
result.push(left.shift());
}
while (right.length) {
result.push(right.shift());
}
return result;
}
使用对数坐标轴
如果 y 轴数据相差很大,那么数据量小的线就会挤在 y 轴靠近 0 的位置,如下图:
为了使所有的线都可以正常显示在坐标轴上,我们应该考虑是否使用对数坐标轴。当y轴的数据在某个阈值时,使用对数坐标轴:
// 根据数据中的最大最小值确定是否需要使用对数坐标轴
let logYAxis = {};
if (yMaxValue > 10000 && (yMaxValue / yMinValue) > 100) {
logYAxis = {
type: 'log',
base: 10,
};
}
在 yAxis 配置中添加 logYAxis,启用对数坐标轴:
yAxis: {
label: {
// 数值格式化为千分位
formatter: v => `${v}`.replace(/\d{1,3}(?=(\d{3})+$)/g, s => `${s},`),
},
max: yMaxValue + yMaxValue / 4,
grid: null,
nice: true,
...logYAxis, // 使用对数坐标轴
tickLine: {},
line: {},
},
如下图,使用对数坐标轴后,所有的线都可以正常显示在坐标轴上了:
项目效果及代码地址:codesandbox.io/s/dark-gras…