株価チャート chartjs-chart-financial を試行錯誤で使ってみた　その１

日頃、株式監視銘柄のリストをデータベース化した Filemaker で、WebViewer からヤフーファイナンス等の Web ページを表示閲覧すると同時にページソースをキャッシュ保存してオフラインでも再確認できるように利用しています。

Filemaker の WebViewer は表示コンテンツを Filemaker から JavaScript を介してリンク等をクリックしてSeleniumの様にブラウジングを自動化したり、Filemaker から WebViewer にデータを渡して JavaScript で高速に計算して結果を Filemaker に再び戻すということも出来て重宝しています。
しかし、チャート画像などはダウンロードしないように利用しているため、チャートを確認したくなってしまうことが度々あります。
そこで、オフラインでも株価チャートを表示できないかとググってみたところ、JavaScript でグラフを表示する[Chart.js](https://github.com/chartjs)の公式プラグインに[chartjs-chart-financial](https://github.com/chartjs/chartjs-chart-financial)が公表されているのを見つけました。

しかし、Chart.js 自体を知りませんでしたし、日本語で書かれている説明ページも見つけることができず、JavaScript もほとんどといっていいほど使ったことがなかったためローソク足チャートやオシレータ等のインジケータを表示するのに非常に苦労しましたが、Chart.jsの公式ドキュメントを見ながら試行錯誤を繰り返したことで、そこそこ利用できることがわかりました。

忘備録を兼ねて、自分の利用方法を公開しようと思います。

![chart1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/510c2d91-a3fe-a95b-8a83-e2e321de9c4f.png)


## ライブラリについて

chartjs-chart-financial公式の[index.html](https://github.com/chartjs/chartjs-chart-financial/blob/master/docs/index.html)によると、利用するライブラリファイルは以下の４つのようです。

- luxon.js
- chart.js
- chartjs-adapter-luxon
- chartjs-chart-financial.js

```
<script src="https://cdn.jsdelivr.net/npm/luxon@1.26.0"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.0.1/dist/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.0.0"></script>
<script src="./chartjs-chart-financial.js" type="text/javascript"></script>
```

### ライブラリをローカルにダウンロードする場合
オンラインで充分高速に実行できますが、自分はオフラインでも使えるようにライブラリをダウンロードしています。
Filemakerで利用する場合には、ライブラリソースを追加したhtmlヘッダの計算フィールドをhtmlソースに随時追加してWebViewerで表示させています。

#### 1. Chart.js

[Chart.js 公式サイト](https://www.chartjs.org/)から「GetStarted」を表示して左のリストから「Installation」をクリックして表示すると、[CDNJS](https://cdnjs.com/)でファイルがダウンロードできるようです。
2024 年 2 月時点で、最新は 4.4.1 ですが 3.8 以降ではローソク足の色がおかしくなったり、まれにエラーが出ることもあったので 3.7.1 以前の利用が安全かもしれません。

> https://github.com/chartjs/chartjs-chart-financial/issues/150

4.4.1 をインターネットに接続しないローカル環境で利用する場合には、「chart.js」では、内部でほかのファイルを呼び込むようなのでうまく動きません。「chart.umd.min.js」か「chart.umd.js」をダウンロードすればいいと思います。

3.7.1 の場合は「chart.mini.js」でよさそうです。

#### 2. chartjs-adapter-luxon

Chart.js と同じく、CDNJS で検索するとダウンロードページが見つかります。
2024 年 2 月時点で、最新は 1.3.1 のようですが、このバージョンについては chartjs-chart-financial で動かないようです。

Version 1.0.0 をダウンロードします。

#### 3. luxon.js

JavaScript で日付を扱うライブラリのようです。
CDNJS の[luxon ページ](https://cdnjs.com/libraries/luxon)でダウンロードできます。

2024 年 2 月時点で、最新は 3.4.4 でこれはそのまま利用できます。

#### 4. chartjs-chart-financial.js

[公式ページ](https://github.com/chartjs/chartjs-chart-financial/tree/master)
の「docs」フォルダ内に「chartjs-chart-financial.js」ファイルがあります。

## とりあえずオンラインで表示してみる

```HTML:code1
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title></title>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.0.0"></script>
<script src="https://www.chartjs.org/chartjs-chart-financial/chartjs-chart-financial.js"></script>
</head>
<body>
    <!-- bodyに必要なエレメントブロック -->
    <div> <canvas id="chart"></canvas> </div>

    <script>
        // 株価データ  x:横軸 タイムスタンプ
        // luxon.DateTime.fromISO('2024-01-30').valueOf()
        // で取得できる
        // o:始値 h:高値 l:安値 c:終値
        // これ以外のフィールドはあっても無視されるようだ
        const barData =
        [
            {x: 1706540400000, o: 2950, h: 2983.5, l: 2931, c: 2959.5},
            {x: 1706626800000, o: 2940, h: 3000, l: 2939, c: 3000},
            {x: 1706713200000, o: 2940.5, h: 2960, l: 2931, c: 2945},
            {x: 1706799600000, o: 2963.5, h: 2966, l: 2938, c: 2951.5},
            {x: 1707058800000, o: 2995.5, h: 3007, l: 2959, c: 2992},
            {x: 1707145200000, o: 2999.5, h: 3148, l: 2951.5, c: 3135},
            {x: 1707231600000, o: 3195, h: 3364, l: 3190, c: 3260},
            {x: 1707318000000, o: 3330, h: 3409, l: 3283, c: 3350},
            {x: 1707404400000, o: 3360, h: 3370, l: 3296, c: 3323},
            {x: 1707750000000, o: 3366, h: 3459, l: 3350, c: 3456},
        ]

        // ローソク足の表示
        const ctx = document.getElementById('chart').getContext('2d');
        const chart = new Chart(ctx, {
            type: 'candlestick',
            data: {
                datasets: [{
                    label: 'テスト',
                    data: barData
                }]
            }
        });
    </script>

</body></html>
```
![result of code1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/314e8379-7ad5-12fb-487c-bd6c92031c18.png)


公式のサンプルコードはランダムデータを取得しており、JavaScript の構文の知識も無いためデータ構造を理解するのに時間がかかってしまいました。

横軸の時系列データはタイムスタンプで、luxon.DateTime.fromISO または luxon.DateTime.fromSQL で取得した luxon データから valueOf()で取得できるようです。

これさえ理解できれば、javascript でデータを変換して渡すことでチャートが表示できます。

たとえば Yahoo ファイナンスの個別銘柄の基本情報ページのhtmlソースには「window.PRELOADED_STATE = 」に続いて json データが埋め込まれており、例えば「mainStocksPriceBoard.priceBoard.code」で銘柄コード、「mainStocksPriceBoard.priceBoard.price」で現在値、「mainStocksDetail.referenceIndex.per」で PER など、ページ内に表示されているデータ等が確認できます。
これらに加えて「mainItemDetailChartSetting.timeSeriesData.histories」に過去 6 か月程度の時系列データも存在します。

これを利用してチャートを作成してみます。

```HTML:code2
<!-- bodyに必要なエレメントブロック -->
<div> <canvas id="chart"></canvas> </div>

<script>
    json_yahoo = ...  // ここにyahoo financeのjsonデータ（window.__PRELOADED_STATE__以下）をコピペ
</script>

<script>

    const barData = getDailyData(json_yahoo);
    const code = json_yahoo.mainStocksPriceBoard.priceBoard.code;
    const brand = json_yahoo.mainStocksPriceBoard.priceBoard.name;

    // データ変換
    function getDailyData(json) {
            const series = [];
            const n = json.mainItemDetailChartSetting.timeSeriesData.histories.length;
            for (let i = 0; i < n; i++) {
                const dt = luxon.DateTime.fromISO(json.mainItemDetailChartSetting.timeSeriesData.histories[i].baseDatetime);
                const data = {
                    x: dt.valueOf(),
                    o: json.mainItemDetailChartSetting.timeSeriesData.histories[i].openPrice,
                    h: json.mainItemDetailChartSetting.timeSeriesData.histories[i].highPrice,
                    l: json.mainItemDetailChartSetting.timeSeriesData.histories[i].lowPrice,
                    c: json.mainItemDetailChartSetting.timeSeriesData.histories[i].closePrice,
                    v: json.mainItemDetailChartSetting.timeSeriesData.histories[i].volume,
                };
                series.push(data);
            }
            return series;
        }


    // ローソク足の表示
    const ctx = document.getElementById('chart').getContext('2d');
    const chart = new Chart(ctx, {
        type: 'candlestick',
        data: {
            datasets: [{
                label: `${code} ${brand}`,
                data: barData
            }]
        }
    });
</script>

```
データセットに出来高も追加しましたが、無視されています。
![result of code2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/ab0523c7-c717-3322-4781-5d75b9dc9651.png)

## 出来高の追加表示

ラベル「ローソク足の表示」の続きから

```JavaScript:code3
// ローソク足の表示
const ctx = document.getElementById('chart').getContext('2d');
// 空のデータセットで初期化
const chart = new Chart(ctx, {
    type: 'candlestick',
    data: { datasets: [] }
});

const data_candles = {
    label: `${code} ${brand}`,
    data: barData,
    // ローソク足の色も変更
    color: {
        up          : 'red',
        down        : 'green',
        unchanged   : 'dimgray',
    },
}
// chart.config.data.datasets = []; // すでにデータセットに何らかのデータが格納されている中でリセットする場合
chart.config.data.datasets.push(data_candles);
const data_volumes = barData.map(d => {return {x:d.x, y:d.v}});
const ds_volume = {
    type: 'bar',
    label: 'volume',
    data: data_volumes,
    borderColor: 'indigo',
    backgroundColor: 'lavender',
    borderWidth: 1,
    pointRadius: 0,
}
// 出来高データを追加
chart.config.data.datasets.push(ds_volume);

chart.update();
```

`chart.config.data.datasets`の配列にデータを追加して、`chart.update()`で追加データが表示されます。
注意点としては、candlestckタイプのチャートにバーチャートやラインチャートを追加する場合のデータ構造は横軸がタイムスタンプ「x」、縦軸が「y」の連想配列であることです。
![出来高バー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/bb1b065b-dbbb-1498-7e64-5d3c4b0bd5bf.png)

出来高は表示できたものの、出来高と価格のスケールが全く異なるため、ローソク足が見えなくなってしまいました。
```JavaScript:code4
const ds_volume = {
    type: 'bar',
    label: 'volume',
    data: data_volumes,
    borderWidth: 1,
    pointRadius: 0,  
    yAxisID: "yAxisSub",
}
```
ds_volumeに軸ラベルidを追加して２軸のMixedChartにしてやるとローソク足と出来高が同時に表示されます。
ここでは出来高が目立たないように色をデフォルトに戻しています。

公式ドキュメントによると、データセットは先に追加したほうが最後に表示される仕様のようです。
![ローソク足と出来高](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/87a43468-5334-2c41-4fc3-f6b73e27f494.png)


最終的にChart.js公式のドキュメントを見ながら、オプションをいろいろと変更してみると、こうなりました。
```JavaScript:code5
const chart = new Chart(ctx, {
    type: 'candlestick',
    data: { datasets: [] }
});

const data_candles = {
    label: `${code} ${brand}`,
    data: barData,
    // ローソク足の色も変更
    color: {
        up          : 'red',
        down        : 'green',
        unchanged   : 'dimgray',
    },
    yAxisID: "yAxisMain",
}
chart.config.data.datasets.push(data_candles);
const data_volumes = barData.map(d => {return {x:d.x, y:d.v}});
const ds_volume = {
    type: 'bar',
    label: 'volume',
    data: data_volumes,
    borderColor: 'lavender',
    backgroundColor: 'aliceblue',
    borderWidth: 1,
    pointRadius: 0,  
    yAxisID: "yAxisSub",
}
// 出来高データを追加
chart.config.data.datasets.push(ds_volume);

const volume_max = data_volumes.map(d => {return d.y}).reduce(function(a, b){return Math.max(a, b)});
// ローソク足凡例の削除と出来高軸スケール等の変更
chart.config.options = {
    plugins: { 
        title: {
            text: `${code} ${brand}`,
            display: true,
        },
        legend: { 
            position: 'right',
            align: 'center',
            labels: {
                filter:function(item){
                    //ローソク足の凡例のみ非表示
                    if(item.datasetIndex === 0)
                        return false;
                    else
                        return true;
                }, 
                boxWidth: 10,
                boxHeight: 2,
        }}},
    scales: {
        'yAxisSub':  {
            suggestedMax: volume_max * 5,
            position: 'right',
            ticks: {
                callback: function(value, index){
                    if(value > volume_max || value == 0)
                        return '';
                    else
                        return `${(value / 10000).toLocaleString()}万`;
                },
                mirror: true,
            },
            grid: {
                drawOnChartArea: false,
            }
        }
    }
}
            
chart.update();
```
![出来高スケール変更](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/388d5bb4-68df-d322-a4ed-e8984d0cc5ff.png)


今回は、とりあえずここまで。