如何获取echarts曲线任意x位置的y值

188 阅读3分钟

echarts可以将离散点绘制成光滑曲线,那么是否能够根据该曲线拟合出任意x位置的y值呢?

首先我们看这个拟合值是否有意义——光滑曲线往往比折线更接近离散点的实际分布,比如以sin函数正态分布的点,echarts最终绘制出来的曲线和该sin函数的拟合度非常高,因此我们可以一定程度上参考该曲线拟合值。

假设能找到该echarts曲线的函数,那么就可以求解出任意拟合值了。要找到这个函数,首先要理解echarts是如何绘制曲线的。

echarts是如何绘制曲线的

ecahrts曲线绘制方法在源码src/chart/line/poly.ts中,

function drawSegment(
  ctx: PathProxy,
  points: ArrayLike<number>,
  start: number,
  segLen: number,
  allLen: number,
  dir: number,
  smooth: number,
  smoothMonotone: "x" | "y" | "none",
  connectNulls: boolean
) {
  
        ...
        ctx[dir > 0 ? "moveTo" : "lineTo"](x, y);
        ...
        ctx.bezierCurveTo(cpx0, cpy0, cpx1, cpy1, x, y);
        ...
        ctx.lineTo(x, y);
     
  }

  return k;
}

显然它调用了canvas的贝塞尔曲线绘制。

canvas 绘制曲线

bezierCurveTo方法用于将三次贝赛尔曲线添加到当前子路径中。该方法需要三个点:前两个点是控制点,第三个点是结束点。起始点是当前路径的最后一个点。

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

由此我们知道了echarts的光滑曲线是三次贝塞尔曲线,也即获得了该曲线的函数:

B(t)=(1t)3P0+3(1t)2tP1+3(1t)t2P2+t3P3B(t) = (1 - t)³ * P0 + 3 * (1 - t)² * t * P1 + 3 * (1 - t) * t² * P2 + t³ * P3

那么接下来的任务就是如何获取x位置的控制点了。

如何获取目标x位置所在曲线的控制点

观察drawSegment,可以看到该函数通过传入的ctx参数绘制曲线,那么我们只要将ctx换成一个收集对象,再模拟曲线绘制,就能得到所有的曲线路径了。

收集对象关键代码如下:

const pathData: PathCMD = [];

const ctx = {

    moveTo: (x: number, y: number) => {
        pathData.push([CMD.M, [x, y]]);
    },
    lineTo: (x: number, y: number) => {
        pathData.push([CMD.L, [x, y]]);
    },
    bezierCurveTo: (cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number) => {
        pathData.push([CMD.C, [cp1x, cp1y], [cp2x, cp2y], [x, y]]);
    },

} as unknown as CanvasRenderingContext2D;

之后只要遍历pathData,就能找到x所在的线段的绘制命令以及控制点了。

到此我们就只剩下一个问题没有解决了——如何求解三次贝塞尔方程。

如何求解三次贝塞尔方程

针对三次贝塞尔方程:

B(t)=(1t)3P0+3(1t)2tP1+3(1t)t2P2+t3P3B(t) = (1 - t)³ * P0 + 3 * (1 - t)² * t * P1 + 3 * (1 - t) * t² * P2 + t³ * P3

已知P0,P1,P2,P3四个控制点的x和y值,以及目标B(t)的x值,求B(t)的y值。要求解这个函数,首先需要求解出t值。

我们将x带入,得到一个一元三次方程:

X=(1t)3X0+3(1t)2tX1+3(1t)t2P2+t3X3X = (1 - t)³ * X0 + 3 * (1 - t)² * t * X1 + 3 * (1 - t) * t² * P2 + t³ * X3

如何求解t值

由于t值的取值范围为0~1,因此我们可以在某个精度下多次迭代推测t的值。比如使用二分法牛顿迭代法

牛顿迭代法简介

设 xn 是方程 f(x)=0 的第 n 次近似根,则第 n+1 次近似根为:xn+1=xnf(xn)f(xn)设 x_n 是方程 f(x) = 0 的第 n 次近似根,则第 n+1 次近似根为:x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

牛顿迭代法的优点是收敛快,不过在实测中求解速度并不如二分法快。

不理解牛顿迭代法也没有关系,因为我也没有采用这个方法~

