g2plot完成区间柱形图和不同柱形圆角配置

656 阅读9分钟

使用antv 配置区间柱形图

iShot_2022-10-26_10.13.42.png

需求: 来源与产品的一个想法,想在 X 轴 每一行输出多个区间柱形,以时间为分段,X轴 底线显示时间, 找了一下 echarts 的实现, 可以使用配置函数的方式 实现,但只实现了一行,而且函数的配置项其API 文档也晦涩难懂, 最后使用 antv 的 区间柱形图来实现, 但是 antv 无法像 echarts 一样配置每一个柱形的颜色, 因为他的 color 配置回调函数只接受一个 type 字符串类型, 最后使用了数组队列匹配的方式来实现

下面是实现之后的效果

iShot_2022-10-26_10.22.32.png

  • 下面分享一下思路

  • 文件引入 这里是 html 文件, 直接可以复制, 下面的 js 文件我我们通过引入 CDN 的方式,这样在使用 demo 的时候更加方便
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    .root {
      padding: 50px;
      width: 1600px;
      height: 500px;
    }
  </style>
  <body>
    <div id="container" class="root"></div>
  </body>
</html>

js文件

 <script
    type="text/javascript"
    src="https://unpkg.com/@antv/g2plot@latest/dist/g2plot.min.js"
  ></script>
  • 上面引入了 g2 以后 , 我们开始分享业务逻辑, 里面包含一些功能函数, 会拆开 首先是功能函数, 会在 我们的业务当中使用到的
// 时间处理
// 小时转分钟
function ChangeStrToMinutes(str) {
  var arrminutes = str.split(":");
  if (arrminutes.length == 2) {
    var minutes = parseInt(arrminutes[0]) * 60 + parseInt(arrminutes[1]);
    return minutes;
  } else {
    return 0;
  }
}
// 分钟转小时
function ChangeHourMinutestr(str) {
  if (str !== "0" && str !== "" && str !== null) {
    return (
      (Math.floor(str / 60).toString().length < 2
        ? "0" + Math.floor(str / 60).toString()
        : Math.floor(str / 60).toString()) +
      ":" +
      ((str % 60).toString().length < 2
        ? "0" + (str % 60).toString()
        : (str % 60).toString())
    );
  } else {
    return "";
  }
}
// 样式处理
const setStyles = (container, styles) => {
  for (const key in styles) {
    container.style[key] = styles[key];
  }
};
// 动态设置画布高度
function setHeigh(data, name, height = 100) {
  const dom = document.getElementsByClassName(name)[0];
  const count = data.length;
  dom.style["height"] = count * height + "px";
}

柱形的圆角配置,这里我找了好一会, 在 g2plot 的文档中并没有这个选项能够配置多柱, 我查看了 g2 的文档,g2.antv.vision/zh/examples… g2 中有圆角堆叠柱形图, 看到有一个 radius 选项, 在 g2plot 中, g2plot.antv.vision/zh/docs/api… 可以使用 barStyle 来绘制我们的柱形, 其中并没有给出 radius 选项 如下图, 但是我在 barStyle 配置 radius 是生效的, barStyle 除了接受一个配置对象,还接受一个 回调函数,使用用 color 的配置一样, 接收一个参数,参数中包含我们每一个柱形的 type 和 values , 但是并不包含我柱形里面的所有信息,所以同 color 一样, 要做一层处理, 找到当前的数据的更多信息来确定绘制 左圆角 还有 右圆角 , 处理就在下面的业务逻辑中的 barStyle 配置项中了

iShot_2022-11-18_17.39.25.png | 最终绘制效果如下: 这其中的数据不是我之前使用的数据了,做了一部分更改, 背景也进行了更改, 这里的背景使用 UI 提供的一个单图,然后使用背景图的方式插入, x 轴平铺, position 调整定位

iShot_2022-11-18_17.43.44.png

  • 业务逻辑处理

