【PHP/JavaScript】svgで暗号通貨のチャートを出力する:複数の時間足に対応してみる

これまで PHP を用いて暗号通貨のローソク足チャートをブラウザ上に出力するコードを書いてきました。一見すると複雑そうですが,ローソク足のデータさえ入手できればグラフを描くこと自体はシンプルであり,プログラミングに興味を持ち始めたばかりの中高生でも十分に取り組めるテーマでしょう。

この連載は主にプログラミングに興味を持つ学生を対象としたものです。掲載しているコードは教育目的に限り著作権者の許諾なく自由に転載・配布できます。

JavsScript との組み合わせ

これまで使用する言語を PHP に限定していました。しかし,本来グラフの描画などは JavaScript を用いて行うべき作業です。以下で紹介するコードを動かしてみると分かりますが,こちらのほうが以前のコードよりもはるかに軽快に動作するはずです。

そもそも PHP はサーバーサイドで動く言語であるため,大部分が PHP で書かれたコードをサーバーに置いて公開すると,サーバーに大きな負担がかかることになります。それに対して JavaScript はクライエントサイドの言語であり,ウェブサイトを見ている側のコンピュータ上で動作します。つまり,サーバーに負荷がかかりません。

PHP と JavaScript は多くの場面で同じようなことができるのですが,理屈で言えば,PHP のコードはなるべく短い方が良いということになります。

さまざまな時間足のチャート

今回作成するチャートは上のようなものです。画面左上にドロップダウンメニューがあり,1分足から1日足までを選択できます。

時間の区切りを変えたときにどの時間足でもグラフが見やすいように微調整する作業が案外やっかいでした。この微調整はコードが長文になった主な原因です。面倒な作業がいろいろありますが,実際に製品として用いられているものに見た目だけでも近づけておこうと思います。

コードの説明

それでは実際のコードを見ていきます。以前の記事と併せて読むと,より理解が深まるでしょう。

<div>
    <select id='duration'>
        <option value='60'>1分</option>
        <option value='300'>5分</option>
        <option value='900'>15分</option>
        <option value='3600'>1時間</option>
        <option value='86400'>1日</option>
    </select>
    <span id="highlow"></span>
</div>

ドロップダウンメニューです。<option>で選択肢を用意します。あとでdurationというidをもちいて,選ばれた選択肢のvalueを取得します。たとえば,「1時間」を選択したら,得られるvalue3600です。この値をもとにして,1時間足のチャートを描画します。

また,<span id="highlow"></span>は,あとから始値や高値などの文字を表示します。

<div id='chart'></div>

グラフを出力するブロック要素です。あとからこの部分にSVGを用いてグラフを描画していきます。ここでは,あとからグラフを放り込むために空っぽの箱だけを用意していると考えれば良いでしょう。

データの取得

<?php
//curlでローソク足,為替を取得
//Bitcoinの1,5,15,1時間,1日足を取得する
$url = "https://api.cryptowat.ch/markets/binance/btcusdt/ohlc?periods=60,300,900,3600,86400";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); //URLを指定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //データを文字列で受け取る
$candlestick = curl_exec($ch);
curl_close($ch);
//為替を取得する
$url = "https://www.gaitameonline.com/rateaj/getrate";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
curl_close($ch);
$exchange = json_decode($res, true);
$usdjpy = $exchange["quotes"][20]["open"];
?>

PHP の部分です。curl を用いてローソク足と為替のデータを取得します。詳しくは以前の記事を参照して下さい。

