【JavaScript】DOM要素とイベントリスナーを設置して積分区間を動かす

前回,定積分の近似値を求めるで積分区間を塗りつぶしたグラフを描きました。今回は積分区間を画面上で変えられるようにして,区間と面積の関係をどんどん変化させられるようにしたいと思います。

イベントリスナーの設置

今回作るページはこのような感じです。下に入力欄を設置して値を入力するとリアルタイムでグラフの積分区間が変化していくようにします。

これを実現するために用いるのがイベントリスナーです。イベントリスナーとは簡単に言うと,ブラウザ上でマウスがクリックされたとか今回のように入力欄の値が変わったとか,何らかの変化(イベント)が起こったときにそれを感知して処理を実行してくれるという仕組みです。

具体的な使い方はあとから紹介します。

DOM要素の操作

もう一つ学習する内容はDOM要素です。今回,DOM要素は一度書いたHTMLの内容をあとから書き換えるために使います。これを使ってグラフや定積分の結果を書き直します。これもあとで使い方を紹介します。

全体のコード

<!DOCTYPE html>
<html>

<body>
  <div id="graph"></div>
  <div id="interval">
    <p>区間:<input id="iStart" type="number" placeholder="0.2" step="0.1" /> から <input id="iEnd" type="number"
        placeholder="1.2" step="0.1" /> まで</p>
  </div>
  <div>
    <p>面積:<span id="result"></span></p>
  </div>

  <script>

    const Range = 3; // x軸両端の幅
    const range = Range / 400;

    //被積分関数を f(x)として定義する
    let f = x => -(x ** 3) - x ** 2 + x + 1;

    //積分の値をS(x)として定義する
    let S = (a, b) => {

      if (a > b) [a, b] = [b, a]; //aがbより大きいときは値入れ替え

      let sum = 0;  //sに計算結果を積み上げることで積分の値とする
      const n = 10 ** 4;  //∞の代わりに10の4乗を用いる
      const dx = (b - a) / n;

      for (let k = 0; k < n; k++) {
        const x = a + (k + 1) * dx;
        sum += Math.abs(f(x) * dx);  //sに計算結果を積み上げる
      }

      return sum;
    };

    //直線を描く関数
    let drawLine = obj => {

      let color, dotted;

      switch (obj.style) {
        case 'blue':
          color = 'blue';
          dotted = 0;
          break;
        case 'greenDotted':
          color = 'green';
          dotted = 0.1;
          break;
        case 'Orange':
          color = '#FFCC00';
          break;
      }

      return '<line x1=' + obj.X1 + ' y1=' + obj.Y1 + ' x2=' + obj.X2 + ' y2=' + obj.Y2 + ' stroke="' + color + '" stroke-width=' + range * 2 + ' stroke-dasharray=' + dotted + ' />';
    };

    //グラフを描画する関数
    let drawgraph = (a, b) => {

      if (a > b) [a, b] = [b, a]; //aがbより大きいときは値入れ替え

      //軸線を描く
      let text = '<div><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"/><g transform="translate(200,200)scale(' + (1 / range) + ', ' + (-1 / range) + ')">';

      let xy = {}; //描画する線の座標などを格納するオブジェクト
      let x = -Range / 2;

      for (i = 0; i < 400; i++) {
        //区間内であれば塗りつぶす
        if (x > a && x < b) {
          xy = {
            X1: x,
            Y1: f(x),
            X2: x,
            Y2: 0,
            style: 'Orange'
          };
          //f(x)の値が実数であれば塗りつぶす
          if (isNaN(xy.Y1) == false) text += drawLine(xy);
        }

        x += range;
      }

      x = -Range / 2;

      for (i = 0; i < 400; i++) {

        xy = {
          X1: x,
          Y1: f(x),
          X2: x + range,
          Y2: f(x + range),
          style: 'blue'
        };

        //始点と終点のどちらかが実数であれば直線をひく
        if (isNaN(xy.Y1) == false || isNaN(xy.Y2) == false) text += drawLine(xy);

        x += range;
      }

      xy = {  //積分区間の左端を示す直線
        X1: a,
        Y1: Range / 2,
        X2: a,
        Y2: -Range / 2,
        style: 'greenDotted'
      };

      text += drawLine(xy);

      xy = {  //積分区間の右端を示す直線
        X1: b,
        Y1: Range / 2,
        X2: b,
        Y2: -Range / 2,
        style: 'greenDotted'
      };

      text += drawLine(xy);

      text += '</g></svg></div>'; //タグを閉じる

      //グラフを描画する
      let element = document.querySelector('#graph');
      element.innerHTML = text;

      //面積の値を表示する
      element = document.querySelector('#result');
      element.innerHTML = S(a, b);

    };

    let a = 0.2, b = 1.2; //積分区間を[a,b]とする
    drawgraph(a, b); //グラフを描画する

    //テキストボックスに値を入力するとグラフを再描画する
    let iStartElement = document.querySelector('#iStart');
    iStartElement.addEventListener('input', (event) => {
      a = Number(iStartElement.value);
      drawgraph(a, b);
    });

    let iEndElement = document.querySelector('#iEnd');
    iEndElement.addEventListener('input', (event) => {
      b = Number(iEndElement.value);
      drawgraph(a, b);
    });

  </script>
