LoginSignup
1
0

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

Posted at

csv ファイルを読み込んでチャート表示

カンマ区切りの csv ファイルを利用してチャートを表示してみました。

SBI 証券の HyperSbi2 では 20 年程度の時系列データをエクスポートできますのでこれを利用します。
そもそも HyperSbi2 でそのままチャートを表示すればいいだけですが、ここではデータの整形手順を説明するためにこのデータを利用します。

csv の構造

csv_4751.png

HyperSbi2 の csv データの特徴として、3 桁までの数値はそのままですが 4 桁以上の数値はダブルクォーテーションで囲まれたカンマを含む 3 桁区切りで格納されているのが特徴で、単純に.split(',').split('","')等では正確な価格を取得することができません。

そこで、各行の模試列を先頭から区切り文字ごとに分割して、残りの先頭文字がダブルクォーテーションかどうかで次の区切り文字を「,」ないし「"」として文字列を取得することにしました。

また、ここでは利用しませんが、MA 等のデータが存在しない列は「--」が記録されています。

本当は csv ファイルを同期的に読み込みたかったのですが、await 等は C#のものとは比べ物にならないくらい難しく12、気軽に入門はできないとあきらめて、以下のページを参考にして3、onload イベント内からチャート描画の function を呼ぶ方法にしました。
加えて、Mac と Windows でエクスポートしたファイルのエンコードが異なるようなので、encoding-japanese4を利用して自動判別できるようにしてみました5

SelectCsv.html
<!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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
<style>
    .info{font-size: small;}
    .json{width: 460px; height: 320px;}
</style>
</head>
<body>

    <div><input type="file" id="selectfile"><label class="info" id="info"></label></div>

    <div> <canvas id="chart"></canvas> </div>

    <!-- json出力エリア 手書き修正でチャートに反映することは未対応 -->
    <div><textarea class="json" id="json"></textarea></div>


    <script src="chart-viewer1.js"></script>
    <script src="chart-csv-reader.js"></script>

</body></html>
chart-csv-reader.js
document.getElementById("selectfile").addEventListener("change", function (event) {
    const file = event.target.files[0];
    const reader = new FileReader();
    // reader.readAsText(file);
    reader.readAsArrayBuffer(file);
    reader.onload = () => {
      // 8ビット符号なし整数値配列と見なす
      const array = new Uint8Array(reader.result);
      const strEncoded = autoEncoding(array);
      const prices = csvToArray(strEncoded);
      if (prices.error === undefined) {
        changeBrand(prices);
        document.getElementById("json").value = JSON.stringify(prices).replace( /},/g, "},\n" );
      } else {
        document.getElementById("json").value = `${prices.error} の列番号が認識できません\n中断しました`;
      }
    };
  });

// refer to https://setchi.hatenablog.com/entry/2014/07/25/231318
function autoEncoding(array) {
  // 文字コードを取得
  switch (Encoding.detect(array)) {
    case "UTF16":
      // 16ビット符号なし整数値配列と見なす
      array = new Uint16Array(e.target.result);
      break;
    case "UTF32":
      // 32ビット符号なし整数値配列と見なす
      array = new Uint32Array(e.target.result);
      break;
  }

  // Unicodeの数値配列に変換
  // Unicodeの数値配列を文字列に変換
  return Encoding.codeToString(Encoding.convert(array, "UNICODE"));
}

