前端该如何实现绘制函数曲线

avatar
Web前端 @CVTE_希沃

希沃ENOW大前端

公司官网:CVTE(广州视源股份)

团队:CVTE旗下未来教育希沃软件平台中心enow团队

本文作者:

1323525a51f90e6d61510e34b67ee00.jpg

前言

不知道大家是否还记得怎么去画一条函数曲线,比如二次函数,三角函数等,上学时候在练习册上往往是取几个点,然后凭借对该函数的图像特征(定义域,值域,单调性,相交性等)进行连线,然后就有了对应函数的图像。

那么如何通过程序将函数对应的图像绘制出来呢?

我们无法得知当前需要绘制的函数的特征是怎样的,因此我们无法只取某几个点就画出函数图像。但是我们都知道线是由许多个点组成的,那么我们想要绘制函数线,只要提供足够多的点以达到当前画面上的拟合度就可以得到图像。

那么问题就转换成求出足够多能让函数线拟合平滑的点。考虑到曲线平滑以及绘制过程不在本次讨论范围,这里使用ECharts图像库绘制图像。ECharts可以把多个点处理成平滑曲线连接起来,其中涉及了贝塞尔曲线的知识,感兴趣可自行Google

整体可以分成两个步骤:

1、计算函数坐标点。

2、计算函数奇点。

以下Demo使用ECharts绘制得出。

计算函数坐标点

以反比例函数为例:

f(x)=k/x(k为常数,k0x0f(x)=k/x(k为常数,k≠0,x≠0)

这里取k=1。

先来回顾一下函数图像(图取自百度百科):

img

三下五除二,就写出了求点的函数:

function func(x) {
  return 1/x;
}
function generateData() {
  let data = [];
  for (let i = -200; i <= 200; i += 0.1) {
    data.push([i, func(i)]);
  }
  return data;
}
const pointList = geerateData()

得到了下面的图像:

img

我们知道反比例函数的定义域为x≠0,值域为y≠0,因此上图存在两个bug:

1、y值异常

2、x=0处有值

y值异常

观察循环知道,每次循环+0.1,0.1是最小正值,求得y=10,则由于x>0,函数单调递减,y便有了最大值10。

x=0处有值

我们并不知道函数定义域,因此无法知道某个点存在与否。就会导致在0处会有值,并产生了奇怪的连线。

以上两点问题,都可以通过求奇点(不存在的点)解决。

计算函数奇点

摘抄一下维基百科概念:

數學中,奇異點奇异点(英語:Singularity),是數學物件中無法定义的點。一般來說,可以分成兩種狀況:這個點的值在數學上沒有定義。例如,一個除以零的點。函數在的點,是一個奇異點;這個點有個性質-它趋向于無限。然而,在數學中,無限的值是沒有定義的。在物理中,也儘量避免或除去導致無限的點,雖然在宇宙学中有引力奇點(黑洞奇點)。或者,在某方面來說,這個點破壞了該數學物件的整體一致性。這個點被稱為病態的,是良态的反義。一般的例子是:1、光滑的曲線或平面(光滑函数)上的尖點,它破壞了該函數的可微性。2、連續的曲線中一個斷掉的點,它破壞了該曲線的連續性

总结在数学函数上就是导致函数不连续的点或者不存在的点。

那么对于

f(x)=1/xf(x)=1/x

而言,x=0处就是函数奇点。我们怎么去求得奇点呢?

我们清楚的知道

f(x)=1/xf(x)=1/x

的奇点是x=0,能否利用这个确切的点去做切入呢?既然我们知道函数f(x)的奇点,能否把任意函数通过f(x)来表示呢,这里复习一个复合函数的概念,摘自维基百科:

复合函数(英語:Function composition),又稱作合成函數,在数学中是指逐点地把一个函数作用于另一个函数的结果,所得到的第三个函数。

将任意函数g(x)用反比例函数的形式表示得:

g(x)=1/1/g(x)g(x)=1/1/g(x)⇒
g(x)=1/h(x),h(x)=1/g(x)g(x)=1/h(x),h(x)=1/g(x)

由上可得,存在x使得h(x)等于0即是g(x)的奇点。问题就转换成了求

1/g(x)=01/g(x)=0

的解。

求解1/g(x)=01/g(x)=0

函数求解也是无法与平时解数学题一样通过整理计算得出,这里还是用到了迭代遍历求解。

常见迭代有二分法,牛顿迭代法。这里由于牛顿迭代法特性,收敛更快,采用牛顿迭代法进行求解。关于牛顿迭代法可参考:马同学的回答。简单描述就是:

通过寻找一个初始点(x1,y1),做函数切线,得到切线与x轴的交点(x2,0),以函数上x=x2的点(x2,y2)作为下一次迭代的初始点,做函数切线,以此类推迭代,直到找到y=0或小于定义的误差值,即可认为该点就是解所在的点或无限接近解的点。

经过推导可得求点公式:

xn+1=xn  f(xn)f(xn)x_{n+1}=x_n\ -\ \frac{f\left(x_n\right)}{f`\left(x_n\right)}

采用强大的mathjs库来求函数的值:

简单编码:

public getSingularity = (fn, range) => {
    const x0 = range[0];
    const x1 = range[1];
    const reciprocal = `1 / (${fn})`;
    const expr = mathCompile(reciprocal);
    const derivative = mathDerivative(reciprocal, 'x');
    const threshold = 0.001;
    const minInterval = 0.001;

    const xArr = this.getXArr(x0, x1, 10);
    let xn = this.getStartX(xArr, expr, derivative);
    let xl = xn - 1;
    let hasSingularity = false;

    for (let i = 0; i < 1000; i++) {
      const y = expr.evaluate({ x: xn });

      if ((Math.abs(y) <= threshold && Math.abs(xn - xl) <= minInterval && i !== 0) || (i === 0 && y === 0) || Math.abs(y) === Infinity) {
        hasSingularity = true;
        break;
      } else {
        xl = xn;
        xn = this.loop(xn, expr, derivative);
      }
    }
    return hasSingularity ? xn : undefined;
  };

for循环内部if判断即是判断是否符合误差的值,符合即找到了解,也就是原始函数的奇点。由于初始值的取值可能导致越来越不收敛,因此需要限制收敛次数避免死循环,这里设置1000

当然这里只能求出一个解,我们知道类似正切函数有无数个奇点,这里可以留给大家去思考如何利用这个求单个解的思路求多个解,本质上也是一种选取初始值和控制误差值的过程。

总结

至此,总结如下:

1.遍历求得函数上的点。

2.通过转换函数并求解的方式来求函数奇点以绘制预期图像。

3.类似正切函数需要求多个解的情况可以通过控制选取初始值和误差值的方式尽可能求更多的解。

本篇文章只是描述了大概的绘制思路,希望可以把这些看到和总结出来的方法分享给大家,感谢阅读。