【JavaScript】y軸に平行な漸近線を描く

以前に関数のグラフを描きましたが,どんな関数でもちゃんとしたグラフになるわけではありません。$y$ 軸に平行な漸近線を持つ分数関数のグラフを描こうとするとおかしなことになります。たとえば,$f(x)=\cfrac{\cfrac{1}{2}x^3+x^2+x}{(2x+1)(x-3)}$ のグラフを描いてみるとこのようになります。

分数関数は漸近線をもちます。関数の分母の $(2x+1)(x-3)$ を $(2x+1)(x-3)=0$ として解を求めると $x=-\cfrac{1}{2},3$ となるので,関数はこれらの 2 か所で $y$ 軸に平行な漸近線をもちます。描かれたグラフを見ると,どうも漸近線の付近でおかしなことになっているようです。

関数は漸近線の付近で +∞ か -∞になります。本来ならグラフをつないではいけないところで無理やり直線を引いているのでこのようなことになっているのです。

漸近線を描く

そこで,コード上で漸近線を判定して直線をひかないようにし,代わりに漸近線を描くという処理を行ってみます。

<!DOCTYPE html>
<html>

<body>
  <script>
    //関数f(x)を定義する
    let f = x => (1 / 2 * x ** 3 + x ** 2 + x) / ((2 * x + 1) * (x - 3));
    let fdRight = x => (f(x + h) - f(x)) / h;
    let fdLeft = x => (f(x) - f(x - h)) / h;

    const Range = 18; // x軸両端の幅
    const range = Range / 400;
    let x = -Range / 2; //始点のx座標
    const h = range;

    //軸線を描く
    document.write('<svg width=400 height=400><line x1=0 y1=200 x2=400 y2=200 stroke="black"/><line x1=200 y1=0 x2=200 y2=400 stroke="black"/>');


    //関数のグラフを描く
    for (let i = 0; i < 400; i++) { //処理を400回繰り返し

      //漸近線の処理
      if (fdLeft(x) * fdRight(x) < -1) {
        //y軸に平行な漸近線を引く
        if (fdLeft(x + range) * fdRight(x + range) > 0) {
          document.write('<g transform="translate(200,200)scale(' + (1 / range) + ', ' + (-1 / range) + ')"><line x1=' + x + ' y1=' + Range / 2 + ' x2=' + x + ' y2=' + (-Range / 2) + ' stroke="green" stroke-width="' + range +
            '" stroke-dasharray=0.1 /></g>');
        }
      } else {
        //始点と終点の座標をもとに直線を引く
        document.write('<g transform="translate(200,200)scale(' + (1 / range) + ', ' + (-1 / range) + ')"><line x1=' + x + ' y1=' + f(x) + ' x2=' + (x + range) + ' y2=' + f(x + range) + ' stroke="blue" stroke-width="' + range * 2 + '" /></g>');
      }

      x += range; // xの値を次の点の座標にする

    }

    //最後にタグを閉じる
    document.write('</svg>');
  </script>
</body>

</html>

なんとかそれらしいグラフになりました。漸近線の付近でグラフが切れてしまっていますが,この問題は後日解決します。

漸近線の判定

    //関数f(x)を定義する
    let f = x => (1 / 2 * x ** 3 + x ** 2 + x) / ((2 * x + 1) * (x - 3));
    let fdRight = x => (f(x + h) - f(x)) / h;
    let fdLeft = x => (f(x) - f(x - h)) / h;

アロー関数を用いて関数を定義します。今回から書き方を省略しています。通常,関数は return 文を必要としますが,関数が 1 行で書ける場合には return を省略できます。ここでは式の計算結果をそのまま返すことになります。

また,f(x) に加え,右側微分を行う fdRight() 関数と,左側微分を行うfdLeft() 関数を定義しています。

以前に微分係数を求めるコードを書きましたが,それと同様です。数式としては