注意点:

  1. 数据使用的是假数据, 这里我们可以根据自己的业务需求进行修改和扩展 因为区间柱形图使用的数据 data 是一个数组, 所以在使用我们的数据的时候需要进行一下改造
  2. 在使用 color 回调函数进行配置颜色的时候,不知道里面的循环为什么会进入两次,如果有大佬可以解答或有其他解决方案欢迎交流, 这里我使用了一个闭包的方式来记录次数,因为这里的数据我们业务上还没有和后端对接,所以暂时无法判定哪一条是第一个数据,而且 color 这回调函数也不提供 其他参数, 只提供一个 type 是字符串 作为参数, 所以我在上面数据处理的方法里面自己加入了一个 空字符的 type,用来区分循环的进入。
  3. 因为 color 方法的参数只有 type, 所以我也无法区分当前应该显示哪一种颜色, 数据里面的 way 就是颜色, 这里我使用了一个临时队列 arr 也是以闭包的形式存储, 然后从我处理好的数据中 过滤出当前 type 类型组成的一个数组, 通过下标的方式 取到当前对应的数据
  4. tooltip 如果想要自定义的话 需要使用 customContent API
  5. 由于我们这柱形一行有多个, 默认是鼠标悬浮到当前行,没有到我们的柱上,就会有 tooltip 提示, 可以设置shared 来匹配鼠标悬浮到柱形上才显示 tooltip
const { Bar } = G2Plot;
// 假数据
var busDataInfo = [
  {
    module: "BJ6123C7BTD",
    line: "8",
    wayLine: [
      {
        time: ["05:50", "06:30"],
        way: "E",
      },
      {
        time: ["06:42", "08:32"],
        way: "S",
      },
      {
        time: ["08:46", "10:39"],
        way: "E",
      },
      {
        time: ["14:06", "15:46"],
        way: "S",
      },
    ],
  },
  {
    module: "BJ6123C7BTD",
    line: "7",
    wayLine: [
      {
        time: ["05:00", "06:30"],
        way: "S",
      },
      {
        time: ["06:42", "08:32"],
        way: "E",
      },
      {
        time: ["08:46", "10:39"],
        way: "S",
      },
      {
        time: ["14:06", "15:46"],
        way: "E",
      },
    ],
  },
  {
    module: "BJ6123C7BTD",
    line: "6",
    wayLine: [
      {
        time: ["05:00", "06:00"],
        way: "S",
      },
      {
        time: ["06:12", "08:42"],
        way: "E",
      },
      {
        time: ["08:46", "09:09"],
        way: "S",
      },
      {
        time: ["14:06", "15:46"],
        way: "E",
      },
      {
        time: ["16:10", "18:00"],
        way: "S",
      },
      {
        time: ["18:34", "20:07"],
        way: "E",
      },
    ],
  },
];
// 数据改造 将上面的数据改造成我需要的数组格式
function changeData(data) {
  const outPutData = [];

  data.forEach((item) => {
    item.wayLine.forEach((every) => {
      outPutData.push({
        type: item.line,
        module: item.module,
        way: every.way,
        values: [
          ChangeStrToMinutes(every.time[0]),
          ChangeStrToMinutes(every.time[1]),
        ],
      });
    });
  });
  //TODO 这里是为了解决 颜色 处理部分循环进入两次的问题, 这里写在 上面的数据处理之前和之后 会有分别,分别在于 后面是否使用 reverse 反转数组
  outPutData.push({});
  return outPutData;
}
    