</body>

</html>

講座が進むにつれ,だんだんコードが長くなっているのでどうしたものか・・・。

<div>を使う

  <div id="graph"></div>
  <div id="interval">
    <p>区間:<input id="iStart" type="number" placeholder="0.2" step="0.1" /> から <input id="iEnd" type="number"
        placeholder="1.2" step="0.1" /> まで</p>
  </div>
  <div>
    <p>面積:<span id="result"></span></p>
  </div>

DOMを使うために HTML の部分に<div>~</div>を配置していきます。これをブロック要素と呼び,<div>~</div>で囲まれた部分を一つのブロックとします。ブロックには id が付けられ,それによってブロックを区別しています。

  <div id="graph"></div>

id が graph となっているブロック要素は中身が何もありませんが,ここにあとからグラフを描いていきます。言い換えれば graph という名前を付けた空箱を用意しておいて,あとからここに中身を放り込もうということです。

  <div id="interval">
    <p>区間:<input id="iStart" type="number" placeholder="0.2" step="0.1" /> から <input id="iEnd" type="number" placeholder="1.2" step="0.1" /> まで</p>
  </div>

次に interval という名前の箱を用意します。ここは積分区間の入力欄を放り込んでいます。<input ~ /> の細かい説明は省きますが,iStart という名前を付けたほうに積分区間の始点,iEnd のほうに積分区間の終点を入力します。ここで入力された内容をあとで利用するために,とりあえず id で名前をつけておくことが大切です。

  <div>
    <p>面積:<span id="result"></span></p>
  </div>

最後にもう一つブロックを用意しています。このブロック要素には名前をつけていませんが,代わりに<span>~</span>の部分に result という名前をつけています。これもまた中身のない箱として用意していますが,あとから定積分の結果を放り込んでいきます。

ここで<div>と<span>という二つの種類の箱が出てきて混乱するかもしれません。<div>は文章で言えば段落のようなもので,</div>の部分で改行され次の行に移ります。<span>は言わば文中の文字のようなものです。今回は「面積:~」のような形で表示したいので,~の部分に<span>を用いています。もしこれを<div>にするとその手前で改行されてしまいます。

もう一つ,<p>~</p>という要素がありますが,こちらが本来の段落を示すものです。<p>~</p>で囲んだ部分が一つの段落であることを示しています。<div>もそれっぽい働きをしますが,あくまで<div>はブロック要素であって段落ではない(違いが分かりにくいですが),そういうものです。

ここまでは HTML 言語による記述です。このコードは HTML言語とJavaScript言語のバイリンガルになっていて,次の行からJavaScriptに入っていきます。

関数の定義

    const Range = 3; // x軸両端の幅
    const range = Range / 400;