$\displaystyle fdRight(x)=\lim_{h\rightarrow0}\cfrac{f(x+h)-f(x)}{h}$

$\displaystyle fdLeft(x)=\lim_{h\rightarrow0}\cfrac{f(x)-f(x+h)}{h}$

ということです。

グラフを見てみると分かりますが,漸近線のところでグラフが大きく折れ曲がっています。数IIIの微分で右側微分と左側微分について学びますが,関数は右側微分と左側微分が異なる点では微分不能という定義がありました。同じように関数の値が±∞になる点でも微分不能です。

これを利用して右側微分と左側微分を求め,その値が異なる点を漸近線として判定します。

    const Range = 18; // x軸両端の幅
    const range = Range / 400;
    let x = -Range / 2; //始点のx座標
    const h = range;

グラフの座標の求め方も少し改良しています。$x$ 軸両端の幅を Range とします。$x$ の値は $-9$ から $+9$ まで変化します。これを 400 等分したものを range とします。これは画面上の 1 ピクセルに相当します。h は微分係数を求めるために使うものです。

      if (fdLeft(x) * fdRight(x) < -1) {

漸近線の判定は fdLeft(x) と fdRight(x) のかけ算によっておこないます。関数がなめらかにつながっているところでは右側微分と左側微分の正負は一致するのでかけ算の結果は正の値となります。一方で,関数が折れ曲がっているところでは右側微分と左側微分の正負が反対になるので,その点を漸近線ができる場所と判断します(ただし絶対値を含んだり,区間によって式が変わる関数には使えません)。

右側微分と左側微分で正負が反対になるなら,そのかけ算の結果は負の値になります。ここで判定を < $0$ としないで < $-1$ としているのは,コンピューターの計算が微小な誤差を含むため,なめらかにつながっている部分でも微小な負の値を返すことがあるからです。

漸近線判定の調節

        if (fdLeft(x + range) * fdRight(x + range) > 0) {

これは,次の点がなめらかにつながっていて漸近線ではないならば,という意味です。この文を加えているのはつじつま合わせです。

実際に微分を使って判定すると,漸近線の前後でコンピューターが「ここが漸近線だぞ」と判定するポイントが連続で2,3か所できます。このようになる原因はグラフの精度が悪いからです。

したがってグラフの精度を上げて判定ポイントを細かくすれば良いのですが,実際のところ精度を上げてもどこかでパーフェクトな結果に到達することはなく,計算量だけが増大することになります。

そこで判定ポイントのうち,最後の1つだけを漸近線として描画することで妥協します。

漸近線の描画

          document.write('<g transform="translate(200,200)scale(' + (1 / range) + ', ' + (-1 / range) + ')"><line x1=' + x + ' y1=' + Range / 2 + ' x2=' + x + ' y2=' + (-Range / 2) + ' stroke="green" stroke-width="' + range +
            '" stroke-dasharray=0.1 /></g>');

ここで実際に漸近線をひいています。$y$ 座標の Range / 2 はグラフの上端,(-Range / 2) はグラフの下端の座標です。

また,stroke-dasharray=0.1 という要素が加わっています。詳しい説明は省きますが,この値を変えることでさまざまな点線をひくことができます。

グラフの描画

      } else {
        //始点と終点の座標をもとに直線を引く
        document.write('<g transform="translate(200,200)scale(' + (1 / range) + ', ' + (-1 / range) + ')"><line x1=' + x + ' y1=' + f(x) + ' x2=' + (x + range) + ' y2=' + f(x + range) + ' stroke="blue" stroke-width="' + range * 2 + '" /></g>');
      }

else は条件にあてはまらないときの処理を書く部分です。ここでは漸近線ではないと判定された場合,ということになるので普通に関数のグラフを描いています。

こうして∞が関係する漸近線をひいてみると,結構難しいものです。コンピューターはあくまで実数でしか計算できないので,∞のような抽象的な値を扱うのは不得意である,というのが今回の教訓でしょうか。