美文网首页
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