使用 G2 绘制多折线图

2,875 阅读2分钟

在最近的一个项目中,需要绘制报表来清晰直观地反映业务流程的创建情况。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图表包含的常用组件,如下图:

image.png

(图片来源于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 的位置,如下图:

image.png

为了使所有的线都可以正常显示在坐标轴上,我们应该考虑是否使用对数坐标轴。当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: {},
},

如下图,使用对数坐标轴后,所有的线都可以正常显示在坐标轴上了:image.png

项目效果及代码地址:codesandbox.io/s/dark-gras…