HTML5 CanvasとJavaScriptで折線グラフを描いてみた
使用した環境は次のとおり。
- Chromium Version 90.0
- Firefox Browser 78.13.0esr
- ECMAScript 6 (2015)以降
ECMAScriptはJavaScriptの標準規格。そのバージョンの6以降または2015以降では変数の宣言にletというキーワードが導入された。定数の宣言にはconstというキーワードが導入された。ただしletは予約語ではない。この投稿で掲載するJavaScriptのコードではletとconstを用いた。
JavaScriptにおける変数の詳細についてはまた別の機会に。
HTML5 Canvasで描いた折線グラフ
日本のマクロ経済データの1つであるインフレ率を1960年から2020年まで時系列に折線グラフにしたものが次の図。
スクリプト言語JavaScriptとHTML5 Canvasの2次元用レンダリングのAPIを利用して今回はこの折線グラフを描いてみた。
HTML
HTML5 Canvasを表示するために用意しておくHTML文書の基本的内容は次のとおり。
<!DOCTYPE html>
<html>
<head>
<title>日本のインフレ率の折線グラフ</title>
</head>
<body>
<!-- canvas要素にはid属性と幅と高さを設定する -->
<canvas id="JapanInflationRateTimeSeries" width="700" height="400">
このブラウザはHTML5 CanvasかJavaScriptをサポートしていません。
</canvas>
</body>
<script>
<!-- ここにJavaScriptのコードを書く -->
</script>
</html>
ウェブ・ブラウザによってscript要素が最後に読み込まれることを期待してscript要素をbody要素の後に置くことにした。
HTML文書内にスクリプト(JavaScriptのソースコード)を直接埋め込まずに別ファイルに分離させる場合には、HTMLのscript要素のsrc属性にそのソースコードのファイル名とパス名を指定する。例えば次のように。
<script src="./script/JapanInflationRateTimeSeries.js"></script>
body要素内に置いたcanvas要素にはそのcanvas要素固有のidを付けておく必要がある。なぜならばJavaScriptからこのcanvas要素を呼び出すためにこのid属性を用いるので。
canvas要素のwidth属性にはCanvasの広さを決めるための幅の値を指定する。ここでは700とした。hight属性は高さ。ここでは400とした。描かれた図の一部がはみ出して隠れてしまわないようにその都度これらを調整する。
HTMLのhead要素内のstyle要素内にCSSを記述してcanvas要素の背景色やら余白やらを設定する方法もあるが、今回はJavaScriptのコードにそれらの設定と描画を任せてみることにした。
HTML5 CanvasのAPIが用いる座標系では左上端が原点の(0,0)となる。しかしここで描こうとするグラフの座標系は通常のデカルト座標系に基づくものなので、HTML5 Canvas APIが用いる座標系とは上下の向きが逆になる。
次にJavaScriptのソースコードの内容について説明する。
このスクリプトの設計思想とコードの全体像
このスクリプトは大規模でも複雑でもないので、その設計手法はオブジェクト指向的ではなく、C言語の古典的な流儀に近いものにした。
ただし、函数の定義の中に別の函数の定義が入っている入れ子構造はC言語の古典的な流儀とは異なるかもしれない。函数の定義の中で更に定義された入れ子の函数はクロージャと呼ばれているらしい。
ソースコードの内容は、函数外の各定数、各変数、そして複数の各函数によって構成されている。
(function() {
/* 各種定数の定義
各種変数の宣言と値の割り当て
各種函数の定義
各種函数の呼び出し */
})();
コードを函数に分けることの利点は、コードの見通しがよくなるだけでなく、識別子の名前が重複して上書きされてしまったりするのを函数のブロックを名前空間として使用して防ぐことができること。
他のスクリプトとの競合を防ぐためにコード全体を匿名函数で更に袋とじにすることにした。(function(){...})();
の部分がそれ。
変数の宣言にはvarキーワードの代わりにconstキーワードとletキーワードを用いることにした。これらのキーワードはECMAScript 2015 (ES6)から採用されたらしい。ただしletは予約語ではないようだ。
constで定義した変数は定数であるかのように機能し、letで宣言した変数は全てのブロック内でローカル変数として機能するようになるらしい。
constキーワードで定義される定数は厳密には定数とは異なるらしい。その詳細についてはまたの機会に。
for文とif文だけを制御構文として用いた。
残念ながら汎用性や拡張性が低いコードになっている。機会があれば修正するかもしれない。
函数外の定数を宣言
まずは函数の間を跨いで働く各種の定数を函数外で宣言した。定数の宣言にはconstキーワードを用いる。定数の宣言と同時にその値を代入する必要がある。
// 方眼紙の属性定数
const topMargin = 35; // 上余白
const leftMargin = 30; // 左余白
const verticalGridNum = 65; // 方眼紙の垂直線数
const horizontalGridNum = 30; // 方眼紙の水平線数
const gridSize = 10; // 方眼紙の線間の幅
// 座標系の原点位置を決める定数
const originX = leftMargin + (gridSize * 0); // x座標
const originY = topMargin + (gridSize * 25); // y座標
// 横軸線の始点
const xAxisSX = originX;
const xAxisSY = topMargin;
// 横軸線の終点
const xAxisLX = originX;
const xAxisLY = topMargin + (gridSize * horizontalGridNum);
// 縦軸線の始点
const yAxisSX = leftMargin;
const yAxisSY = originY;
// 縦軸線の終点
const yAxisLX = leftMargin + (gridSize * verticalGridNum);
const yAxisLY = originY;
// インフレ率の数値データ配列定数
const dataSet = [3.57,5.36,6.83,6.7,3.8,6.65,5.04,3.98,5.33,
5.24,6.92,6.39,4.84,11.6,23.22,11.73,9.37,8.16,
4.2,3.7,7.77,4.91,2.74,1.89,2.26,2.03,0.59,0.12,
0.67,2.27,3.07,3.25,1.76,1.24,0.69,-0.12,0.13,
1.74,0.66,-0.34,-0.67,-0.74,-0.92,-0.25,0,-0.28,
0.24,0.06,1.38,-1.35,-0.71,-0.26,-0.05,0.34,2.76,
0.78,-0.11,0.46,0.97,0.47,-0.01];
// データをプロットした座標を保管する配列定数
const plotXSet = [];
const plotYSet = [];
各定数の意味は次のとおり。
- topMargin
- 方眼紙の上側の余白の広さ。
- leftMargin
- 方眼紙の左側の余白の広さ。
- verticalGridNum
- 方眼紙を構成する垂直の線の数。
- horizontalGridNum
- 方眼紙を構成する水平の線の数。
- gridSize
- 方眼紙を構成する格子状の線と線の間の広さ。
- originX
- グラフの座標系の原点を表すx座標。
- originY
- グラフの座標系の原点を表すy座標。
- xAxisSX
- グラフの座標系の横軸(x軸)の始点を表すx座標。
- xAxisSY
- グラフの座標系の横軸(x軸)の始点を表すy座標。
- xAxisLX
- グラフの座標系の横軸(x軸)の終点を表すx座標。
- xAxisLY
- グラフの座標系の横軸(x軸)の終点を表すy座標。
- yAxisSX
- グラフの座標系の縦軸(y軸)の始点を表すx座標。
- yAxisSY
- グラフの座標系の縦軸(y軸)の始点を表すy座標。
- yAxisLX
- グラフの座標系の縦軸(y軸)の終点を表すx座標。
- yAxisLY
- グラフの座標系の縦軸(y軸)の終点を表すy座標。
- dataSet
- インフレ率の各値が時系列に並んだ配列定数。
- plotXSet
- インフレ率の各値をプロットした位置のx座標を入れる配列定数。
- plotYSet
- インフレ率の各値をプロットした位置のy座標を入れる配列定数。
函数外の変数を宣言して値を代入
プログラムの実行途上で値が変化する場合はletキーワードを使って変数として宣言した。
// 垂直方向の方眼線の始点
let verticalLineSX = leftMargin; // x座標
let verticalLineSY = topMargin; // y座標
// 垂直方向の方眼線の終点
let verticalLineLX = leftMargin; // x座標
let verticalLineLY = topMargin +
(gridSize * horizontalGridNum); // y座標
// 水平方向の方眼線の始点
let horizontalLineSX = leftMargin; // x座標
let horizontalLineSY = topMargin; // y座標
// 水平方向の方眼線の終点
let horizontalLineLX = leftMargin +
(gridSize * verticalGridNum); // x座標
let horizontalLineLY = topMargin; // y座標
これらをconstキーワードで定義した場合、コンソール上にエラーが表示されてしまう。
各変数の意味は次のとおり。
- verticalLineSX
- 垂直方向に伸びる方眼線の始点を表すx座標。
- verticalLineSY
- 垂直方向に伸びる方眼線の始点を表すy座標。
- verticalLineLX
- 垂直方向に伸びる方眼線の終点を表すx座標。
- verticalLineLY
- 垂直方向に伸びる方眼線の終点を表すy座標。
- horizontalLineSX
- 水平方向に伸びる方眼線の始点を表すx座標。
- horizontalLineSY
- 水平方向に伸びる方眼線の始点を表すy座標。
- horizontalLineLX
- 水平方向に伸びる方眼線の終点を表すx座標。
- horizontalLineLY
- 水平方向に伸びる方眼線の終点を表すy座標。
canvasの背景色を描く函数
canvasの背景色を白にするための函数を定義した。
/* Canvasの背景色を描画する函数 */
function drawBackgroundOfCanvas(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// canvasの背景色設定
ctx.fillStyle = "white"; // 背景の色
ctx.fillRect(0, 0, canvas.width, canvas.height); // 長方形
}
函数の定義はfunction 函数名(引数){ステートメント1; ステートメント2}
のような構成になる。
この函数はHTMLのcanvas要素のid属性をその引数に受け取る。
この函数ブロック内の始めの2行はHTML5 Canvasの2次元のAPIを利用するときにお馴染みのステートメント。getElementByIdメソッドで特定のid属性を持つcanvas要素を取得し、getContextメソッドで2次元のCanvas APIを利用できるようにするもの。
いずれもconstキーワードを使ってオブジェクト定数のようなものにした。こうすることでctxという名前を与えたオブジェクトが提供するメソッドやプロパティを利用してcanvasに2次元の図を描けるようになった。
fillStyleプロパティには塗りつぶすときの色を指定する。ここでは白色にした。
fillRectメソッドは長方形の面を塗りつぶしてくれるもの。その引数には長方形の左上端と右下端の座標を指定する。canvasオブジェクトの座標系の原点となる0,0とcanvasオブジェクトの幅(width)と高さ(height)の座標を表すプロパティをここでは指定した。
方眼線を描く函数を定義
次に方眼線を描く函数を定義した。この函数が受け取る引数もHTMLのcanvas要素のid。
/* 方眼紙を描く函数 */
function drawGraphPaper(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// 垂直方向の方眼線を描く
for (let i = 0; i <= verticalGridNum; i++) {
const remainder = i % 5; // 剰余計算
if (remainder == 1) {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "silver"; // 方眼線の色
} else {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "lightgrey"; // 方眼線の色
}
ctx.moveTo(verticalLineSX, verticalLineSY); // 方眼線の始点
ctx.lineTo(verticalLineLX, verticalLineLY); // 方眼線の終点
ctx.stroke(); // 方眼線の描画
// 方眼線の幅だけ始点と終点を移動
verticalLineSX = verticalLineSX + gridSize;
verticalLineLX = verticalLineLX + gridSize;
}
// 水平方向の方眼線を描く
for (let i = 0; i <= horizontalGridNum; i++) {
const remainder = i % 2;
if (remainder == 1) {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "silver"; // 方眼線の色
} else {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "lightgrey"; // 方眼線の色
}
ctx.moveTo(horizontalLineSX, horizontalLineSY); // 方眼線の始点
ctx.lineTo(horizontalLineLX, horizontalLineLY); // 方眼線の終点
ctx.stroke(); // 描画
// 方眼線の幅だけ始点と終点を移動
horizontalLineSY = horizontalLineSY + gridSize;
horizontalLineLY = horizontalLineLY + gridSize;
}
// 垂直方向の方眼線の始点
verticalLineSX = leftMargin; // x座標
verticalLineSY = topMargin; // y座標
// 垂直方向の方眼線の終点
verticalLineLX = leftMargin; // x座標
verticalLineLY = topMargin +
(gridSize * horizontalGridNum); // y座標
// 水平方向の方眼線の始点
horizontalLineSX = leftMargin; // x座標
horizontalLineSY = topMargin; // y座標
// 水平方向の方眼線の終点
horizontalLineLX = leftMargin +
(gridSize * verticalGridNum); // x座標
horizontalLineLY = topMargin; // y座標
}
函数のブロック内の始めのコードはHTML5 Canvasの2次元用のAPIを利用するためのお決まりのステートメント2行。ちなみに1行で表す記法もある。
垂直方向と水平方向の方眼線を1本ずつ描くためにそれぞれにfor文を使った。カウンター用の変数iが0からverticalGridNum(垂直方向の方眼線の総数)またはhorizontalGridNum(水平方向の方眼線の総数)に達するまで同じ処理を繰り返すようにした。
方眼線の色を一定間隔で使い分けるためにfor文のブロック内に更にif文を設けて処理を分岐させた。分岐の条件は、垂直方向の方眼線を描くfor文では5で割った余りが1に等しいかそうでないか、水平方向の方眼線を描くfor文では2で割った余りが1に等しいかそうでないか。
beginPathメソッドはパスの初期化のためのもの。パスとは線や面をどこにどのように描くかを決める属性値の総称のようなもの。lineWidthプロパティは線の太さ。strokeStyleプロパティは線の色を決める。
moveToメソッドで線の始点となる座標を指定し、lineToメソッドで線の終点となる座標を指定し、strokeメソッドで線を描く。垂直方向の方眼線の始点と終点を決める各々の座標、水平方向の方眼線の始点と終点を決める各々の座標が入っている各変数を、これらのメソッドの引数として指定した。
このようにして方眼線を描くコードが完成。
最後に方眼線の座標を決める各変数の値を初期値に戻す必要があったので、for文のブロックから脱け出た後にそのためのコードが記述してある。
ここまでのコードを仮に実行した場合の結果は次のとおり。
方眼線が微妙に色分けされていることが分かるはず。
座標系の横軸と縦軸を描く函数を定義
次の函数はグラフの座標系の原点を通る横軸(x軸)と縦軸(y軸)を描くためのもの。この函数もその引数にHTMLのcanvas要素のidを受け取る。
/* 座標系の縦軸と横軸を描く函数 */
function drawAxis(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// パスの初期化と軸線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "black"; // 方眼線の色
// 横軸線と縦軸線のパス
ctx.moveTo(xAxisSX, xAxisSY); // 横軸開始位置
ctx.lineTo(xAxisLX, xAxisLY); // 横軸終了位置
ctx.moveTo(yAxisSX, yAxisSY); // 縦軸開始位置
ctx.lineTo(yAxisLX, yAxisLY); // 縦軸終了位置
ctx.stroke(); // 縦軸線と横軸線の描画
}
moveToメソッドとlineToメソッドの引数には、縦軸と横軸の各々の始点と終点を表す座標が定義されている定数を指定した。
ここまでのコードを仮に実行した場合の結果は次のとおり。
座標系の横軸と縦軸の目盛りを割り振る函数を定義
座標系の横軸と縦軸に方眼線と一致するように目盛りを割り振る函数が次のコード。この函数もHTMLのcanvas要素のidをその引数として受け取る。
/* 横軸と縦軸の目盛項目を書き込む函数 */
function drawScaleItems(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// 横軸線の項目(年)が入った配列データの作成
const scaleX = []; // 定数を配列として定義
let year = 1960; // 開始年を格納
const finalCount = 2020 - 1960; // 回数の計算
for (let i = 0; i <= finalCount; i++) {
scaleX[i] = year; // 1960から順に配列に格納
year = year + 1; // 変数yearに1を加える
}
// 横軸線の項目を描画する
ctx.font = "12px sans-serif"; // フォント
ctx.textBaseline = "middle"; // 文字の垂直位置
ctx.fillStyle = "black"; // 文字の色
ctx.direction = "ltr"; // 文字列の向き
ctx.textAlign = "center"; // 文字の水平位置
for (let i = 0; i < scaleX.length; i++) {
// 変数iに1を加えることでx軸を1升右へ移動
let x = leftMargin + (gridSize * (i + 1));
let y = verticalLineLY + 10;
ctx.fillText(scaleX[i], x, y); // 文字列の描画
i = i + 4; // 配列番号を4つずつ跳ばして描くため
}
// 座標系の原点から上方向への方眼線数
const positiveScaleCount = (originY - topMargin) / gridSize;
// 座標系の原点から下方向への方眼線数
const negativeScaleCount = horizontalGridNum - positiveScaleCount;
// 縦軸線の項目を描画する
ctx.textAlign = "right"; // 文字の水平位置
for (let i = 0; i < positiveScaleCount; i++) {
const x = originX - 5;
const y = originY - (gridSize * i);
ctx.fillText(i, x, y); // 正数の描画
i = i + 1; // 偶数だけ描画するため
}
for (let i = 2; i < negativeScaleCount; i++) {
const x = originX - 5;
const y = originY + (gridSize * i);
ctx.fillText(-1 * i, x, y); // 負数の描画
i = i + 1; // 偶数だけ描画するため
}
}
このコードで使った配列、定数、変数は次のとおり。
- scaleX[]
- 年を順番に保存しておく配列定数。
- year
- 年を保存しておく変数。初期値として1960を代入。
- finalCount
- 1960から2020までを配列に入れるために処理を何回繰り返す必要があるかを計算してその結果を入れる定数。
- positiveScaleCount
- 縦軸の正の目盛り(方眼紙の升目)がいくつあるかを計算して入れておく定数。
- negativeScaleCount
- 縦軸の負の目盛り(方眼紙の升目)がいくつあるかを計算して入れておく定数。
この折れ線グラフではその横軸の目盛りとして1960から2020までの年単位を時系列として取るので、この函数ブロック内の最初のfor文で1960から2020までの数を生成して配列定数として定義した。
そして文字に関する様々なプロパティに値を代入して文字の属性を指定しているのがその次のコード。
fontプロパティにはフォントの属性を指定する。12pxはフォントのサイズが12ピクセルということ。sans-serifはフォント・ファミリー名。
textBaselineプロパティは文字列の垂直方向の配置。ここでは中間(middle)とした。fillStyleプロパティは文字を塗りつぶす色。directionプロパティは文字列の向き。ここでは左から右へ書くことを表すltrと指定した。rtlと指定すれば右から左へ書くことを意味する。textAlignプロパティは水平方向の文字列の配置。ここでは中央(center)とした。
for文とfillTextメソッドを使って文字列を描画するコードがそれに続く。
for文の引数にあるscaleX.lengthはscaleXという配列の要素数を知ることができるプロパティ。ここでは配列の長さと同じ値になるまでfor文を繰り返すための条件式(比較演算)にこのプロパティを用いた。
positiveScaleCountとnegativeScaleCountという変数には正の目盛りの数と負の目盛りの数を計算して定義した。これらの定数を条件にして正方向(上方向)と負方向(下方向)の目盛りをfor文を用いてそれぞれ描くコードがこの函数の後半部分。
縦軸(y軸)の目盛りは、textAlignプロパティをrightに設定して文字列を右揃えにした。そして方眼紙の色分けに合わせて偶数だけを表示させるようにした。
ここまでのコードを仮に実行した場合の結果が次のとおり。
方眼線の色分けと目盛りの文字の位置が一致するように配置できているはず。
グラフの表題を書き込む函数を定義
次の函数はグラフの上に表題、グラフの下に参照元や説明のための文字列を書き込むもの。この函数もその引数にHTMLのcanvas要素のidを受け取る。
/* グラフに文字列を書き込む函数 */
function writeString(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// グラフの表題
ctx.font = "bold 15px sans-serif"; // フォント
ctx.textAlign = "start"; // 文字の水平位置
ctx.textBaseline = "middle"; // 文字の垂直位置
ctx.fillStyle = "black"; // 文字の色
ctx.direction = "ltr"; // 文字列の向き
ctx.fillText("図1 日本のインフレ率の時系列 1960-2020 (横軸 : 年, 縦軸 : %)",
leftMargin + 0, verticalLineSY - 15); // 文字列の描画
// グラフの参照元を記す文字列
ctx.font = "italic 12px sans-serif"; // フォント
ctx.fillText("参照元 : ",
leftMargin + 0, verticalLineLY + 40); // 文字列の描画
}
fontプロパティのboldは太字、italicはイタリック体。textAlignプロパティのstartは書き始めの位置へ文字列を寄せること。
fillTextメソッドの引数は左から、表示する文字列、x座標、y座標、文字列表示の最大許容幅。最後の引数は省略可能なのでこのコードでは省略してある。
ここまでのコードを仮に実行したとすると次のようになるはず。
データを座標系にプロットする函数を定義
次のコードはインフレ率の数値データを座標系にプロットする函数。この函数もHTMLのcanvas要素のidをその引数に取る。
/* データを座標系にプロットする函数 */
function plotData(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// インフレ率のデータを座標上にプロットする
for (let i = 0; i < dataSet.length; i++) {
// プロットする位置を示す座標を入れる変数
let plotX;
let plotY;
plotX = originX + (gridSize * (i + 1));
// Canvasの座標系はy軸が逆向きなので-1を掛けて正負を反転
plotY = originY + (-1 * gridSize * dataSet[i]);
// プロットしたデータの座標を配列定数に記録
plotXSet[i] = plotX;
plotYSet[i] = plotY;
// パスの設定と描画
ctx.beginPath(); // パスを初期化
ctx.arc(plotX, plotY, 1.5,
(Math.PI/180)*0, (Math.PI/180)*360); // 円のパス
ctx.closePath(); // パスを閉じる
ctx.fillStyle = "blue"; // 点の色
ctx.fill(); // 点を塗りつぶす
}
}
配列定数にしたインフレ率のデータを順番に取り出して座標系にプロットするためにfor文を用いた。
for文のブロック中では、x座標とy座標を計算して入れる変数plotXとplotYを設けた。y座標はHTML5 Canvasの座標系とこのグラフで用いる座標系では逆向きになるため、-1を掛けて符号を反転させることでその問題に対処した。
函数の外であらかじめ宣言しておいた配列定数であるplotXSetとplotYSetに、プロットした各点の座標を順番に入れて保存した。この配列定数は函数外で定義しておいたグローバル定数なので、折線グラフを描くために定義したこの次の函数で利用することができる。
beginPathメソッドでパスを初期化し、arcメソッドでしかるべき座標に点を描画する指示をしてfillメソッドで塗りつぶすようにした。
closePathメソッドはパスを閉じるためのもの。円弧を描くarcメソッドを用いて円を描くので円弧を閉じる必要があり、そのためにarcメソッドの後にclosePathメソッドを用いた。
インフレ率のデータをプロットした結果は次のようになるはず。
プロットした点を繋いで折線グラフを描く函数を定義
インフレ率のデータをプロットした点を線で結び折線グラフを作る函数のコードは次のとおり。この函数もHTMLのcanvas要素のidをその引数に取る。
/* 折線グラフを描く函数 */
function drawLineGraph(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// プロットした点を折線で結ぶ
for (let i = 0; i < plotXSet.length; i++) {
ctx.beginPath(); // パスを初期化
ctx.moveTo(plotXSet[i], plotYSet[i]); // 折線の始点
ctx.lineTo(plotXSet[i + 1], plotYSet[i + 1]); // 折線の終点
ctx.strokeStyle = "red"; // 折線の色
ctx.stroke(); // 折線の描画
}
}
プロットした各点の座標を保存しておいたplotXSetとplotYSetという配列を利用し、for文の中でmoveToメソッドとlineToメソッドとstrokeメソッドを使って各点を繋ぐと折れ線グラフが完成する。
for文では配列定数の要素数を示すlengthプロパティをその条件式の比較演算で用いた。
strokeStyleプロパティに赤色を指定することで折れ線の色を赤にした。
ここまでのコードを仮に実行した場合の結果は次のとおり。
函数を呼び出すステートメント
これまでは各々の函数の定義をしただけなのでそれを呼び出して実行するコードが必要。
// 各函数を呼び出す
let canvasID = "JapanInflationRateTimeSeries";
// 各函数を呼び出す
drawBackgroundOfCanvas(canvasID)
drawGraphPaper(canvasID);
drawAxis(canvasID);
drawScaleItems(canvasID);
writeString(canvasID);
plotData(canvasID);
drawLineGraph(canvasID);
函数を呼び出すステートメントを記述した順番に各函数は実行される。
スクリプト全体を匿名函数で袋とじ
これまでのコードを全てまとめると次のようになる。その際、このスクリプト全体を匿名函数によって袋とじにした。こうしておけばこのスクリプトの影響が外部に副作用として及ばないはず。
<script>
// このスクリプト全体を匿名函数で袋とじ
(function() {
// 方眼紙の属性定数
const topMargin = 35; // 上余白
const leftMargin = 30; // 左余白
const verticalGridNum = 65; // 方眼紙の垂直線数
const horizontalGridNum = 30; // 方眼紙の水平線数
const gridSize = 10; // 方眼紙の線間の幅
// 座標系の原点位置を決める定数
const originX = leftMargin + (gridSize * 0); // x座標
const originY = topMargin + (gridSize * 25); // y座標
// 横軸線の始点
const xAxisSX = originX;
const xAxisSY = topMargin;
// 横軸線の終点
const xAxisLX = originX;
const xAxisLY = topMargin + (gridSize * horizontalGridNum);
// 縦軸線の始点
const yAxisSX = leftMargin;
const yAxisSY = originY;
// 縦軸線の終点
const yAxisLX = leftMargin + (gridSize * verticalGridNum);
const yAxisLY = originY;
// インフレ率の数値データ配列定数
const dataSet = [3.57,5.36,6.83,6.7,3.8,6.65,5.04,3.98,5.33,
5.24,6.92,6.39,4.84,11.6,23.22,11.73,9.37,8.16,
4.2,3.7,7.77,4.91,2.74,1.89,2.26,2.03,0.59,0.12,
0.67,2.27,3.07,3.25,1.76,1.24,0.69,-0.12,0.13,
1.74,0.66,-0.34,-0.67,-0.74,-0.92,-0.25,0,-0.28,
0.24,0.06,1.38,-1.35,-0.71,-0.26,-0.05,0.34,2.76,
0.78,-0.11,0.46,0.97,0.47,-0.01];
// データをプロットした座標を保管する配列定数
const plotXSet = [];
const plotYSet = [];
// 垂直方向の方眼線の始点
let verticalLineSX = leftMargin; // x座標
let verticalLineSY = topMargin; // y座標
// 垂直方向の方眼線の終点
let verticalLineLX = leftMargin; // x座標
let verticalLineLY = topMargin +
(gridSize * horizontalGridNum); // y座標
// 水平方向の方眼線の始点
let horizontalLineSX = leftMargin; // x座標
let horizontalLineSY = topMargin; // y座標
// 水平方向の方眼線の終点
let horizontalLineLX = leftMargin +
(gridSize * verticalGridNum); // x座標
let horizontalLineLY = topMargin; // y座標
/* Canvasの背景色を描画する函数 */
function drawBackgroundOfCanvas(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// canvasの背景色設定
ctx.fillStyle = "white"; // 背景の色
ctx.fillRect(0, 0, canvas.width, canvas.height); // 長方形
}
/* 方眼紙を描く函数 */
function drawGraphPaper(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// 垂直方向の方眼線を描く
for (let i = 0; i <= verticalGridNum; i++) {
const remainder = i % 5; // 剰余計算
if (remainder == 1) {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "silver"; // 方眼線の色
} else {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "lightgrey"; // 方眼線の色
}
ctx.moveTo(verticalLineSX, verticalLineSY); // 方眼線の始点
ctx.lineTo(verticalLineLX, verticalLineLY); // 方眼線の終点
ctx.stroke(); // 方眼線の描画
// 方眼線の幅だけ始点と終点を移動
verticalLineSX = verticalLineSX + gridSize;
verticalLineLX = verticalLineLX + gridSize;
}
// 水平方向の方眼線を描く
for (let i = 0; i <= horizontalGridNum; i++) {
const remainder = i % 2;
if (remainder == 1) {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "silver"; // 方眼線の色
} else {
// 方眼線のパスの初期化と線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "lightgrey"; // 方眼線の色
}
ctx.moveTo(horizontalLineSX, horizontalLineSY); // 方眼線の始点
ctx.lineTo(horizontalLineLX, horizontalLineLY); // 方眼線の終点
ctx.stroke(); // 描画
// 方眼線の幅だけ始点と終点を移動
horizontalLineSY = horizontalLineSY + gridSize;
horizontalLineLY = horizontalLineLY + gridSize;
}
// 垂直方向の方眼線の始点
verticalLineSX = leftMargin; // x座標
verticalLineSY = topMargin; // y座標
// 垂直方向の方眼線の終点
verticalLineLX = leftMargin; // x座標
verticalLineLY = topMargin +
(gridSize * horizontalGridNum); // y座標
// 水平方向の方眼線の始点
horizontalLineSX = leftMargin; // x座標
horizontalLineSY = topMargin; // y座標
// 水平方向の方眼線の終点
horizontalLineLX = leftMargin +
(gridSize * verticalGridNum); // x座標
horizontalLineLY = topMargin; // y座標
}
/* 座標系の縦軸と横軸を描く函数 */
function drawAxis(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// パスの初期化と軸線の属性
ctx.beginPath(); // パスの初期化
ctx.lineWidth = 1; // 方眼線の太さ
ctx.strokeStyle = "black"; // 方眼線の色
// 横軸線と縦軸線のパス
ctx.moveTo(xAxisSX, xAxisSY); // 横軸開始位置
ctx.lineTo(xAxisLX, xAxisLY); // 横軸終了位置
ctx.moveTo(yAxisSX, yAxisSY); // 縦軸開始位置
ctx.lineTo(yAxisLX, yAxisLY); // 縦軸終了位置
ctx.stroke(); // 縦軸線と横軸線の描画
}
/* x軸とy軸の目盛項目を書き込む函数 */
function drawScaleItems(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// 横軸線の項目(年)が入った配列データの作成
const scaleX = []; // 定数を配列として定義
let year = 1960; // 開始年を格納
const finalCount = 2020 - 1960; // 回数の計算
for (let i = 0; i <= finalCount; i++) {
scaleX[i] = year; // 1960から順に配列に格納
year = year + 1; // 変数yearに1を加える
}
// 横軸線の項目を描画する
ctx.font = "12px sans-serif"; // フォント
ctx.textBaseline = "middle"; // 文字の垂直位置
ctx.fillStyle = "black"; // 文字の色
ctx.direction = "ltr"; // 文字列の向き
ctx.textAlign = "center"; // 文字の水平位置
for (let i = 0; i < scaleX.length; i++) {
// 変数iに1を加えることでx軸を1升右へ移動
let x = leftMargin + (gridSize * (i + 1));
let y = verticalLineLY + 10;
ctx.fillText(scaleX[i], x, y); // 文字列の描画
i = i + 4; // 配列番号を4つずつ跳ばして描くため
}
// 座標系の原点から上方向への方眼線数
const positiveScaleCount = (originY - topMargin) / gridSize;
// 座標系の原点から下方向への方眼線数
const negativeScaleCount = horizontalGridNum - positiveScaleCount;
// 縦軸線の項目を描画する
ctx.textAlign = "right"; // 文字の水平位置
for (let i = 0; i < positiveScaleCount; i++) {
const x = originX - 5;
const y = originY - (gridSize * i);
ctx.fillText(i, x, y); // 正数の描画
i = i + 1; // 偶数だけ描画するため
}
for (let i = 2; i < negativeScaleCount; i++) {
const x = originX - 5;
const y = originY + (gridSize * i);
ctx.fillText(-1 * i, x, y); // 負数の描画
i = i + 1; // 偶数だけ描画するため
}
}
/* グラフに文字列を書き込む函数 */
function writeString(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// グラフの表題
ctx.font = "bold 15px sans-serif"; // フォント
ctx.textAlign = "start"; // 文字の水平位置
ctx.textBaseline = "middle"; // 文字の垂直位置
ctx.fillStyle = "black"; // 文字の色
ctx.direction = "ltr"; // 文字列の向き
ctx.fillText("図1 日本のインフレ率の時系列 1960-2020 (横軸 : 年, 縦軸 : %)",
leftMargin + 0, verticalLineSY - 15); // 文字列の描画
// グラフの参照元を記す文字列
ctx.font = "italic 12px sans-serif"; // フォント
ctx.fillText("参照元:",
leftMargin + 0, verticalLineLY + 40); // 文字列の描画
}
/* データを座標系に描画する函数 */
function plotData(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// インフレ率のデータを座標上にプロットする
for (let i = 0; i < dataSet.length; i++) {
// プロットする位置を示す座標を入れる変数
let plotX = originX + (gridSize * (i + 1));
// Canvasの座標系はy軸が逆向きなので-1を掛けて正負を反転
let plotY = originY + (-1 * gridSize * dataSet[i]);
// プロットしたデータの座標を配列定数に記録
plotXSet[i] = plotX;
plotYSet[i] = plotY;
// パスの設定と描画
ctx.beginPath(); // パスを初期化
ctx.arc(plotX, plotY, 1.5,
(Math.PI/180)*0, (Math.PI/180)*360); // 円のパス
ctx.closePath(); // パスを閉じる
ctx.fillStyle = "blue"; // 点の色
ctx.fill(); // 点を塗りつぶす
}
}
/* 折線グラフを描く函数 */
function drawLineGraph(canvasID) {
const canvas = document.getElementById(canvasID);
const ctx = canvas.getContext("2d");
// プロットした点を折線で結ぶ
for (let i = 0; i < plotXSet.length; i++) {
ctx.beginPath(); // パスを初期化
ctx.moveTo(plotXSet[i], plotYSet[i]); // 折線の始点
ctx.lineTo(plotXSet[i + 1], plotYSet[i + 1]); // 折線の終点
ctx.strokeStyle = "red"; // 折線の色
ctx.stroke(); // 折線の描画
}
}
// 各函数を呼び出す
let canvasID = "JapanInflationRateTimeSeries";
drawBackgroundOfCanvas(canvasID)
drawGraphPaper(canvasID);
drawAxis(canvasID);
drawScaleItems(canvasID);
writeString(canvasID);
plotData(canvasID);
drawLineGraph(canvasID);
})(); // 袋とじ用の匿名函数の終わり
</script>
このスクリプトには異なるデータセットに対応できるような汎用性や拡張性がまだ考慮されていない。少しずつ改良して汎用性のあるものにしていきたい。例えばJSON形式のデータを受け取ってそれを折線グラフに表現するように最終的にはしたい。
データの視覚化を行うJavaScriptのライブラリはすでに数多くあるので、そちらを利用する方法についてはまたの機会に。
コメント
コメントを投稿