先上效果
![](https://img.haomeiwen.com/i1987062/a4b8c8050d88c9d7.png)
注册图形
数据源如下
[
{ 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 的源码里面扒的,其实应该有第三方库支持这方面的计算。
网友评论