回到方程本身,那么一元三次方程有没有类似一元二次方程的通用求根公式呢?有的。

一元三次方程的通用解法

一元三次方程的标准形式为:ax3+bx2+cx+d=0(a0)ax^3 + bx^2 + cx + d = 0 \quad (a \neq 0)

通过变量代换 x=yb3ax = y - \frac{b}{3a},可消去二次项,将方程化为 缺省型三次方程y3+py+q=0y^3 + py + q = 0 其中 p=3acb23a2q=2b39abc+27a2d27a3p = \frac{3ac - b^2}{3a^2},q = \frac{2b^3 - 9abc + 27a^2d}{27a^3}

针对该弱三次方程,16实际卡尔达诺就找到了通用解法,即卡尔达诺公式。但相比该公式,我们可以使用更简洁的盛金公式求解t值。

盛金公式介绍

盛金公式通过计算 判别式 A、B、C 和 总判别式 Δ\Delta 来判断根的情况:

  1. 判别式定义{A=b23acB=bc9adC=c23bdΔ=B24AC\begin{cases} A = b^2 - 3ac \\ B = bc - 9ad \\ C = c^2 - 3bd \\ \Delta = B^2 - 4AC \end{cases}

  2. 根的分类与对应公式

    • 情况 1:Δ>0\Delta > 0 (有一个实根和两个共轭虚根)
    • 情况 2:Δ=0\Delta = 0 (有三个实根,其中至少两个相等)
    • 情况 3:Δ<0\Delta < 0 (有三个互不相等的实根)

判断规则如下:

1. 当 Δ>0\Delta > 0 时

  • 实根公式:x1=bY13Y233ax_1 = \frac{-b - \sqrt[3]{Y_1} - \sqrt[3]{Y_2}}{3a} 其中:{Y1=b3+9abd227a2d2+BΔ2Y2=b3+9abd227a2d2BΔ2\begin{cases} Y_1 = b^3 + \frac{9abd}{2} - \frac{27a^2d}{2} + \frac{B\sqrt{\Delta}}{2} \\ Y_2 = b^3 + \frac{9abd}{2} - \frac{27a^2d}{2} - \frac{B\sqrt{\Delta}}{2} \end{cases}
  • 虚根可通过共轭复数形式表示,需注意 Y13\sqrt[3]{Y_1} 和 Y23\sqrt[3]{Y_2} 取实数根。

2. 当 Δ=0\Delta = 0 时

  • 若 A=B=0A = B = 0(三重实根):x1=x2=x3=b3ax_1 = x_2 = x_3 = -\frac{b}{3a}
  • 若 A0A \neq 0(有一个二重实根和一个单实根):{x1=b+K3ax2=x3=bK6a,其中 K=BA\begin{cases} x_1 = \frac{-b + K}{3a} \\ x_2 = x_3 = \frac{-b - K}{6a} \end{cases}, \quad \text{其中 } K = \frac{B}{A}

3. 当 Δ<0\Delta < 0 时(三个实根,需用三角函数表示)

  • 令 θ=13arccosT\theta = \frac{1}{3} \arccos T,其中 T=2B39ABC+27A2D2A3/2T = \frac{2B^3 - 9ABC + 27A^2D}{2A^{3/2}}(此处 D=d/aD = d/a),则:{x1=b2Acosθ3ax2=b+A(cosθ+3sinθ)3ax3=b+A(cosθ3sinθ)3a\begin{cases} x_1 = \frac{-b - 2\sqrt{A} \cos\theta}{3a} \\ x_2 = \frac{-b + \sqrt{A} (\cos\theta + \sqrt{3}\sin\theta)}{3a} \\ x_3 = \frac{-b + \sqrt{A} (\cos\theta - \sqrt{3}\sin\theta)}{3a} \end{cases}

最后

至此,我们得到了echarts曲线任意x位置的y值的拟合方法。

在找求解方法的过程中还有很多非常有趣的点,比如

  • echarts是如何保证曲线光滑的(光滑曲线的数学证明)
  • 贝塞尔曲线的特性和公式的推导过程
  • 牛顿迭代法的原理和缺陷
  • 卡尔达诺本人以及卡尔达诺公式的证明

上学时老师讲的数学之美,在我毕业10年后终于能够理解到了一些。