periods=60,300,900,3600,86400と書くことで,1分足,5分足,15分足,1時間足,1日足のデータをまとめて取得できます(詳しくはhttps://docs.cryptowat.ch/rest-api/markets/ohlcを参照)。必要に応じて,もっと数を増やしてもよいでしょう。

$candlestick には1分足から1日足までのデータがまとめてJSON文字列として格納されます。また,$usdjpyは米ドル日本円の為替が格納されます。

今回のプログラムは,curlを用いてデータを取得する部分だけがどうしても JavaScript で書くことができない部分です。これ以外の処理は JavaScript を用います。

前処理と始値・高値などの出力

let display_chart = function() {
    let element = document.querySelector("#duration");
    const duration = element.value;
    localStorage.setItem('btc-chart', element.selectedIndex);
    const usdjpy = <?php echo $usdjpy; ?>; //米ドル円為替
    const candlestick = <?php echo $candlestick; ?>; //ローソク足
    const latest = candlestick["result"][duration].length - 1;
    let high = Math.floor(candlestick["result"][duration][latest][1] * usdjpy); //高値
    let low = Math.floor(candlestick["result"][duration][latest][2] * usdjpy); //安値
    let open = Math.floor(candlestick["result"][duration][latest][3] * usdjpy); //始値
    let close = Math.floor(candlestick["result"][duration][latest][4] * usdjpy); //終値
    const formatter = new Intl.NumberFormat('ja-JP');
    element = document.querySelector('#highlow');
    element.innerHTML = '始値' + formatter.format(open)
        + ' 高値' + formatter.format(high)
        + ' 安値' + formatter.format(low)
        + ' 終値' + formatter.format(close);

チャートの出力を関数 display_chart の中にまとめていきます。あとからこの関数を呼び出して,実際にグラフを画面に出力します。

let display_chart = function()function display_chart() と書くこともできます。

    let element = document.querySelector("#duration");
    const duration = element.value;

#duration は上で作ったドロップダウンメニューを指します。メニューで「1時間」を選択した場合,element.value = 3600 となるので,この値を duration に格納します。ローソク足のデータを格納した配列candlestickは,たとえばcandlestick["result"][60]に1分足のデータ,candlestick["result"][3600]に1時間足のデータが格納されています。したがって,candlestick["result"][duration]とすれば,選択した時間足のデータを取り出せます。

    localStorage.setItem('btc-chart', element.selectedIndex);

element.selectedIndex はドロップダウンメニューの上から何番目が選択されたかを表します。例えば,「1分」が選択された場合は 0,「1時間」が選択された場合は 3 が格納されています。

この値をlocalStorage.setItemでローカルストレージに保存します。ローカルストレージとはブラウザにデータを保存する仕組みです。実際に保存できるのはあくまで小さなデータに限定されますが,たとえば「1時間」を選択したあといったんブラウザを閉じ,再び同じページにアクセスすると,前回閉じたときの設定である1時間足のチャートを表示することができます。

細かい部分ですが,こうしておくことで普段1時間足でチャートを見ている人にとって,ブラウザを開くたびにドロップダウンメニューを操作する必要がなくなります。

PHP のデータをJavaScript で扱う

    const usdjpy = <?php echo $usdjpy; ?>; //米ドル円為替
    const candlestick = <?php echo $candlestick; ?>; //ローソク足

PHP で取得したデータを JavaScript に渡します。コード実行の順番から言えば,PHP のコードが実行されたあとに JavaScript が実行されます。

PHPがサーバサイドの言語であることを思い出してください。PHP はサーバーの側でいったん解釈されたあとクライエントに送られます。クライエントであるあなたのコンピュータは,それをもとにして今度は JavaScript のコードを実行します。

たとえば,<?php echo $usdjpy; ?>はサーバーに解釈されることによって,具体的には109.36のような数値となって,あなたのコンピュータに送り込まれます。したがって,クライエント側に届いたときには,const usdjpy = <?php echo $usdjpy; ?>;const usdjpy =109.36; という形に変換されています。

    const latest = candlestick["result"][duration].length - 1;

.length は配列の大きさを求めます。たとえば,duration が 60 のとき(これは 60 秒,つまり1分足のローソク足のデータです),配列は candlestick["result"][duration][0]candlestick["result"][duration][999] まで 1000 個存在します。従って,candlestick["result"][duration].length - 1 = 999 となり,これは配列の一番最後の要素(時間が最も新しい要素)を指します。

    let high = Math.floor(candlestick["result"][duration][latest][1] * usdjpy); //高値
    let low = Math.floor(candlestick["result"][duration][latest][2] * usdjpy); //安値
    let open = Math.floor(candlestick["result"][duration][latest][3] * usdjpy); //始値
    let close = Math.floor(candlestick["result"][duration][latest][4] * usdjpy); //終値

latest を用いて,ローソク足の最新の情報を取得します。

    const formatter = new Intl.NumberFormat('ja-JP');

highlowなどの値を日本円として扱うためのフォーマットを作ります。

element = document.querySelector('#highlow');
element.innerHTML = '始値' + formatter.format(open)
    + ' 高値' + formatter.format(high)
    + ' 安値' + formatter.format(low)
    + ' 終値' + formatter.format(close);

ブロック要素 highlow に始値,高値などの情報を出力します。formatter.format()は,指定した値を上で定義した日本円として出力します。これによって値が 3 桁ごとにコンマを入れた文字列として出力されます。

描画領域の設定

    const ct = {
        width: 800,     //描画領域の幅
        height: 400,    //描画領域の高さ
        margin: {       //上下左右のマージン
            top: 10,
            right: 100, 
            bottom: 50,
            left: 10
        },
        padding: 10     //パディング
    };

ここから,グラフの描画に移ります。まず,オブジェクト ct を作ります。この中には,SVG で描画する領域の幅や高さなどの情報をまとめて記述しておきます。あとで変更する可能性のある情報は,このようにオブジェクトとしてまとめておくと良いでしょう。

枠線の描画

    let chart =
        "<svg xmlns='http://www.w3.org/2000/svg' "
        + "viewBox='0 0 " + ct["width"] + " " + ct["height"]
        + "' width='" + ct["width"] + "' height='" + ct["height"] +"'>";
    //枠線の描画
    chart += "<rect x='" + ct["margin"]["left"] + "' y='" + ct["margin"]["top"]
        + "' width='"
        + (ct["width"] - ct["margin"]["left"] - ct["margin"]["right"])
        + "' height='"
        + (ct["height"] - ct["margin"]["top"] - ct["margin"]["bottom"])
        + "' stroke='#FFFFFF' stroke-width='1' fill='none'/>";

枠線を描画します。いったん文字列 chart にSVGのコードを格納し,あとで描画します。枠線は描画領域からマージンを差し引いて描画します。

垂直方向の範囲を求める

    let start = latest - 84; //配列の何番目から描画するか
    let prices = [];
    for (let i = 0; i < 85; i++) {
        for (let j = 0; j < 4; j++) {
            prices.push(candlestick['result'][duration][start + i][j + 1] * usdjpy);
        }
    }
    let max = Math.max.apply(null, prices);
    let min = Math.min.apply(null, prices);
    let scale = (ct["height"] - ct["margin"]["top"]
        - ct["margin"]["bottom"] - (ct["padding"] * 2))/(max - min);

curl を用いて得られるデータは 1000 個ほどありますが,画面上に出力するローソク足は 85 本です。そこで,画面に描画するローソク足のデータのスタート地点を決め,start に格納します。

次に,for 文で始値,高値,安値,終値の値を取得し,配列 prices に格納していきます。curl で取得した価格は米ドルなので,usdjpy をかけて日本円に変換します。

そして,Math.max.apply()Math.min.apply() を用いて最大値と最小値を取得します。これらの値は画面に出力するグラフの上端と下端として利用します。

最後に,価格をグラフ上の y 座標に変換するための縮尺を求め,scale に格納します。あとで価格に scale をかけることでグラフの y 座標を求めます。縮尺の計算方法は以前の記事を参照下さい。

直線を引く関数

    let drawLine = function(obj) {
        chart += "<line x1='" + obj["x1"] + "' y1='" + obj["y1"]
            + "' x2='" + obj["x2"] + "' y2='" + obj["y2"]
            + "' stroke='" + obj["stroke"]
            + "' stroke-width='" + obj["strokeWidth"] + "'/>"
    }

ここから時間や価格を示す罫線やローソク足など,直線を引く作業が多く出てきます。これらの作業を関数 drawLine で一括して行うことにします。

オブジェクト obj に直線の座標や線の色,太さのデータを与え drawLine に渡すと,それらをもとにSVG のコードを出力します。

時間線の描画

チャートは一定の時間ごとに基準となる垂直方向の罫線が描画されています。1分足と1日足では直線を引く基準が異なり,これらをうまく調整しながら,線を引いていく必要があります。

    const duration_unit = {
        60: 600,
        300: 1800,
        900: 7200,
        3600: 21600,
    };

1分~1時間足における罫線を引く単位を duration_unit に定めます。例えば,1分足は 60 というキーで指定されていて,1分足のときは 600 秒,つまり 10 分ごとに罫線を描画します。この辺りは,実際の画面を見ながら適切な値を決めると良いでしょう。また,1日足の場合はルールがかなり異なるので別に設定します。

    let obj = { //直線の座標,線の色,線の太さ
        x1: 0,
        y1: ct["margin"]["top"],
        x2: 0,
        y2: ct["height"] - ct["margin"]["bottom"],
        stroke: "#808080",
        strokeWidth: 1
    }

直線の設定をオブジェクトに格納します。座標(x1, y1)から(x2, y2) に向かって直線を引きます。y1 は枠線の上端,y2 は枠線の下端を指定します。stroke は線の色(ここではグレー),strokeWidthは線の太さです。x1x2 の値はあとから決定します。

    for (let i = 0; i < 85; i++) {
        const x = i * 8 + ct["margin"]["left"] + ct["padding"];
        obj["x1"] = x;
        obj["x2"] = x;

for 文で i の値を変化させながら繰り返し処理します。ローソク足は 85 本あるので 85 回繰り返し,必要な部分に直線を描きます。

ここではローソク足の x 座標を求め,上で作った obj x1x2 に格納します。垂直方向の直線を引くので,x 座標はどちらも同じです。

        const unixtime = candlestick['result'][duration][start + i][0];
        const date = new Date(unixtime * 1000);

データからUNIX時間を取り出し,日付オブジェクトdateを生成します。JavaScriptの場合は,このようにいったん日付オブジェクトというものを作り,そこから日付や時間,分,秒といった情報を取り出します。

        if (duration == 86400) {
            if (date.getDate() == 1 || date.getDate() == 8
                || date.getDate() == 15 || date.getDate() == 22) {
                drawLine(obj);
                chart += "<text x='" + ( x - 10 )
                    + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                    + "' font-size='10' fill='#FFFFFF'>"
                    + ( date.getMonth() + 1 ) + "/"
                    + date.getDate()
                    + "</text>";
            }
            if (date.getDate() == 29) {
                drawLine(obj);
            }

1日足の場合です。このときは,1,8,15,22日のところに直線と日付の文字を入れ,29日のところは直線のみを引きます。このあたりも実際の画面を見ながら設定を決めていくとよいでしょう。

date.getDate()は上で作った日付オブジェクトdateから日付を取り出します。同様に,date.getMonth()は月を取り出しますが,1月を0として返すため +1 します。

SVGで文字を出力する<text></text>については以前の記事を参照して下さい。

関数drawLine(obj)で直線を引きます。上で作ったobjに座標などの必要な情報を格納しdrawLineに送り込みます。

        } else {
            //それ以外
            const time = date.getHours() * 3600 + date.getMinutes() * 60;
            if (time % duration_unit[duration] == 0) {
                drawLine(obj);
                if (date.getHours() == 0 && date.getMinutes() == 0) {
                    chart += "<text x='" + ( x - 14 )
                        + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                        + "' font-size='10' fill='#FFFFFF'>"
                        + ( date.getMonth() + 1 ) + "/"
                        + date.getDate()
                        + "</text>";
                } else {
                    chart += "<text x='" + ( x - 14 )
                        + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                        + "' font-size='10' fill='#FFFFFF'>"
                        + date.getHours() + ":"
                        + String(date.getMinutes()).padStart(2,'0')
                        + "</text>";
                }
            }
        }

1分~1時間足の場合は直線を引くルールが異なります。いったん時間と分を秒になおします。たとえば,1時1分なら3660としてtimeに格納します。

1分足であれば,duration_unit[duration]=600(600秒=10分)なので,timeを600で割った余りが0になったときに直線を引くようにすれば10分ごとに直線が入るようになります。

直線の下の文字は,0時0分のときだけ,9/16のような日付を入れ,それ以外では1:10のような時刻を入れます。

.padStart(2,'0')はゼロパディングといって,たとえば1:0を1:00とするように,分のところを強制的に2桁にする命令です。

価格線の描画

価格を表す水平方向の直線を引きます。これも表示される価格帯によって何円単位で直線を引くかを調整する必要があります。

let unit = Math.floor((max - min) / 15);
    switch (true) {
        case (unit < 1000):
            unit = 2500;
            break;
        case (unit >= 1000 && unit < 2500 ):
            unit = 2500;
            break;
        case (unit >= 2500 && unit < 5000 ):
            unit = 5000;
            break;
        case (unit >= 5000 && unit < 10000 ):
            unit = 10000;
            break;
        case (unit >= 10000 && unit < 25000 ):
            unit = 25000;
            break;
        case (unit >= 25000 && unit < 50000 ):
            unit = 50000;
            break;
        case (unit >= 50000 && unit < 100000 ):
            unit = 100000;
            break;
        case (unit >= 100000 && unit < 250000 ):
            unit = 250000;
            break;
        case (unit >= 250000):
            unit = 500000;
            break;
    }

たとえば,価格の上端が400万円で下端が350万円なら,max - min は50万円です。これがグラフ上の範囲を表しています。今回は,価格線をおよそ15本入れようとしているので,15で割り,unitに格納します。この値はキリの悪いものなので,たとえば,unit=1397であればunit=2500として2500円ごとに直線を引くようにします。

    let price = min - (min % unit) + unit;

unitをもとに最初に罫線を引く価格を求めます。

    obj = { //直線の座標,線の色,線の太さ
        x1: ct["margin"]["left"],
        y1: 0,
        x2: ct["width"] - ct["margin"]["right"],
        y2: 0,
        stroke: "#808080",
        strokeWidth: 1
    }

再びobjに水平方向の直線を引くための座標などの情報を格納します。y1y2はあとで定めます。

    while (price < max) {
        const y = (max - price) * scale + ct["margin"]["top"] + ct["padding"];
        obj["y1"] = y;
        obj["y2"] = y;
        drawLine(obj);
        chart += "<text x='" + ( ct["width"] - ct["margin"]["right"] + 4 )
            + "' y='" + ( y + 4 )
            + "' font-size='10' fill='#FFFFFF'>"
            + formatter.format( price ) + "</text>";
        price += unit;
    }

価格線の処理については以前の記事を参照して下さい。下から上に向かって価格を増やしながら,罫線を引いていきます。

ローソク足を描画する

    let color;
    for(let i = 0; i < 85; i++) {
        high = Math.floor(candlestick["result"][duration][start + i][2] * usdjpy); //高値
        low = Math.floor(candlestick["result"][duration][start + i][3] * usdjpy); //安値
        open = Math.floor(candlestick["result"][duration][start + i][1] * usdjpy); //始値
        close = Math.floor(candlestick["result"][duration][start + i][4] * usdjpy); //終値
        if (open <= close) {
            color = "#FFCC00";
        } else {
            color = "#FF6600";
        }
        const x = i * 8 + ct["margin"]["left"] + ct["padding"];   //x座標
        obj = { //直線の座標,線の色,線の太さ
            x1: x,
            y1: (max - high) * scale + ct["margin"]["top"] + ct["padding"],
            x2: x,
            y2: (max - low) * scale + ct["margin"]["top"] + ct["padding"],
            stroke: color,
            strokeWidth: 1
        }
        drawLine(obj);
        obj["y1"] = (max - open) * scale + ct["margin"]["top"] + ct["padding"]; //始値のy座標
        obj["y2"] = (max - close) * scale + ct["margin"]["top"] + ct["padding"]; //終値のy座標
        obj["strokeWidth"] = 5;
        drawLine(obj);
    }

ローソク足の直線を引きます。これについても以前の記事を参照下さい。始値と終値を比べて,上昇と下降によって色分けして描画します。

    chart += "</svg>";
    element = document.querySelector("#chart");
    element.innerHTML = chart;

最後にsvgタグを閉じて,生成した文字列をブロック要素chartに送り込みます。この命令によって実際に画面上にグラフを出力します。

これで,グラフを描画する関数display_chartは終わりです。

イベントリスナーの設置

window.addEventListener('DOMContentLoaded', display_chart);
element = document.querySelector("#duration");
element.addEventListener('change', display_chart);

最後に,チャートを表示するためのイベントリスナーを設置しておきます。

DOMContentLoadedは画面が読み込まれたときに実行されるイベントリスナーです。また,ドロップダウンメニューにもイベントリスナーを設置し,変更されたときに関数display_chartを実行するようにします。

まとめ

今までPHPで記述していた処理の大部分をJavaScriptで書き直しました。また,ドロップダウンメニューの選択によって動的にグラフを描画できるようになりました。一方で時間や価格の罫線を引くコードがかなり煩雑なものになっているため,もう少し工夫が必要でしょう。

最後にコード全体を示します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<style type="text/css">body{color:#fff;background-color:#000;}
    line{shape-rendering:crispEdges;}</style>
</head>
<body>
<p>BTC/JPY-Binance</p>
<div>
    <select id='duration'>
        <option value='60'>1分</option>
        <option value='300'>5分</option>
        <option value='900'>15分</option>
        <option value='3600'>1時間</option>
        <option value='86400'>1日</option>
    </select>
    <span id="highlow"></span>
</div>
<div id='chart'></div>
<?php
//curlでローソク足,為替を取得
//Bitcoinの1,5,15,1時間,1日足を取得する
$url = "https://api.cryptowat.ch/markets/binance/btcusdt/ohlc?periods=60,300,900,3600,86400";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); //URLを指定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //データを文字列で受け取る
$candlestick = curl_exec($ch);
curl_close($ch);
//為替を取得する
$url = "https://www.gaitameonline.com/rateaj/getrate";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
curl_close($ch);
$exchange = json_decode($res, true);
$usdjpy = $exchange["quotes"][20]["open"];
?>
<script>
//チャートの描画
let display_chart = function() {
    let element = document.querySelector("#duration");
    const duration = element.value;
    localStorage.setItem('btc-chart', element.selectedIndex);
    const usdjpy = <?php echo $usdjpy; ?>; //米ドル円為替
    const candlestick = <?php echo $candlestick; ?>; //ローソク足
    const latest = candlestick["result"][duration].length - 1;
    let high = Math.floor(candlestick["result"][duration][latest][1] * usdjpy); //高値
    let low = Math.floor(candlestick["result"][duration][latest][2] * usdjpy); //安値
    let open = Math.floor(candlestick["result"][duration][latest][3] * usdjpy); //始値
    let close = Math.floor(candlestick["result"][duration][latest][4] * usdjpy); //終値
    const formatter = new Intl.NumberFormat('ja-JP');
    element = document.querySelector('#highlow');
    element.innerHTML = '始値' + formatter.format(open)
        + ' 高値' + formatter.format(high)
        + ' 安値' + formatter.format(low)
        + ' 終値' + formatter.format(close);
    //描画領域の設定
    const ct = {
        width: 800,     //描画領域の幅
        height: 400,    //描画領域の高さ
        margin: {       //上下左右のマージン
            top: 10,
            right: 100, 
            bottom: 50,
            left: 10
        },
        padding: 10     //パディング
    };
    //チャートの描画
    let chart =
        "<svg xmlns='http://www.w3.org/2000/svg' "
        + "viewBox='0 0 " + ct["width"] + " " + ct["height"]
        + "' width='" + ct["width"] + "' height='" + ct["height"] +"'>";
    //枠線の描画
    chart += "<rect x='" + ct["margin"]["left"] + "' y='" + ct["margin"]["top"]
        + "' width='"
        + (ct["width"] - ct["margin"]["left"] - ct["margin"]["right"])
        + "' height='"
        + (ct["height"] - ct["margin"]["top"] - ct["margin"]["bottom"])
        + "' stroke='#FFFFFF' stroke-width='1' fill='none'/>";
    //y軸方向の範囲を求める
    let start = latest - 84; //配列の何番目から描画するか
    let prices = [];
    for (let i = 0; i < 85; i++) {
        for (let j = 0; j < 4; j++) {
            prices.push(candlestick['result'][duration][start + i][j + 1] * usdjpy);
        }
    }
    let max = Math.max.apply(null, prices);
    let min = Math.min.apply(null, prices);
    let scale = (ct["height"] - ct["margin"]["top"]
        - ct["margin"]["bottom"] - (ct["padding"] * 2))/(max - min);
    //直線を引く関数
    let drawLine = function(obj) {
        chart += "<line x1='" + obj["x1"] + "' y1='" + obj["y1"]
            + "' x2='" + obj["x2"] + "' y2='" + obj["y2"]
            + "' stroke='" + obj["stroke"]
            + "' stroke-width='" + obj["strokeWidth"] + "'/>"
    }
    //罫線の描画
    //時間線
    const duration_unit = {
        60: 600,
        300: 1800,
        900: 7200,
        3600: 21600
    };
    let obj = { //直線の座標,線の色,線の太さ
        x1: 0,
        y1: ct["margin"]["top"],
        x2: 0,
        y2: ct["height"] - ct["margin"]["bottom"],
        stroke: "#808080",
        strokeWidth: 1
    }
    for (let i = 0; i < 85; i++) {
        const x = i * 8 + ct["margin"]["left"] + ct["padding"];
        obj["x1"] = x;
        obj["x2"] = x;
        const unixtime = candlestick['result'][duration][start + i][0];
        const date = new Date(unixtime * 1000);
        //1日足
        if (duration == 86400) {
            if (date.getDate() == 1 || date.getDate() == 8
                || date.getDate() == 15 || date.getDate() == 22) {
                drawLine(obj);
                chart += "<text x='" + ( x - 10 )
                    + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                    + "' font-size='10' fill='#FFFFFF'>"
                    + ( date.getMonth() + 1 ) + "/"
                    + date.getDate()
                    + "</text>";
            }
            if (date.getDate() == 29) {
                drawLine(obj);
            }
        } else {
            //それ以外
            const time = date.getHours() * 3600 + date.getMinutes() * 60;
            if (time % duration_unit[duration] == 0) {
                drawLine(obj);
                if (date.getHours() == 0 && date.getMinutes() == 0) {
                    chart += "<text x='" + ( x - 14 )
                        + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                        + "' font-size='10' fill='#FFFFFF'>"
                        + ( date.getMonth() + 1 ) + "/"
                        + date.getDate()
                        + "</text>";
                } else {
                    chart += "<text x='" + ( x - 14 )
                        + "' y='" + ( ct["height"] - ct["margin"]["bottom"] + 12 )
                        + "' font-size='10' fill='#FFFFFF'>"
                        + date.getHours() + ":"
                        + String(date.getMinutes()).padStart(2,'0')
                        + "</text>";
                }
            }
        }
    }
    //価格線
    let unit = Math.floor((max - min) / 15);
    switch (true) {
        case (unit < 1000):
            unit = 2500;
            break;
        case (unit >= 1000 && unit < 2500 ):
            unit = 2500;
            break;
        case (unit >= 2500 && unit < 5000 ):
            unit = 5000;
            break;
        case (unit >= 5000 && unit < 10000 ):
            unit = 10000;
            break;
        case (unit >= 10000 && unit < 25000 ):
            unit = 25000;
            break;
        case (unit >= 25000 && unit < 50000 ):
            unit = 50000;
            break;
        case (unit >= 50000 && unit < 100000 ):
            unit = 100000;
            break;
        case (unit >= 100000 && unit < 250000 ):
            unit = 250000;
            break;
        case (unit >= 250000):
            unit = 500000;
            break;
    }
    let price = min - (min % unit) + unit;
    obj = { //直線の座標,線の色,線の太さ
        x1: ct["margin"]["left"],
        y1: 0,
        x2: ct["width"] - ct["margin"]["right"],
        y2: 0,
        stroke: "#808080",
        strokeWidth: 1
    }
    while (price < max) {
        const y = (max - price) * scale + ct["margin"]["top"] + ct["padding"];
        obj["y1"] = y;
        obj["y2"] = y;
        drawLine(obj);
        chart += "<text x='" + ( ct["width"] - ct["margin"]["right"] + 4 )
            + "' y='" + ( y + 4 )
            + "' font-size='10' fill='#FFFFFF'>"
            + formatter.format( price ) + "</text>";
        price += unit;
    }
    //ローソク足
    let color;
    for(let i = 0; i < 85; i++) {
        high = Math.floor(candlestick["result"][duration][start + i][2] * usdjpy); //高値
        low = Math.floor(candlestick["result"][duration][start + i][3] * usdjpy); //安値
        open = Math.floor(candlestick["result"][duration][start + i][1] * usdjpy); //始値
        close = Math.floor(candlestick["result"][duration][start + i][4] * usdjpy); //終値
        if (open <= close) {
            color = "#FFCC00";
        } else {
            color = "#FF6600";
        }
        const x = i * 8 + ct["margin"]["left"] + ct["padding"];   //x座標
        obj = { //直線の座標,線の色,線の太さ
            x1: x,
            y1: (max - high) * scale + ct["margin"]["top"] + ct["padding"],
            x2: x,
            y2: (max - low) * scale + ct["margin"]["top"] + ct["padding"],
            stroke: color,
            strokeWidth: 1
        }
        drawLine(obj);
        obj["y1"] = (max - open) * scale + ct["margin"]["top"] + ct["padding"]; //始値のy座標
        obj["y2"] = (max - close) * scale + ct["margin"]["top"] + ct["padding"]; //終値のy座標
        obj["strokeWidth"] = 5;
        drawLine(obj);
    }
    chart += "</svg>";
    element = document.querySelector("#chart");
    element.innerHTML = chart;
}
element = document.querySelector("#duration");
let select = localStorage.getItem('btc-chart');
if (select) {
    element.options[select].selected = true;
}
window.addEventListener('DOMContentLoaded', display_chart);
element = document.querySelector("#duration");
element.addEventListener('change', display_chart);
</script>
</body>
</html>