美文网首页
g2 实现带有虚线的平滑折线图

g2 实现带有虚线的平滑折线图

作者: VioletJack | 来源:发表于2023-03-23 12:09 被阅读0次

    先上效果

    折线图

    注册图形

    数据源如下

    [
      { x: '2020Q1', y: 17, legend: 'a', dashIndex: 3 },
      { x: '2020Q2', y: 15, legend: 'a', dashIndex: 3 },
      { x: '2020Q3', y: 12, legend: 'a', dashIndex: 3 },
      { x: '2020Q4', y: 15, legend: 'a', dashIndex: 3 },
      ....
    ]
    
    import { registerShape } from '@antv/g2'
    import { getSplinePath } from './path'
    
    /**
     * 注册断点折线图图形
     */
    export default function registerSplitLine() {
      registerShape('line', 'split-line', {
        draw(cfg, container) {
          const dashIndex = cfg.data[0].dashIndex
          const group = container.addGroup()
    
          // 折线图实现
          // const path1 = []
          // const path2 = []
          // for (let i = 0; i < cfg.points.length; i++) {
          //   const point = cfg.points[i]
          //   let pre = 'L'
          //   if (i === 0) {
          //     pre = 'M'
          //   }
          //   if (i <= dashIndex) {
          //     path1.push([pre, point.x, point.y])
          //   } else {
          //     path2.push([pre, point.x, point.y])
          //   }
          //   if (i === dashIndex) {
          //     path2.push(['M', point.x, point.y])
          //   }
          // }
    
          const { start, end } = this.coordinate
          const constraint = [
            [start.x, end.y],
            [end.x, start.y],
          ]
    
          // 过滤 null 的数据
          const filterdPoints = cfg.points.filter((point) => !Number.isNaN(point.y))
          const path = getSplinePath(filterdPoints, false, constraint)
    
          const path1 = path.slice(0, dashIndex + 1)
          const path2 = [
            ['M', cfg.points[dashIndex].x, cfg.points[dashIndex].y],
            ...path.slice(dashIndex + 1),
          ]
    
          const lineWidth = 2
          group.addShape('path', {
            attrs: {
              path: path1,
              lineWidth,
              lineCap: 'round',
              lineJoin: 'round',
              stroke: cfg.color,
            },
          })
          group.addShape('path', {
            attrs: {
              path: path2,
              stroke: cfg.color,
              lineWidth,
              lineCap: 'round',
              lineJoin: 'round',
              lineDash: [3, 3],
            },
          })
    
          return group
        },
      })
    }
    

    生成贝塞尔曲线的函数

    path.js

    /* eslint-disable */
    import { vec2 } from '@antv/matrix-util'
    import { each } from '@antv/util'
    
    /**
     * @ignore
     * 获取当前点到坐标系圆心的距离
     * @param coordinate 坐标系
     * @param point 当前点
     * @returns distance to center
     */
    export function getDistanceToCenter(coordinate, point) {
      const center = coordinate.getCenter()
      return Math.sqrt((point.x - center.x) ** 2 + (point.y - center.y) ** 2)
    }
    
    function _points2path(points, isInCircle) {
      const path = []
      if (points.length) {
        path.push(['M', points[0].x, points[0].y])
        for (let i = 1, length = points.length; i < length; i += 1) {
          const item = points[i]
          path.push(['L', item.x, item.y])
        }
    
        if (isInCircle) {
          path.push(['Z'])
        }
      }
    
      return path
    }
    
    function _convertArr(arr, coord) {
      const tmp = [arr[0]]
      for (let i = 1, len = arr.length; i < len; i = i + 2) {
        const point = coord.convert({
          x: arr[i],
          y: arr[i + 1],
        })
        tmp.push(point.x, point.y)
      }
      return tmp
    }
    function _convertArcPath(path, coord) {
      const { isTransposed } = coord
      const r = path[1]
      const x = path[6]
      const y = path[7]
      const point = coord.convert({ x, y })
      const direction = isTransposed ? 0 : 1
      return ['A', r, r, 0, 0, direction, point.x, point.y]
    }
    
    function _convertPolarPath(pre, cur, coord) {
      const { isTransposed, startAngle, endAngle } = coord
      const prePoint =
        pre[0].toLowerCase() === 'a'
          ? {
              x: pre[6],
              y: pre[7],
            }
          : {
              x: pre[1],
              y: pre[2],
            }
      const curPoint = {
        x: cur[1],
        y: cur[2],
      }
      const rst = []
      const xDim = isTransposed ? 'y' : 'x'
      const angleRange =
        Math.abs(curPoint[xDim] - prePoint[xDim]) * (endAngle - startAngle)
      const direction = curPoint[xDim] >= prePoint[xDim] ? 1 : 0 // 圆弧的方向
      const flag = angleRange > Math.PI ? 1 : 0 // 大弧还是小弧标志位
      const convertPoint = coord.convert(curPoint)
      const r = getDistanceToCenter(coord, convertPoint)
      if (r >= 0.5) {
        // 小于1像素的圆在图像上无法识别
        if (angleRange === Math.PI * 2) {
          const middlePoint = {
            x: (curPoint.x + prePoint.x) / 2,
            y: (curPoint.y + prePoint.y) / 2,
          }
          const middleConvertPoint = coord.convert(middlePoint)
          rst.push([
            'A',
            r,
            r,
            0,
            flag,
            direction,
            middleConvertPoint.x,
            middleConvertPoint.y,
          ])
          rst.push(['A', r, r, 0, flag, direction, convertPoint.x, convertPoint.y])
        } else {
          rst.push(['A', r, r, 0, flag, direction, convertPoint.x, convertPoint.y])
        }
      }
      return rst
    }
    
    // 当存在整体的圆时,去除圆前面和后面的线,防止出现直线穿过整个圆的情形
    function _filterFullCirleLine(path) {
      each(path, (subPath, index) => {
        const cur = subPath
        if (cur[0].toLowerCase() === 'a') {
          const pre = path[index - 1]
          const next = path[index + 1]
          if (next && next[0].toLowerCase() === 'a') {
            if (pre && pre[0].toLowerCase() === 'l') {
              pre[0] = 'M'
            }
          } else if (pre && pre[0].toLowerCase() === 'a') {
            if (next && next[0].toLowerCase() === 'l') {
              next[0] = 'M'
            }
          }
        }
      })
    }
    
    /**
     * @ignore
     * 计算光滑的贝塞尔曲线
     */
    export const smoothBezier = (points, smooth, isLoop, constraint) => {
      const cps = []
      const hasConstraint = !!constraint
    
      let prevPoint
      let nextPoint
      let min
      let max
      let nextCp0
      let cp1
      let cp0
    
      if (hasConstraint) {
        ;[min, max] = constraint
        for (let i = 0, l = points.length; i < l; i++) {
          const point = points[i]
          min = vec2.min([0, 0], min, point)
          max = vec2.max([0, 0], max, point)
        }
      }
    
      for (let i = 0, len = points.length; i < len; i++) {
        const point = points[i]
        if (i === 0 && !isLoop) {
          cp0 = point
        } else if (i === len - 1 && !isLoop) {
          cp1 = point
          cps.push(cp0)
          cps.push(cp1)
        } else {
          prevPoint = points[isLoop ? (i ? i - 1 : len - 1) : i - 1]
          nextPoint = points[isLoop ? (i + 1) % len : i + 1]
    
          let v = [0, 0]
          v = vec2.sub(v, nextPoint, prevPoint)
          v = vec2.scale(v, v, smooth)
    
          let d0 = vec2.distance(point, prevPoint)
          let d1 = vec2.distance(point, nextPoint)
    
          const sum = d0 + d1
          if (sum !== 0) {
            d0 /= sum
            d1 /= sum
          }
    
          let v1 = vec2.scale([0, 0], v, -d0)
          let v2 = vec2.scale([0, 0], v, d1)
    
          cp1 = vec2.add([0, 0], point, v1)
          nextCp0 = vec2.add([0, 0], point, v2)
    
          // 下一个控制点必须在这个点和下一个点之间
          nextCp0 = vec2.min([0, 0], nextCp0, vec2.max([0, 0], nextPoint, point))
          nextCp0 = vec2.max([0, 0], nextCp0, vec2.min([0, 0], nextPoint, point))
    
          // 重新计算 cp1 的值
          v1 = vec2.sub([0, 0], nextCp0, point)
          v1 = vec2.scale([0, 0], v1, -d0 / d1)
          cp1 = vec2.add([0, 0], point, v1)
    
          // 上一个控制点必须要在上一个点和这一个点之间
          cp1 = vec2.min([0, 0], cp1, vec2.max([0, 0], prevPoint, point))
          cp1 = vec2.max([0, 0], cp1, vec2.min([0, 0], prevPoint, point))
    
          // 重新计算 nextCp0 的值
          v2 = vec2.sub([0, 0], point, cp1)
          v2 = vec2.scale([0, 0], v2, d1 / d0)
          nextCp0 = vec2.add([0, 0], point, v2)
    
          if (hasConstraint) {
            cp1 = vec2.max([0, 0], cp1, min)
            cp1 = vec2.min([0, 0], cp1, max)
            nextCp0 = vec2.max([0, 0], nextCp0, min)
            nextCp0 = vec2.min([0, 0], nextCp0, max)
          }
    
          cps.push(cp0)
          cps.push(cp1)
          cp0 = nextCp0
        }
      }
    
      if (isLoop) {
        cps.push(cps.shift())
      }
    
      return cps
    }
    
    /**
     * @ignore
     * 贝塞尔曲线
     */
    export function catmullRom2bezier(crp, z, constraint) {
      const isLoop = !!z
      const pointList = []
      for (let i = 0, l = crp.length; i < l; i += 2) {
        pointList.push([crp[i], crp[i + 1]])
      }
    
      const controlPointList = smoothBezier(pointList, 0.4, isLoop, constraint)
      const len = pointList.length
      const d1 = []
    
      let cp1
      let cp2
      let p
    
      for (let i = 0; i < len - 1; i++) {
        cp1 = controlPointList[i * 2]
        cp2 = controlPointList[i * 2 + 1]
        p = pointList[i + 1]
    
        d1.push(['C', cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]])
      }
    
      if (isLoop) {
        cp1 = controlPointList[len]
        cp2 = controlPointList[len + 1]
        p = pointList[0]
    
        d1.push(['C', cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]])
      }
      return d1
    }
    
    /**
     * @ignore
     * 将点连接成路径 path
     */
    export function getLinePath(points, isInCircle) {
      return _points2path(points, isInCircle)
    }
    
    /**
     * @ignore
     * 根据关键点获取限定了范围的平滑线
     */
    export function getSplinePath(points, isInCircle, constaint) {
      const data = []
      const first = points[0]
      let prePoint = null
      if (points.length <= 2) {
        // 两点以内直接绘制成路径
        return getLinePath(points, isInCircle)
      }
      for (let i = 0, len = points.length; i < len; i++) {
        const point = points[i]
        if (!prePoint || !(prePoint.x === point.x && prePoint.y === point.y)) {
          data.push(point.x)
          data.push(point.y)
          prePoint = point
        }
      }
      const constraint = constaint || [
        // 范围
        [0, 0],
        [1, 1],
      ]
      const splinePath = catmullRom2bezier(data, isInCircle, constraint)
      splinePath.unshift(['M', first.x, first.y])
      return splinePath
    }
    
    /**
     * @ignore
     * 将归一化后的路径数据转换成坐标
     */
    export function convertNormalPath(coord, path) {
      const tmp = []
      each(path, (subPath) => {
        const action = subPath[0]
        switch (action.toLowerCase()) {
          case 'm':
          case 'l':
          case 'c':
            tmp.push(_convertArr(subPath, coord))
            break
          case 'a':
            tmp.push(_convertArcPath(subPath, coord))
            break
          case 'z':
          default:
            tmp.push(subPath)
            break
        }
      })
      return tmp
    }
    
    /**
     * @ignore
     * 将路径转换为极坐标下的真实路径
     */
    export function convertPolarPath(coord, path) {
      let tmp = []
      let pre
      let cur
      let transposed
      let equals
      each(path, (subPath, index) => {
        const action = subPath[0]
    
        switch (action.toLowerCase()) {
          case 'm':
          case 'c':
          case 'q':
            tmp.push(_convertArr(subPath, coord))
            break
          case 'l':
            pre = path[index - 1]
            cur = subPath
            transposed = coord.isTransposed
            // 是否半径相同,转换成圆弧
            equals = transposed
              ? pre[pre.length - 2] === cur[1]
              : pre[pre.length - 1] === cur[2]
            if (equals) {
              tmp = tmp.concat(_convertPolarPath(pre, cur, coord))
            } else {
              // y 不相等,所以直接转换
              tmp.push(_convertArr(subPath, coord))
            }
            break
          case 'a':
            tmp.push(_convertArcPath(subPath, coord))
            break
          case 'z':
          default:
            tmp.push(subPath)
            break
        }
      })
      _filterFullCirleLine(tmp) // 过滤多余的直线
      return tmp
    }
    

    记得安装依赖

    > yarn add @antv/matrix-util
    > yarn add @antv/util
    

    图形使用

    import registerSplitLine from '@/utils/splitLineRegister'
    
    registerSplitLine()
    
          const view1 = this.chart.createView()
          view1.data(chartData1)
          view1.line().position('x*y').color('#F6674F').shape('smooth')
    
          const view2 = this.chart.createView()
          view2.data(chartData2)
          ...
          view2.line().position('x*y').color('#666976').shape('split-line')
    

    最后

    生成贝塞尔曲线的代码我直接从 G2 的源码里面扒的,其实应该有第三方库支持这方面的计算。

    相关文章

      网友评论

          本文标题:g2 实现带有虚线的平滑折线图

          本文链接:https://www.haomeiwen.com/subject/frjirdtx.html