// 样式
const divStyles = {
  position: "absolute",
  background: "rgba(255,255,255,0.95)",
  boxShadow: "rgb(174, 174, 174) 0px 0px 10px",
  borderRadius: "4px",
  padding: "10px",
};
// 画布高度设置, 是根据数组数据长度,挂载点,以及高度 三个参数来进行设置
setHeigh(busDataInfo, "root", 60);
const data = changeData(busDataInfo);
// 画布实例化
const barPlot = new Bar("container", {
  barWidthRatio: 0.7, // 条形图宽度占比
  data: data,
  xField: "values",
  yField: "type",
  colorField: "type", // 部分图表使用 seriesField
  barStyle: (() => {
      var count = 0
      var arr = []
      return (arg) => {
        const { type = '' } = arg
        if (type === '') {
          count = count + 1
        }
        // 解决循环会进入两次的问题
        if (count !== 1) return
        arr.push(type)
        const currentArr = data.filter((item) => item.type === type)
        const filterArr = arr.filter((item) => item === type)
        // 获取当前往返类型
        const wayType = currentArr[filterArr.length - 1]?.way
        if (wayType === 'E') {
          return { radius: [10, 10, 0, 0] }
        } else if (wayType === 'M') {
          return { radius: [0, 0, 0, 0] }
        }
        return { radius: [0, 0, 10, 10] }
      }
  })(),
  color: ((arg) => {
    var count = 0;
    var arr = [];
    return (arg) => {
      const { type = "" } = arg;
      if (type === "") {
        count = count + 1;
      }
      // 解决循环会进入两次的问题
      if (count !== 1) return;
      arr.push(type);
      let currentArr = data.filter((item) => item.type === type);
      let filterArr = arr.filter((item) => item === type);
      // 获取当前往返类型
      const wayType = currentArr[filterArr.length - 1]?.way;
      console.log("type", type, wayType, currentArr);
      if (wayType === "E") {
        return "#FF5CA2";
      }
      return "#6C3E00";
    };
  })(),
  xAxis: {
    // x轴文本标签配置项
    label: {
      formatter: (value) => {
        return ChangeHourMinutestr(value);
      },
    },

    // 坐标轴网格线的配置项
    grid: {
      // alignTick:false,
    },
    // 坐标轴线样式
    // line:
    // {
    //   style:
    //   {
    //     stroke: 'black',
    //     lineWidth: 1,
    //     strokeOpacity: 0.7,
    //     shadowColor: 'black',
    //     shadowBlur: 10,
    //     shadowOffsetX: 5,
    //     shadowOffsetY: 5,
    //     cursor: 'pointer'
    //   }
    // },
    // 坐标轴刻度线样式
    tickLine: {
      length: 1,
      style: (item, index, items) => {
        return {
          stroke: "black",
          lineWidth: 2,
          lineDash: [4, 5],
          strokeOpacity: 0.7,
          shadowColor: "black",
          shadowBlur: 10,
          shadowOffsetX: 5,
          shadowOffsetY: 5,
          cursor: "pointer",
        };
      },
    },
    min: 270, // 坐标轴最小值  这里指分钟数
    tickCount: 20, // 坐标轴刻度数量 如果写了下面的刻度间隔, 则数量优先级变低
    tickInterval: 30, // 坐标轴刻度间隔
  },
  isRange: true,
  // 柱形上面的文字
  label: {
    content: "",
  },
  // barBackground: {
  //   style: {
  //     fill: '#000',
  //     fillOpacity: 0.01,
  //   }
  // },
  tooltip: {
    fields: ["type", "way", "module", "values"],
    // formatter:(data)=>{
    //   const { type,module,way,values} = data
    //   console.log('data',data);
    //   return {
    //     name:module,
    //     values:way
    //   }

    // },
    shared: false, // 只在鼠标悬浮到块上再展示
    showCrosshairs: false,
    customContent: (value, item) => {
      console.log("customContent", value, item[0]);
      if (!value || !item) return;
      const { data } = item[0];
      const container = document.createElement("div");
      setStyles(container, divStyles);
      container.innerHTML = `<div>车辆号码${data.type}</div><div>往返:${data?.way}</div>`;
      return container;
    },
  },
});

