Reactで使用する株価ローソク足表示ライブラリはAppache Echartsがおすすめです。株価分析Webアプリでのカスタマイズ事例紹介
個人開発した、米国株主要銘柄の株価・業績分析Webアプリにおいて、グラフ表示ライブラリとして、Appache Echartsを使用しました。TanStackQuery(ReactQuery)との併用で、ユーザーが入力・編集したMarker情報をチャート上にリアルタイムで表示・更新する機能を実装しましたので、その概要も一部コードを交えながら紹介したいと思います。
要件
まず、この株価分析Webアプリで実現したかったのは、以下のイメージ。
株価と業績と社会イベントが一目でわかるチャートを銘柄ごとに表示させたいと考えました。
ポイントは、以下2点です。
- ローソク足(月足)折れ線領域(業績:EPS等)グラフを重ね合わせる。
- ユーザーが入力した、注釈データをチャート上にプロットできる。
ECharts Candle Stick Samples
チャートライブラリの比較
比較対象
- Chart.js : chartjs-financial-pluginとの併用で、株価ローソク足対応は可能だが、マーカー対応はなさそう。
見た目もいまいち。 - ReCharts : 追加ライブラリなどとの併用で、株価ローソク足対応は可能だが、マーカー対応はなさそう。
- ApexCharts :https://apexcharts.com/react-chart-demos/
株価ローソク足対応あり、マーカー対応はなさそう。 - Ignite UI for React ファイナンシャル / 株価チャート : https://jp.infragistics.com/products/ignite-ui-react/react/components/charts/types/stock-chart
株価ローソク足対応かなり豊富。マーカー対応あり。但し、基本的に有料。 - WIJIMO FinancialChart : https://demo.grapecity.com/wijmo/demos/Chart/Financial/EventAnnotations/react
株価ローソク足対応かなり豊富。マーカー対応あり。但し、基本的に有料。 - HightCharts : https://www.highcharts.com/blog/products/stock/
株価ローソク足対応かなり豊富。マーカー対応あり。(Annotation)但し、基本的に有料。 - lightweight-charts : https://tradingview.github.io/lightweight-charts/
Finance系に特化した高機能ライブラリ、サーバーサイドレンダリング対応していないので、対象外。
(実際の調査は2022年の4月頃に行いました、現在は、ライブラリのアップデート等で、状況が変わっている可能性があります。)
有力な候補については、それぞれのライブラリ毎に、簡単なPOCアプリを作成して、要件が満たされるかどうかを見極めました。
比較の結果、最終的にEchartsを選択しました。
デザインの良さや、カスタマイズ性の高さ、扱いやすさ、及び価格面(無料)でのバランスがよいと感じました。
ECharts Candle Stick Samples
また、Next.jsとの相性もよいです。
・TypeScript対応
・SG(Static Generation)に対応しているので、表示が高速です。
ECharts サーバーサイドレンダリング対応
参考(ApacheEcharts表示ページのLighthouseスコア)
実装
では実際にどのように実装したか、一部情報をピックアップして以下ご紹介いたします。
ApacheEchartsについての基本的なライブラリインストール等については、割愛します。
前提環境
Next.js
TypeScript
TanStackQuery(ReactQuery)
ReduxToolkit
Supabase(バックエンドデータベース)
モジュール構成
コンポーネントの構成
コード
Appache ECharts コンポーネントのoption 設定(関数として別定義)
series の中の、markPoint に、マーカーのデータを渡しています。
// Chart Option ==============================================================
import { EChartsOption } from 'echarts-for-react'
export const chartOption = (
newDateData: Array<string>,
filteredDateForBarChart: Array<string>,
newPriceData: Array<Array<number>>,
markerChartData: Array<Object>,
slicedResultTheoryStockPriceAsset: Array<number>,
slicedResultTheoryStockPriceOperation: Array<number>,
): EChartsOption => {
return {
// (省略)
series: [
{
type: 'candlestick',
data: newPriceData,
grid: 0,
xAxisIndex: 0,
yAxisIndex: 0,
markPoint: {
label: {},
data: markerChartData,
},
},
// (省略)
],
}
}
マーカーのデータを作成する関数
API経由で受け取った、marker配列のデータを、Echartsのマーカー用のデータに加工しています。
coord プロパティで、markerの表示位置を調整しています。
item.dateは横軸の位置、highPriceは縦軸の位置を示す値です。
縦軸は、株価の高値の1.1倍とし、ローソク足と重ならないようにしています。
const markerChartData = marker.map((item: any, i: number) => {
const highPrice = priceData.find((value: any) => value.date === item.date)?.High * 1.1
return {
value: item.value,
coord: [item.date, highPrice],
name: item.name,
date: item.date,
itemStyle: {
color: 'rgb(41,60,85)',
},
}
})
更新用のカスタムフック
Supabaseデータの更新が完了したら、onSuccess移行の処理で、キャッシュを更新します。
また、Reduxのstoreにある、編集中のマーカー情報をリセットします。
import { useQueryClient, useMutation } from 'react-query'
import { useDispatch } from 'react-redux'
import { resetEditedMarker } from '../store/editInfoSlice'
import { supabase } from '../utils/supabase'
import { EditedMarker, Marker } from '../types/StoreTypes'
export const useMutateMarker = () => {
const queryClient = useQueryClient()
const dispatch = useDispatch();
const reset = () => dispatch(resetEditedMarker());
const createMarkerMutation = useMutation(
async (marker: Partial<Marker[]> | Partial<Marker>) => {
const { data, error } = await supabase.from('marker').insert(marker)
if (error) throw new Error(error.message)
return data
},
{
onSuccess: (res) => {
const previousMarkers =
queryClient.getQueryData<Omit<Marker, 'user_id' | 'created_at'>[]>('marker')
if (previousMarkers) {
queryClient.setQueryData('marker', [...previousMarkers, res[0]])
}
reset()
},
onError: (err: any) => {
alert(err.message)
reset()
},
}
)
const updateMarkerMutation = useMutation(
async (marker: EditedMarker) => {
const { data, error } = await supabase
.from('marker')
.update({ memo: marker.memo, date: marker.date })
.eq('id', marker.id)
if (error) throw new Error(error.message)
return data
},
{
onSuccess: (res, variables) => {
const previousMarkers =
queryClient.getQueryData<Omit<Marker, 'user_id' | 'created_at'>[]>('marker')
if (previousMarkers) {
queryClient.setQueryData<Omit<Marker, 'user_id' | 'created_at'>[]>(
'marker',
previousMarkers.map((marker) => (marker.id === variables.id ? res[0] : marker))
)
}
reset()
},
onError: (err: any) => {
alert(err.message)
reset()
},
}
)
const deleteMarkerMutation = useMutation(
async (id: number) => {
const { data, error } = await supabase.from('marker').delete().eq('id', id)
if (error) throw new Error(error.message)
return data
},
{
onSuccess: (_, variables) => {
const previousMarkers =
queryClient.getQueryData<Omit<Marker, 'user_id' | 'created_at'>[]>('marker')
if (previousMarkers) {
queryClient.setQueryData(
'marker',
previousMarkers.filter((marker) => marker.id !== variables)
)
}
reset()
},
onError: (err: any) => {
alert(err.message)
reset()
},
}
)
return { deleteMarkerMutation, createMarkerMutation, updateMarkerMutation }
}
完成形UI
最終的なWebアプリの画面は以下の通り
Next.jsのSG(Static Generation)を使用しているにも関わらず、マーカーの情報はリアルタイムで反映されます。
TanStackQuery(ReactQuery)のキャッシュを更新していることで実現しています。
株価チャートにマーカーを追加・即時反映
さいごに
ApacheEchartsは、株価のローソク足チャート以外にも様々な、チャート表示が可能ですので、興味を持たれた方は検討してみてはいかがでしょうか。
Discussion