😸

Reactで使用する株価ローソク足表示ライブラリはAppache Echartsがおすすめです。株価分析Webアプリでのカスタマイズ事例紹介

2023/05/04に公開

個人開発した、米国株主要銘柄の株価・業績分析Webアプリにおいて、グラフ表示ライブラリとして、Appache Echartsを使用しました。TanStackQuery(ReactQuery)との併用で、ユーザーが入力・編集したMarker情報をチャート上にリアルタイムで表示・更新する機能を実装しましたので、その概要も一部コードを交えながら紹介したいと思います。

要件

まず、この株価分析Webアプリで実現したかったのは、以下のイメージ。
株価と業績と社会イベントが一目でわかるチャートを銘柄ごとに表示させたいと考えました。

ポイントは、以下2点です。

  1. ローソク足(月足)折れ線領域(業績:EPS等)グラフを重ね合わせる。
  2. ユーザーが入力した、注釈データをチャート上にプロットできる。

Requirement Image
ECharts Candle Stick Samples

チャートライブラリの比較

比較対象

(実際の調査は2022年の4月頃に行いました、現在は、ライブラリのアップデート等で、状況が変わっている可能性があります。)

有力な候補については、それぞれのライブラリ毎に、簡単なPOCアプリを作成して、要件が満たされるかどうかを見極めました。

比較の結果、最終的にEchartsを選択しました。
デザインの良さや、カスタマイズ性の高さ、扱いやすさ、及び価格面(無料)でのバランスがよいと感じました。

ApacheEcharts Candle Sample
ECharts Candle Stick Samples

また、Next.jsとの相性もよいです。
・TypeScript対応
・SG(Static Generation)に対応しているので、表示が高速です。
ECharts サーバーサイドレンダリング対応

参考(ApacheEcharts表示ページのLighthouseスコア)

Lighthouse Score

実装

では実際にどのように実装したか、一部情報をピックアップして以下ご紹介いたします。
ApacheEchartsについての基本的なライブラリインストール等については、割愛します。

前提環境

Next.js
TypeScript
TanStackQuery(ReactQuery)
ReduxToolkit
Supabase(バックエンドデータベース)

モジュール構成

Module Structure
コンポーネントの構成

コード

Appache ECharts コンポーネントのoption 設定(関数として別定義)

series の中の、markPoint に、マーカーのデータを渡しています。

chartOption.ts
// 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倍とし、ローソク足と重ならないようにしています。

StockCandleChart.tsx(一部抜粋)

  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にある、編集中のマーカー情報をリセットします。

useMutateMarker.ts
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)のキャッシュを更新していることで実現しています。

CandleStick with Marker
株価チャートにマーカーを追加・即時反映

さいごに

ApacheEchartsは、株価のローソク足チャート以外にも様々な、チャート表示が可能ですので、興味を持たれた方は検討してみてはいかがでしょうか。

Discussion