// 官方颜色主题
barPlot.update({
  theme: {
    styleSheet: {
      brandColor: "#FF4500",
      paletteQualitative10: [
        "#FF4500",
        "#1AAF8B",
        "#406C85",
        "#F6BD16",
        "#B40F0F",
        "#2FB8FC",
        "#4435FF",
        "#FF5CA2",
        "#BBE800",
        "#FE8A26",
      ],
      paletteQualitative20: [
        "#FF4500",
        "#1AAF8B",
        "#406C85",
        "#F6BD16",
        "#B40F0F",
        "#2FB8FC",
        "#4435FF",
        "#FF5CA2",
        "#BBE800",
        "#FE8A26",
        "#946DFF",
        "#6C3E00",
        "#6193FF",
        "#FF988E",
        "#36BCCB",
        "#004988",
        "#FFCF9D",
        "#CCDC8A",
        "#8D00A1",
        "#1CC25E",
      ],
    },
  },
});
挂载
barPlot.render();
  • 如果想查看本 demo 的效果, 可以把以上代码直接复制到 自己的 HTML 文件中, 运行即可
  • 参考 antv g2plot

这两天新增了需求,增加了一个字段,也就是一行当中可以多个颜色来显示,数据其实没有太大的改变,只不过图形处理逻辑上要特别是颜色那个部分增加一个判断即可 如下:

 color: ((arg) => {
var count = 0;
var arr = [];
return (arg) => {
  const { type = "" } = arg;
  if (type === "") {
    count = count + 1;
  }
  // 解决循环会进入两次的问题
  if (count !== 1) return;
  arr.push(type);
  let currentArr = data.filter((item) => item.type === type);
  let filterArr = arr.filter((item) => item === type);
  // 获取当前往返类型
  const wayType = currentArr[filterArr.length - 1] ?.way;
  if (wayType === "E") {
    return "#4435FF";
  }else if(wayType === "M"){
    return "#FF5CA2";
  }
  return "#1CC25E";
};
})(),

鼠标悬浮的提示我们可以在 customContent 方法中 根据参数的不同来判断一下就可以了 这个方法可以进一步封装,本人再封装一下 最终实现的效果如下:

iShot_2022-11-01_11.30.03.png

补充

  • 解决时间轴超过一天的问题 上面的代码逻辑中,在数据处理这一步,没有兼容超过0:00 的时间, 这里有两个方案, 我这里实现的就是暂时只判定 到达时间在某个时间段的, 正常来说这个应该在我们的数据中添加一个是否是第二天的车辆时间的一个字段,但我这里暂时没有那种需求,只需要判断到达时间大概在 0:00 到 3:00 之间的就可以了。
  • 先看一下我们修改之前是这样的: iShot_2023-05-15_10.16.50.png 解决方法其实就是,第二天时间假如是00:09 , 那我们就需要加上 24:00 , 先看解决以后 iShot_2023-05-15_10.11.59.png 这里只需要需改我们的数据改造方法:(这里需要注意,ChangeStrToMinutes('24:00:00') 这个方法我后来又改变, 这里到底是按分钟,还是按秒,大家根据业务来就行。)
export function handleData(res) {
  // 数据改造
  function changeData(data) {
    const outPutData = []
    data.forEach((item) => {
      let arrival = 0
      if (ChangeStrToMinutes(item.arrivalTime) < 200) {
        arrival = ChangeStrToMinutes('24:00:00') + ChangeStrToMinutes(item.arrivalTime)
      } else {
        arrival = ChangeStrToMinutes(item.arrivalTime)
      }
      outPutData.push({
        type: String(item.busId),
        module: item.busTypeName,
        way: item.arrivalStation,
        values: [ChangeStrToMinutes(item.departureTime), arrival]
      })
    })
    // 这里是为了解决 颜色 处理部分循环进入两次的问题, 这里写在 上面的数据处理之前和之后 会有分别,分别在于 后面是否使用 reverse 反转数组
    outPutData.push({})
    return outPutData
  }
  const data = changeData(res)
  data.sort((a, b) => a.type - b.type)
  console.log(
    'data',
    data.filter((item) => item.values?.includes(0))
  )
  return data
}