今回はグラフの幅を 3 とします。よって,x 軸の左端は -1.5,右端は +1.5 です。グラフは 400 ピクセル幅で表示するので,x 軸の幅を 400 分割してグラフ上の 1 ピクセルに相当する x の変位量を range として定義しておきます。

    //被積分関数を f(x)として定義する
    let f = x => -(x ** 3) - x ** 2 + x + 1;

積分する関数を $f(x)=-x^3-x^2+x+1$ として定義します。

    //積分の値をS(x)として定義する
    let S = (a, b) => {

      if (a > b) [a, b] = [b, a]; //aがbより大きいときは値入れ替え

      let sum = 0;  //sに計算結果を積み上げることで積分の値とする
      const n = 10 ** 4;  //∞の代わりに10の5乗を用いる
      const dx = (b - a) / n;

      for (let k = 0; k < n; k++) {
        const x = a + (k + 1) * dx;
        sum += Math.abs(f(x) * dx);  //sに計算結果を積み上げる
      }

      return sum;
    };

定積分の値を S(a,b) として定義します。a,b は f(x) を区間 [a, b] において定積分するという意味です。もう少し細かく見ていきましょう。

      if (a > b) [a, b] = [b, a]; //aがbより大きいときは値入れ替え

今回は積分区間をあとから変更できるようにしています。そのため,入力する値によっては積分区間がひっくりかえることがあります。この文の意味は「もし a が b より大きければ a に b を代入し,b に a を代入しろ」ということです。つまり a と b の値を入れ替えるわけです。積分区間が [2, 0] みたいなことになれば,値を入れ替えて [0, 2] の区間で定積分します。

      let sum = 0;  //sに計算結果を積み上げることで積分の値とする
      const n = 10 ** 4;  //∞の代わりに10の4乗を用いる
      const dx = (b - a) / n;

      for (let k = 0; k < n; k++) {
        const x = a + (k + 1) * dx;
        sum += Math.abs(f(x) * dx);  //sに計算結果を積み上げる
      }

      return sum;
    };

この部分に関しては,前回の説明を参照してください。区分求積法を用いてグラフを小さな長方形に分割し,その面積を sum に足し合わせていくことで定積分の結果を求めていきます。

Math.abs() は絶対値を求める関数です。通常,積分では x 軸より下側はマイナスの値として計算結果が出てきます。ここでは面積を求めるということにして,マイナスの部分もプラスの値として合計していきます。

直線をひく関数

    //直線を描く関数
    let drawLine = obj => {

      let color, dotted;

      switch (obj.style) {
        case 'blue':
          color = 'blue';
          dotted = 0;
          break;
        case 'greenDotted':
          color = 'green';
          dotted = 0.1;
          break;
        case 'Orange':
          color = '#FFCC00';
          break;
      }

      return '<line x1=' + obj.X1 + ' y1=' + obj.Y1 + ' x2=' + obj.X2 + ' y2=' + obj.Y2 + ' stroke="' + color + '" stroke-width=' + range * 2 + ' stroke-dasharray=' + dotted + ' />';
    };

