- 記事一覧 >
- ブログ記事
Material UI(MUI) Recharts WebSocket FastAPIでリアルタイムグラフ描画
はじめに
MUI(Material-UI)v5 Recharts WebSocket FastAPI でリアルタイムグラフ描画の画面を作ってみました。
今回、
フロントエンド:React, MUI(Material-UI)v5, Recharts
バックエンド:FastAPI
通信手段:WebSocket
によるリアルタイムグラフ描画 が実現するまでの手順を紹介していきたいと思います。
【検証環境】
Windows 10 Pro x64
Visual Studio Code 1.69.0
Raspberry Pi Desktop OS(Linux raspberry 4.19.0-13-amd64)
node 14.16.0
npm 6.14.11
react 18.2.0
recharts 2.1.12
mui/material 5.8.7
Python 3.7.3
pip 22.1.2
fastapi 0.78.0
uvicorn 0.18.2
この記事に Recharts の実装の話はほぼありません。(既存の実装にデータを渡しているだけです。)
MUI(Material-UI)v5
まず、MUI(Material-UI)v5 に対応したプロジェクトを作成します。
今回の趣旨とは全く関係無いですが、なんとなく、Tailwind CSS に対応した状態で始めたかったので、https://github.com/mui/material-ui/tree/master/examples/tailwind-css
をベースに採用します。
$ curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/tailwind-css
$ cd tailwind-css
$ npm install
$ npm start
OK!
Dashboard
画面を作り込まず、テンプレートの "Dashboard" を採用します。
https://github.com/mui/material-ui/tree/v5.8.7/docs/data/material/getting-started/templates/dashboard
から*.tsx
をダウンロードします。
↓
Chart.tsx
Dashboard.tsx
Deposits.tsx
Orders.tsx
Title.tsx
listItems.tsx
$ mkdir src/dashboard
src/dashboard
に*.tsx
を置きます。
src/App.tsx
を以下の内容にします。
import React from 'react';
import DashboardContent from './dashboard/Dashboard';
const App = () => {
return <DashboardContent />;
};
export default App;
確認します。
$ npm start
依存パッケージが足りていなくて、エラーになります。
依存パッケージをインストールします。
$ npm install recharts @mui/icons-material
確認します。
$ npm start
ダッシュボードが表示されます。 なお、データは固定で、いろいろクリックしてもほとんど何も起きません。
FastAPI 側
FastAPI 側(WebSocket サーバー側)を作成します。
https://github.com/ustropo/websocket-example
を参考にしました。
こちらの場合、今回と異なり、フロントエンドからメッセージ送信して、それをきっかけにして、データを取り出しています。フロントエンドの setInterval で1秒毎になっています。また、フロントエンドは、jsです。
python3, pip3 はあらかじめ入っている環境です。
ただ、そのままインストールしようとすると、エラーになりましたので、一旦 setuptools をアップグレードしています。
# pip3 install fastapi "uvicorn[standard]"
・・・
from setuptools.command.build import build as CommandBuild # type: ignore[import]
ModuleNotFoundError: No module named 'setuptools.command.build'
----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-804avrft/maturin/
----------------------------------------
Command "/usr/bin/python3 -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-nshvgw65 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple --extra-index-url https://www.piwheels.org/simple -- maturin>=0.12,<0.13" failed with error code 1 in None
↓
# pip3 install --upgrade pip setuptools
# pip3 install fastapi "uvicorn[standard]"
# vi run.py
以下の内容で実装します。
/ws
で待ち受けて、コネクションが有ったら、1秒に1回
{
"amount": 0~1までのランダムな数値,
"time": 現在時刻のUNIX TIME
}
の json を返す仕様です。
from fastapi import FastAPI, WebSocket
import random
import time
import asyncio
app = FastAPI(title='WebSocket Example')
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
print('a new websocket to create.')
await websocket.accept()
while True:
try:
resp = {'amount': random.uniform(0, 1), 'time': time.time()}
print('resp:', resp)
await websocket.send_json(resp)
await asyncio.sleep(1)
except Exception as e:
print('error:', e)
break
print('Bye..')
起動します。何も指定が無い場合、8000 番ポートで待ち受けます。
# uvicorn --host "0.0.0.0" run:app
Chart.tsx
グラフのコンポーネントは、src/dashboard/Chart.tsx
です。
これに固定値 const data =
が書かれていますので、コメントアウトして、引数で data
を受け取るようにします。
function createData(time: string, amount?: number) {
return { time, amount };
}
const data = [
createData('00:00', 0),
createData('03:00', 300),
createData('06:00', 600),
createData('09:00', 800),
createData('12:00', 1500),
createData('15:00', 2000),
createData('18:00', 2400),
createData('21:00', 2400),
createData('24:00', undefined),
];
export default function Chart() {
↓
// function createData(time: string, amount?: number) {
// return { time, amount };
// }
// const data = [
// createData('00:00', 0),
// createData('03:00', 300),
// createData('06:00', 600),
// createData('09:00', 800),
// createData('12:00', 1500),
// createData('15:00', 2000),
// createData('18:00', 2400),
// createData('21:00', 2400),
// createData('24:00', undefined),
// ];
interface Props {
data: {
time: string;
amount: number;
}[];
}
export default function Chart({ data }: Props) {
Dashboard.tsx
Chart コンポーネントは、src/dashboard/Dashboard.tsx
で import
していますので、Dashboard.tsx
で WebSocket 通信をして、得られたデータを Chart コンポーネントに渡します。
("ws://192.168.11.6:8000/ws")
のIPアドレスは、環境によって変更が必要です。
function DashboardContent() {
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
return (
<ThemeProvider theme={mdTheme}>
↓
function DashboardContent() {
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
type Message = {
time: string;
amount: number;
};
const [message, setMessage] = React.useState<Message[]>([]);
const ws: React.MutableRefObject<WebSocket | null> = React.useRef(null);
React.useEffect(() => {
ws.current = new WebSocket("ws://192.168.11.6:8000/ws");
ws.current.onopen = () => {
console.log("Connection Established!");
};
ws.current.onclose = () => {
console.log("Connection Closed!");
};
ws.current.onerror = (event) => {
console.log("WS Error", event);
};
return () => {
if (ws.current !== null) ws.current.close();
};
}, []);
React.useEffect(() => {
if (!ws.current) return;
ws.current.onmessage = (evt: MessageEvent) => {
const response: Message = JSON.parse(evt.data);
if (!message.length) {
// 空配列の場合
setMessage([response]);
} else if (message.length > 19) {
// 配列が20個以上の場合
setMessage([...message.slice(1), response]);
} else {
setMessage([...message, response]);
}
};
}, [message]);
return (
<ThemeProvider theme={mdTheme}>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
height: 240,
}}
>
<Chart />
</Paper>
↓
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
height: 240,
}}
>
<Chart data={message} />
</Paper>
起動します。
$ npm start
注意:サーバー側も起動しておきます。
# uvicorn --host "0.0.0.0" run:app
OK!!
画面遷移したり、ブラウザを閉じると、コネクションが閉じます。
時刻が UNIX TIME のままだったり、目盛りが表示しきれていなかったりしますが、あとは頑張れば良いということで、以上!
React 実装の説明
最初鼻歌交じりに、以下の実装にしていたのですが、浅はかでした。
一度しか定義されない onmessage
のコールバック関数にとって、その時の message
(空 = []
)しか覚えていなくて、データの追加がうまくいきませんでした。
type Message = {
time: string;
amount: number;
};
const [message, setMessage] = React.useState<Message[]>([]);
React.useEffect(() => {
console.log("useEffect");
const ws = new WebSocket("ws://192.168.11.6:8000/ws");
ws.onmessage = (evt: MessageEvent) => {
const response: Message = JSON.parse(evt.data);
if (!message.length) {
// 空配列の場合
setMessage([response]);
} else if (message.length > 19) {
// 配列が20個以上の場合
setMessage([...message.slice(1), response]);
} else {
setMessage([...message, response]);
}
};
ws.onopen = () => {
console.log("Connection Established!");
};
ws.onclose = () => {
console.log("Connection Closed!");
};
ws.onerror = (event) => {
console.log("WS Error", event);
};
return () => {
if (ws !== null) ws.close();
};
}, []);
かと言って、useEffect の第二引数を}, [message);
にすると、コネクト →message 受信 と高速で繰り返し、無限ループします。
結果、
・接続切断関係の処理は一回だけ。
・onmessage 関係の処理は、message 受信毎。
と分離するとうまくいきました。
さらに、以下の対応が必要でした。
・WebSocket オブジェクト ws
は、useEffect の中ではなく、外側のスコープで useRef の参照として宣言。
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。