はじめに
この記事で作るものとできること
この記事では、楽天証券の注文履歴CSVファイルをもとに、注文の集中している時間帯をグラフで可視化するアプリを Vue 3 + TypeScript + Chart.js で作成する。
CSVの読み込みには PapaParse、文字コード変換には encoding-japanese を使用し、ANSIにも対応。
また、Chart.jsを用いて注文数を30分ごとの時間帯で集計・表示し、さらに表形式でも同じ情報を表示する。
このアプリを作ることで、次のようなことができる。
↓
・楽天証券からダウンロードしたCSVを読み込んで分析
・注文が集中しやすい時間帯を 棒グラフで視覚化
・30分ごとの時間帯に分類して、テーブルで数値を一覧表示
・CSVファイルのエンコード(Shift_JIS)に対応したアップロード処理
VueとTypeScriptでグラフ表示アプリを実装してみる。
対象とするCSVファイルの中身を確認
今回扱うのは、楽天証券のウェブ取引画面からエクスポートできる「注文履歴」のCSVファイル。
注意点としては、「取引履歴」ではないところ。取引履歴では取引の「日」まではわかるが、「時間」がわからない。今回はあくまで自分の注文時間の癖を視覚化したいという意図で作成するので、何時何分に注文を出したのかというのが重要である。
下記のような画面からCSVをダウンロードできる。
私の場合は日本株において信用取引を利用しているが、現物をやっている人は注文種類を変えればいい。とにかくCSVが手に入れば、あとは加工するだけ。
最大1か月分の注文履歴をまとめてダウンロードでき、内容は以下のような列構成になっている。
(Excelで表示したものをコピペしただけなので、カンマは入っていない)
1 |
注文番号 アルゴ注文番号 繰越区分 状況 状況(逆指値) セット注文 注文日時 執行条件 注文期限 銘柄 銘柄コード・市場 取引 売買 口座 注文方法 アルゴ注文情報 逆指値条件 セット注文条件 信用区分(弁済期限) 注文数量[株/口] 約定数量[株/口] 注文単価[円] 約定単価[円] 現在値[円] 約定代金[円] 手数料[円] |
このうち、今回のアプリでは 注文日時 列のみを使用し、注文が行われた時間帯を30分刻みで集計・グラフ表示してみる。
そのためその他の列については今回の処理に直接関係しないが、将来的に「売買方向」や「銘柄ごとの傾向分析」を行いたい場合には利用が可能。
なお、このCSVファイルは一般的に Shift_JIS 形式(ANSI)でエンコードされているため、文字化けを防ぐために encoding-japanese ライブラリを使用してUnicodeへ変換する処理も組み込んでいる。
開発環境の準備
使う技術:Vue 3, TypeScript, Chart.js, vue-chartjs
Vue 3やTypeScriptについて初心者だよという方はこちらから
・【TypeScript入門】MacのVSCodeで環境構築&簡単なサンプルを作ってみよう
・Vue.jsってなに?TypeScriptで始める、Vue 3の基本と画面表示までの流れ① 〜概要編〜
Vue 3は、柔軟かつ学習コストの低いJavaScriptフレームワーク。テンプレート構文がシンプルで直感的なだけでなく、内部的には非常に高速かつ拡張性の高い仕組みが整えられている。本記事では、Composition API を採用。これはVue 3で正式導入された記述スタイルで、状態管理やロジックを関数的にまとめることができるのが特徴である。従来のOptions APIに比べて、ロジックの再利用性が高く、型安全なコードが書きやすいというメリットがある。TypeScriptはJavaScriptのスーパーセットで、静的型付けによりバグの予防とコード補完の精度向上を実現。Vue 3はもともとTypeScriptを前提に設計されており、型定義ファイルも充実している。
Chart.jsとvue-chartjsは本サイトでは初出。
Chart.jsは、JavaScriptで動作する軽量かつ高機能なグラフ描画ライブラリである。棒グラフ、折れ線グラフ、円グラフなど、さまざまな種類のチャートを簡単に描くことができる点が特徴であり、設定方法も直感的であるため、初心者にも扱いやすい。Canvas要素を利用して描画を行うため、描画パフォーマンスにも優れており、動的なグラフの更新やレスポンシブ対応も比較的容易に実現できる。一方、vue-chartjsは、Chart.jsをVue.jsのコンポーネントとして扱いやすくするために作られたラッパーライブラリである。Chart.jsは本来、純粋なJavaScriptで操作するため、Vueでの開発にそのまま組み込もうとすると、ライフサイクルの管理やリアクティブなデータ更新との整合が煩雑になりやすい。vue-chartjsはその煩雑さを取り除き、Vueのコンポーネント内でChart.jsを簡潔かつ自然に扱えるようにしてくれる。Vue 3のComposition APIにも対応しているため、モダンな開発スタイルと親和性が高い。本記事では、vue-chartjsを通じてChart.jsの機能をVue 3アプリケーションに組み込み、TypeScriptと連携させながら、CSVで取り込んだ注文履歴データを棒グラフとして可視化する構成を採用する。これにより、Vueのデータバインディングの強みを活かしつつ、視覚的に洗練されたグラフを表示することが可能となる。
必要なライブラリとインストール手順、ファイル構成
Vite + Vue プロジェクトの作成
今回のアプリケーションでは、まずViteを用いてVue 3 + TypeScriptの開発環境を構築する。そのうえで、CSVファイルの読み取りやグラフの描画に必要な外部ライブラリを追加していく。
まず最初に、Viteを使ってVue 3 + TypeScriptプロジェクトを作成する。
ターミナル、もしくはVSCode内のターミナルで下記を実行。
1 2 3 |
npm create vite@latest vue-csv-chart-app -- --template vue-ts cd vue-csv-chart-app npm install |
npm install するパッケージ一覧
次に必要なライブラリを追加する。本記事で扱う機能の実装には、以下の4つのパッケージが必要である。
- papaparse:CSVファイルの読み込み・解析を行うライブラリで、CSVをJSON形式に変換する処理を簡単に実装
- encoding-japanese:Shift_JISをUTF-8に変換
- chart.js:グラフ描画を担うメインライブラリ
- vue-chartjs:Chart.jsをVueコンポーネントとして扱うためのラッパーで、Vueのライフサイクルやリアクティブデータと自然に統合できるようにしてくれる
1 |
npm install papaparse encoding-japanese chart.js vue-chartjs |
アプリの構成と全体コードの概要
Viteによって作成される初期プロジェクトには App.vue および HelloWorld.vue が用意されているが、今回はこの HelloWorld.vue は使用せず、新たに作成した CsvParser.vue を主役とする構成で進める。具体的には、App.vue の中で CsvParser.vue を読み込み、そのコンポーネントを表示する形とする。これは、Vueアプリにおいてルートコンポーネントから自作の機能を呼び出す基本的な構成であり、今後の開発においても応用可能な手法である。初期の App.vue は、以下のようなコードに書き換える。
1 2 3 4 5 6 7 |
<template> <CsvParser /> </template> <script setup lang="ts"> import CsvParser from './components/CsvParser.vue'; </script> |
CsvParser.vueは下記のように書いた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
<template> <div class="p-4"> <h1 class="text-xl font-bold mb-4">注文傾向の時間帯分析</h1> <input type="file" accept=".csv" @change="handleFileUpload" class="mb-4" /> <div v-if="Object.keys(hourlyCounts).length"> <Bar :data="chartData" :options="chartOptions" class="mb-6" /> <h2 class="text-lg font-semibold mb-2">注文数一覧(時間帯別)</h2> <table class="table-auto w-full border border-gray-300 text-sm"> <thead> <tr> <th class="border px-2 py-1">時間帯</th> <th class="border px-2 py-1">注文数</th> </tr> </thead> <tbody> <tr v-for="(count, time) in sortedHourlyCounts" :key="time"> <td class="border px-2 py-1">{{ time }}</td> <td class="border px-2 py-1">{{ count }}</td> </tr> </tbody> </table> </div> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import Papa from 'papaparse'; import Encoding from 'encoding-japanese'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'; import { Bar } from 'vue-chartjs'; ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale); const hourlyCounts = ref<{ [hour: string]: number }>({}); const chartData = ref({ labels: [] as string[], datasets: [ { label: '注文数(30分単位)', data: [] as number[], backgroundColor: '#3b82f6', }, ], }); const chartOptions = { responsive: true, plugins: { legend: { display: true, }, }, }; const sortedHourlyCounts = computed(() => { return Object.fromEntries(Object.entries(hourlyCounts.value).sort(([a], [b]) => (a < b ? -1 : 1))); }); function handleFileUpload(event: Event) { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const arrayBuffer = e.target?.result as ArrayBuffer; const uint8Array = new Uint8Array(arrayBuffer); const encoding = Encoding.detect(uint8Array); const text = Encoding.convert(uint8Array, { to: 'UNICODE', from: encoding, type: 'string', }) as string; const parsed = Papa.parse(text, { header: true, skipEmptyLines: true }); const raw = parsed.data as any[]; const timeCountMap: { [key: string]: number } = {}; raw.forEach((row) => { const datetime = row['注文日時']?.trim(); if (!datetime) return; const [_, timePart] = datetime.split(' '); if (!timePart) return; const [hour, minute] = timePart.split(':').map(Number); if (isNaN(hour) || isNaN(minute)) return; const startHour = hour.toString().padStart(2, '0'); const startMinute = minute < 30 ? '00' : '30'; const endHour = minute < 30 ? startHour : ((hour + 1) % 24).toString().padStart(2, '0'); const endMinute = minute < 30 ? '30' : '00'; const slotLabel = `${startHour}:${startMinute}-${endHour}:${endMinute}`; timeCountMap[slotLabel] = (timeCountMap[slotLabel] || 0) + 1; }); const sortedKeys = Object.keys(timeCountMap).sort(); hourlyCounts.value = timeCountMap; chartData.value.labels = sortedKeys; chartData.value.datasets[0].data = sortedKeys.map((k) => timeCountMap[k]); }; reader.readAsArrayBuffer(file); } </script> <style scoped> input[type='file'] { margin-bottom: 1rem; } </style> |
簡単にプログラムの全体の流れを説明すると、以下のとおり。
1.Vite が開発用サーバーを立ち上げる
2.index.html の div id="app" を見つける
3.main.ts が App.vue を #app にマウント
4.App.vue が CsvParser.vue を表示
5.CsvParser.vue が初期描画&ファイルアップロード受付
6.ユーザー操作によって handleFileUpload() が発火し、Chart.js を使ってグラフを描画
CSVファイルのアップロードとパース
FileReaderでファイルの読み込み。encoding-japaneseで文字コード変換
自分のパソコンに保存しているCSVファイルを読み込んで中身を解析する。そのために使用するのが、「FileReader」という機能である。たとえば、Webページ上にファイルを選ぶためのボタン(input type="file")を用意しておけば、ユーザーがCSVファイルを選んだ瞬間に「ファイルが選ばれましたよ」という合図(イベント)が発生する。その合図をキャッチし、選ばれたファイルの中身を読み取るのが FileReader の役割である。CSVの内容を読み取るにはいくつかの方法があるが、今回は「ArrayBuffer」という読み方を選ぶ。これは、ファイルの中身を「文字」ではなく「データのかたまり」として読み込む方法である。なぜそんな読み方をするかというと、楽天証券のCSVファイルは「ANSI」で作られており、そのまま文字として読むと文字化けが発生してしまうからである。そのため、一度「バイナリデータ」として読み込んでおいて、あとから「日本語に正しく変換する」という流れをとる。この変換処理は、別のライブラリ(encoding-japanese)に任せることになる。なお、FileReader はファイルの読み込みを始めるとすぐに結果が出るわけではない。ファイルが完全に読み込まれるのを待つ必要があり、その完了を知らせる「onload」というイベントを使って、次の処理を実行していく。このように「読み込み → 完了を待つ → 中身を処理する」という順番で動作するのが基本である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const arrayBuffer = e.target?.result as ArrayBuffer; const uint8Array = new Uint8Array(arrayBuffer); const encoding = Encoding.detect(uint8Array); const text = Encoding.convert(uint8Array, { to: 'UNICODE', from: encoding, type: 'string', }) as string; |
CSVファイルを選ぶと、handleFileUpload 関数が実行される。最初の処理は「ファイルが選ばれたか確認し、FileReaderで読み込む準備をする」というもの。直接文字列を読み込んでも文字コードの違いで文字化けが起こるかもしれないので、バイナリとして読み込んだ後、文字コード変換。encoding-japanese ライブラリを使っている。
注文時間帯の集計ロジックを実装
CSVファイルを読み込んだあとは、各注文がどの時間帯に発生しているかを分析する処理へと進む。ここでは、「注文日時」から時刻を抜き出し、それを30分単位の時間帯に丸めたうえで、件数を集計していく。Vueコンポーネント内部でこのロジックはすべてTypeScriptで実装されている。
注文日時から時刻を抽出する
CSVの各行には「注文日時」という列があり、そこには "2025/06/14 10:25" のような文字列が格納されている。まずはこの文字列から、注文が行われた時刻部分のみを取り出す必要がある。
コード上では次のように処理している。
1 2 3 4 5 6 |
const datetime = row['注文日時']?.trim(); if (!datetime) return; const [_, timePart] = datetime.split(' '); if (!timePart) return; const [hour, minute] = timePart.split(':').map(Number); if (isNaN(hour) || isNaN(minute)) return; |
注文日時の値を空白で分割し、日付部分と時刻部分に分けて、時刻を「10」と「25」のように数値として取得している。ここまでで、注文が「何時何分」に行われたかという情報が得られる。
30分刻みの時間帯に丸めるロジック
注文が「10:25」に行われた場合、それを「10:00〜10:30」という30分単位の枠に分類したい。これを実現するため、分の値(minute)に応じて、時間帯の開始・終了を計算している。
1 2 3 4 5 6 7 |
const startHour = hour.toString().padStart(2, '0'); const startMinute = minute < 30 ? '00' : '30'; const endHour = minute < 30 ? startHour : ((hour + 1) % 24).toString().padStart(2, '0'); const endMinute = minute < 30 ? '30' : '00'; const slotLabel = `${startHour}:${startMinute}-${endHour}:${endMinute}`; |
たとえば 10:25 であれば 10:00-10:30、10:45 であれば 10:30-11:00 といった具合に、時間帯ラベルを動的に生成している。このようにして全注文の時刻を適切なスロットへ分類していく。
TypeScriptで集計用マップを管理
生成した時間帯ラベルごとに、注文件数をカウントしていく。ここではTypeScriptのオブジェクトをマップとして使い、各時間帯ごとの注文数を記録している。
1 2 3 |
const timeCountMap: { [key: string]: number } = {}; timeCountMap[slotLabel] = (timeCountMap[slotLabel] || 0) + 1; |
時間帯ラベル(たとえば 10:00-10:30)をキーとして使い、すでに存在していればカウントを1つ増やし、なければ1から始めるという処理を行っている。
TypeScriptによる型注釈もいい感じ。
Chart.js用のデータ形式に変換
最終的に、この timeCountMap をもとに、Chart.js が扱いやすいデータ構造へと変換する。まずキーを時系列順にソートし、それに基づいてラベルと値を生成する。
1 2 3 4 |
const sortedKeys = Object.keys(timeCountMap).sort(); hourlyCounts.value = timeCountMap; chartData.value.labels = sortedKeys; chartData.value.datasets[0].data = sortedKeys.map((k) => timeCountMap[k]); |
hourlyCounts はVueの ref でリアクティブに保持されており、表としての表示にも再利用される。chartData は vue-chartjs コンポーネントに直接渡すためのオブジェクトであり、棒グラフのX軸には時間帯、Y軸には件数が反映されるようになる。
Chart.jsでグラフを表示する
集計した注文データを、Chart.js を用いて棒グラフにする。
Vueコンポーネント内では、Chart.js 単体ではなく、Vueラッパーである vue-chartjs を利用。
vue-chartjsの基本構成と描画設定
Chart.jsの描画はBarコンポーネントを用いて行う。これは vue-chartjs によって提供されており、以下のようにテンプレート内に記述するだけで、棒グラフの描画が可能。
1 |
<Bar :data="chartData" :options="chartOptions" class="mb-6" /> |
chartData は棒グラフに表示する実際のデータセット、chartOptions はグラフのスタイルやラベルの表示有無などを定義するオプションオブジェクトである。
以下のように書いている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const chartData = ref({ labels: [] as string[], datasets: [ { label: '注文数(30分単位)', data: [] as number[], backgroundColor: '#3b82f6', }, ], }); const chartOptions = { responsive: true, plugins: { legend: { display: true, }, }, }; |
labels には時間帯(例:"10:00-10:30")の配列、data には各時間帯に対応する件数の配列が格納される。色やラベル名もここで自由に変更可能である。
Chart.js 本体のプラグインや描画要素は、以下のように事前に登録しておく必要がある。
1 2 3 |
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'; ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale); |
上記により、ツールチップや凡例(legend)、目盛りのスケーリングといった基本機能が有効になる。
注文データの表形式表示を追加
グラフによる可視化は直感的で便利であるが、具体的な数値を確認するには表形式のデータ表示も有用である。
時間帯ごとの注文件数をテーブルとして表示し、棒グラフと並列して確認できるようしてみよう。
時間帯ごとの注文数をテーブルに出力
Vueテンプレート内では、v-for ディレクティブを用いて、30分刻みの時間帯と対応する注文数を繰り返し表示するテーブルを構築している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<h2 class="text-lg font-semibold mb-2">注文数一覧(時間帯別)</h2> <table class="table-auto w-full border border-gray-300 text-sm"> <thead> <tr> <th class="border px-2 py-1">時間帯</th> <th class="border px-2 py-1">注文数</th> </tr> </thead> <tbody> <tr v-for="(count, time) in sortedHourlyCounts" :key="time"> <td class="border px-2 py-1">{{ time }}</td> <td class="border px-2 py-1">{{ count }}</td> </tr> </tbody> </table> |
sortedHourlyCounts は、注文数が格納されたオブジェクトを時間帯の順に並び替えたもの。
1 2 3 4 5 |
const sortedHourlyCounts = computed(() => { return Object.fromEntries( Object.entries(hourlyCounts.value).sort(([a], [b]) => (a < b ? -1 : 1)) ); }); |
Vueのリアクティブシステムと v-for を組み合わせることで、集計データの更新に応じてテーブル表示も自動的に更新される。
グラフと表のレイアウト統合
グラフの下にテーブルを配置するだけでも、視線の流れが自然になり、理解しやすい構成となる。
1 2 3 |
<Bar :data="chartData" :options="chartOptions" class="mb-6" /> <!-- ↓にテーブルを配置 --> <table>...</table> |
おわりに
私が表示させると、下記のような画面となった。
今回は、楽天証券のCSVファイルを読み込んで、注文の時間帯を30分ごとに集計・可視化するシンプルなアプリを作成した。vue-chartjsを使ったグラフ表示や、encoding-japaneseでの文字コード変換など、実際のデータを扱う中で必要になる処理をひととおり組み込んでいる。とくに文字コード対応や時間帯の丸め処理などは、CSVデータを扱うときによく出てくる課題なので、今後なにか別のデータを可視化したいときにも応用がきくと思われる。
ここまで読んでもらい、感謝申し上げる。