function csvToArray(content) {
  const lines = content.split("\n");
  // 1行目はフィールドであること 1行目から列番号取得
  const fields = lines[0].split(",");
  const fieldIndexes = [6];
  const label_eng = ["date", "open", "high", "low", "close", "vol"];
  const label_jp = ["日付", "始値", "高値", "安値", "終値", "出来高"];
  for (let j = 0; j < label_eng.length; j++) {
    for (let i = 0; i < fields.length; i++) {
      if ( fields[i].toLowerCase().indexOf(label_eng[j]) > -1 || fields[i].toLowerCase().indexOf(label_jp[j]) > -1 ) {
        fieldIndexes[j] = i;
        break;
      }
    }
    if (j < 5 && fieldIndexes[j] === undefined) {
      return { error: label_jp[j] };
    }
  }
  // 汎用性を高めるためcsvフィールド名を解析する仕様にしたら複雑になりすぎ 勘違いがあるかも
  const prices = [];
  for (let i = 1; i < lines.length; i++) {
    if (lines[i] === "") continue;
    const cols = [];
    let line = lines[i];
    // 3桁は数値、4桁以上は3桁区切り ""で囲まれている
    for (let j = 0; j < fields.length; j++) {
      const delimiter = line.substr(0, 1) == '"' ? '"' : ",";
      if (delimiter === '"') line = line.substr(1);
      if (fieldIndexes.includes(j)) {
        cols[fieldIndexes.indexOf(j)] = line.substr(0, line.indexOf(delimiter)).replace(/,/g, "");
      }
      line = line.substr(line.indexOf(delimiter) + (delimiter === '"' ? 2 : 1));
    }
    // luxonは「/」区切りの日付は認識出来ないようだ
    // adjust priceがある場合には未対応でもう一工夫が必要(各priceに * adjust / close)
    const date = cols[0].replace(/\//g, "-").replace("", "-").replace("", "-").replace("", "");
    const data = {
      d: date, // console.log等で確認用に
      x: luxon.DateTime.fromSQL(date).valueOf(),
      o: Number(cols[1]),
      h: Number(cols[2]),
      l: Number(cols[3]),
      c: Number(cols[4]),
    };
    if (cols[5] !== undefined && !isNaN(cols[5])) {
      data.v = Number(cols[5]);
    }
    prices.push(data);
  }
  // csvは逆順のため日付で昇順に constもsortできるようだ
  return prices.sort((a, b) => a.x - b.x);
}

時系列データは逆順のままでもチャートは昇順に表示されるようですが、マウスポインタの移動で表示されるツールチップの挙動がおかしくなるようなので、昇順にソートをしています。

取得した時系列データ配列を chart-viewer1.jsの changeBrand に渡すと同時にTextAreaにJsonデータとして出力します。

chart-viewer1.js
const colors = {
    candle: { up: "orangered", down: "limegreen", unchanged: "dimgray" },
    volume: "blue",
};

const ctx = document.getElementById("chart").getContext("2d");
const chart = new Chart(ctx, {
    type: "candlestick",
    data: { datasets: [] },
});

// チャートに表示するデータを格納したグローバルデータ変数
let chartData = {};

// 銘柄(ファイル)変更
function changeBrand(dailyPrices) {
    chartData = {
        candle: dailyPrices,
        volume: dailyPrices.find(d => d.v) ? dailyPrices.map(d => { return { x: d.x, y: d.v } }) : undefined,
    };
    document.getElementById("info").textContent = 
        `${dailyPrices[0].d} ${dailyPrices[dailyPrices.length - 1].d}${dailyPrices.length.toLocaleString()}本`;
    changeDatasource();
}

function changeDatasource(barCount = 0) {
    if (chartData.candle === undefined) return;
    // barCount 0 の場合は全て表示する データ期間のフィルター用
    if (barCount === 0) 
        barCount = chartData.candle.length;
    
    chartData.from = {
        index: chartData.candle.length - barCount,
        d: chartData.candle[chartData.candle.length - barCount].d,
        x: chartData.candle[chartData.candle.length - barCount].x,
    };

    // chartDataからchartのデータセットを設定していく
    // ここで各データに関して表示本数でフィルターをかける
    const datasets = [];
    const data_candles = {
        data: chartData.candle.filter(d => d.x >= chartData.from.x),
        color: colors.candle,
        yAxisID: "yAxisMain",
    };
    datasets.push(data_candles);
    if (chartData.volume !== undefined) {
        const ds_volume = {
        type: "bar",
        label: "volume",
        data: chartData.volume.filter(d => d.x >= chartData.from.x),
        borderColor: colors.volume,
        backgroundColor: colors.volume,
        borderWidth: 1,
        pointRadius: 0,
        yAxisID: "yAxisSub",
        };
        // 出来高データを追加
        datasets.push(ds_volume);
    }
    chartData.datasets = datasets;
    refreshChart(chartData);
}

// チャートにデータセットの設定とオプション設定
function refreshChart(chartData) {
    chart.config.data.datasets = chartData.datasets;
    chart.config.options = {
        plugins: {
        title: { display: false },
        legend: {
            position: "top",
            align: "center",
            labels: {
                filter: function (item) {
                    // console.log(item);
                    //ローソク足とvolumeの凡例を非表示
                    if (item.datasetIndex === 0 || item.text === "volume") return false;
                    else return true;
                },
                boxWidth: 6,
                boxHeight: 2,
            },
        },
        },
        //再度アクセスする場合に初回定義しなければ undefined となる
        scales: {},
    };

    if ( chart.config.data.datasets.length > 1 && chart.config.data.datasets[1].label == "volume") {
        //データセットから出来高max取得
        const volumes = chart.config.data.datasets[1].data.map(d => { return d.y });
        const volume_max = volumes.reduce(function (a, b) { return Math.max(a, b) });
        chart.config.options.scales.yAxisSub = {
            suggestedMax: volume_max * 3,
            position: "right",
            ticks: {
                callback: function (value, index) {
                if (value > volume_max || value < (volume_max * 2) / 4 || value == 0)
                    return "";
                else 
                    return `${(value / 10000).toLocaleString()}万`;
                },
                mirror: true,
            },
            grid: {
                drawOnChartArea: false,
            },
        };
    }

    chart.update();
}

もっとシンプルな方法もあると思います。

その他参考にしたページ678910

lasertec.png

参考ページ

  1. async/await 入門(JavaScript)

  2. JavaScript の「コールバック関数」とは一体なんなのか

  3. JavaScript ファイル読み込み完全ガイド!10 の使い方とサンプルコード

  4. encoding.js

  5. 【FileAPI, readAsText】JavaScript で文字コードを判別して文字化けを倒す

  6. replace の文字列置換・正規表現の使い方まとめ

  7. 【JavaScript】オブジェクト(連想配列)を昇順・降順に並び替える方法

  8. JavaScript における JSON データの読み込みや値取得の方法を解説

  9. 【JavaScript】配列の最大値(最小値)を取得するには reduce を使うのがいいらしい

  10. 配列を征する者は JS を制す。JavaScript のスマートな配列操作テクニック 2023

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0