関数の値をもとに直線をひく部分です。ここでは始点・終点の座標などが入ったオブジェクトを obj として受け取り,直線をひく <line ~ /> 文を返します。ここでは実際に直線をひいているわけではなく,直線をひく HTML の文を返しています。あとからこの結果を受け取って実際に直線を描画します。

    //グラフを描画する関数
    let drawgraph = (a, b) => {

グラフを描画する関数を drawgraph として定義します。a, b は積分区間を表します。今回はあとから何度もグラフを描きなおすことになるので,関数として定義してあとから呼び出します。

グラフを描画する

      //グラフを描画する
      let element = document.querySelector('#graph');
      element.innerHTML = text;

      //面積の値を表示する
      element = document.querySelector('#result');
      element.innerHTML = S(a, b);

グラフの描画の部分も基本的には以前と同じものなのです。今までは document.write で随時画面に書き込んでいっていたのですが,ここでは text という変数に文字列を追加していき,最後に一気に書き込むようになっています。

ここで用意した element は変数ではなくオブジェクトです。element = document.querySelector(‘#graph’); は「id が graph であるブロック要素について,さまざまな情報を element という箱の中に入れる。」という意味です。

実際,element の中にはいろいろな情報が入ってくるのですが,今回はその中でも innerHTML というラベルがついた紙だけ取り出します。最初は id=graph のブロック要素は空っぽなので白紙の紙があると思ってください。そこに element.innerHTML = text とすることで文字列を書き込みます。text にはグラフを描画するための HTML文が入っているので,これで画面にグラフが表示されるわけです。

次に id が result であるインライン要素(<span>~</span>で囲まれた部分はインライン要素と呼びます)は,element.innerHTML = S(a, b) として定積分の結果を書き込みます。

これらをDOM要素の操作と言います。言葉が難しそうですが,基本的な操作はそれほどやっかいなものではありません。理屈よりも使い方を先に覚えてしまったほうが良いでしょう。

    let a = 0.2, b = 1.2; //積分区間を[a,b]とする
    drawgraph(a, b); //グラフを描画する

ここで,積分区間 a,b を設定して,上で作ったグラフを描画する関数を呼び出します。drawgraph(a, b); で「グラフを描いて区間 a,b の定積分の結果を表示してね。」という指示を実際に出しています。

イベントリスナーの部分

    //テキストボックスに値を入力するとグラフを再描画する
    let iStartElement = document.querySelector('#iStart');
    iStartElement.addEventListener('input', (event) => {
      a = Number(iStartElement.value);
      drawgraph(a, b);
    });

    let iEndElement = document.querySelector('#iEnd');
    iEndElement.addEventListener('input', (event) => {
      b = Number(iEndElement.value);
      drawgraph(a, b);
    });

最後にイベントリスナーを設置しています。

    let iStartElement = document.querySelector('#iStart');

この部分は先ほどのDOM要素の操作の部分と同じで,iStartElement というオブジェクトを作ってその中に id が iStart の要素(<input ~/>の部分)の情報を放り込みます。<input ~/>は積分区間を入力する部分です。

    iStartElement.addEventListener('input', (event) => {

ここがイベントリスナーです。’input’ というのは「入力欄に情報が書き込まれたら」という意味です。(event) => { はこれまでも使ってきたアロー関数です。この書き方は関数に名前をつけず event というオブジェクトを関数に渡すということなのですが,とりあえずそういうものなのだ,というくらいに思ってください。実際のところ関数に渡した event というオブジェクトは今回は使っていないので,() と書いて省略することもできます。

これまでオブジェクトはいろいろな情報が入った箱であると説明してきましたが,箱の中には情報が書かれた紙だけではなく,何かの作業をしてくれるロボット(関数)を入れることもできます。iStartElement.addEventListener という表現は iStartElement という名前の箱の中にある addEventListener というロボットを指しています。このロボットは入力欄に情報が書き込まれると,アロー関数の部分の処理を行ってくれます。

      a = Number(iStartElement.value);

ロボットが行ってくれる処理を見てみます。iStartElement というオブジェクトには入力欄に入ってきた文字が書いてある紙があり,value というラベルがついています。 したがって,iStartElement.value は入力された文字を指しています。これを Number() に入れて,文字を数字として扱うようにします。面倒なのですが,これをやっておかないとコンピューターは入力された文字を数字として認識せず「数字じゃないから計算できません」みたいなことを言ってくるので,Number() を使う必要があります。そして入力された数字を a に代入します。

      drawgraph(a, b);

あとは drawgraph を呼び出してグラフと定積分の結果を書き直してもらいます。

    let iEndElement = document.querySelector('#iEnd');
    iEndElement.addEventListener('input', (event) => {
      b = Number(iEndElement.value);
      drawgraph(a, b);

この部分は上とほとんど同じですが,今度は積分区間の b のほうに数字が入力されたらグラフと定積分の結果を書き直してもらいます。