徒然なるままに技術に触れて

Dash(Protly)で降水量表示アプリを作成

今回したこと

Pythonの可視化ライブラリであるDashを利用して気象庁が公開するデータを表示するアプリを作成した.
作成したアプリはHerokuにpushしてオンライン上で動いているが,若干動作がガタガタ...(Herokuはファイル保存ができないが無理やり保存しているせい)
URLはこちら
https://weather-dash-app.herokuapp.com
2019/8/27 *追記
コアサーバーをレンタルしてデプロイしてみた.
若干動作は遅くなったけど普通にファイルを読み書きできるので安定している.
http://catdance124.m24.coreserver.jp/dash/weather-app/
catdance124.hatenablog.jp


ソースはこちら
github.com
使用したデータソースはこちら
www.data.jma.go.jp

作成したアプリの概要

気象庁が公開するデータを取得し,pandasで加工したものをDashで地図上にプロットした.
地図上の点を選択すると,その地点の過去24時間降水量をグラフで見ることができる.
点は複数選択可能で比較することができる.
f:id:catdance124:20190824191911g:plain

作成に関すること

Dashとは

Dashとは,Web アプリケーション 用 Python フレームワークである.
Flask / Plotly.js / React.jsで構成されているらしい.
plot.ly
インストールは下記のように

pip install dash==1.1.1
pip install dash-daq==0.1.0

参考にしたもの

この記事から着想を得た
www.mazarimono.net
公式ユーザーガイドにはデモが豊富にあるので困ったらここを見る
dash.plot.ly
plot.ly
指定オプションに困ったら公式リファレンスを見る
これ以外を見る必要がないくらい充実しているので見るべし
plot.ly

気象庁が公開しているデータには緯度経度が含まれていないので,下記サイトから地点コード一覧を頂き照らし合わせる
washitake.com
Herokuで一時的にファイルを保存する方法
hungrykirby.hatenablog.com

工夫した点

コードはgithubにあるので全部見たい人はそちらで
GitHub - catdance124/weather_dash_app: 気象庁が公開する降水量データをDashで可視化するアプリケーション

下記コードは解説点以外省略している.

地点をクリックすると連動してグラフが描画される

メインの地図(id=precipitation)にclickmodeを設定し,クリックしたときにselectedDataが発生するようにした.
selectedDataをグラフ(id=precipitation_24)で受け取り,描画している.
clickmodeは下記を参照
Dash User Guide and Documentation - Dash by Plotly

app.layout = html.Div(
    children=[
        html.Div(
            [dcc.Graph(id ='precipitation', selectedData={'points': [{'customdata': 44132}]})],  <--selectedDataに初期値(東京)を指定
            style={'width':'80%', 'display': 'inline-block'}
        ),
        html.Div(
            [dcc.Graph(id ='precipitation_24h')],
            style={'width':'80%', 'display': 'inline-block'}
        ),
    ]
)

@app.callback(Output('precipitation', 'figure'),
            [Input('interval-component', 'n_intervals')])
def plot_precip(n):
    ~~~~~~~~~~~~~~~~~~~~~~~
    figure = {
        'data':[
            ~~~~~~~~~~~~~~~~~~~~~~~~
        ],
        'layout':
            go.Layout(
                ~~~~~~~~~~~~~~~~~~~~~~
                clickmode = 'event+select'              <--ここで地図に対しclickmodeを設定
            )
    }
    return figure

@app.callback(Output('precipitation_24h', 'figure'),
            [Input('precipitation', 'selectedData')])   <--24hグラフはselectedDataを受け取り描画される
def plot_precip_24h(selectedData):
    if selectedData is None:            <--初期化の際にエラー処理を挟む
        return dash.no_update
    else:
        selectedData = selectedData['points']
        data = []
        for sd in selectedData:     <--複数地点をグラフ描画
            ~~~~~~~~~~~~~~~~~~~~~~~~
            data.append(go.Scatter(
                x=city['Date'],
                y=city['precip'],
                mode='lines+markers',
                name=city_name,
                showlegend=True
            ))
        figure = {
            'data': data,
            'layout': dict(
                ~~~~~~~~~~~~~~~~~~~~~~~
            )
        }
        return figure
地点の色を降水量に合わせて色付け

アメダスを参考に0-1-5-10-20-30-50-80-100で色をつける.
気象庁 | アメダス
カラースケールの指定はリファレンスを参照
plot.ly

def get_colorscale():    <--0~1の割合でカラースケールを作成する
    colorscale = []
    colorscale.append([0, f'rgb(230, 237, 230)'])
    colorscale.append([0.01, f'rgb(230, 237, 230)'])
    colorscale.append([0.01, f'rgb(150, 255, 255)'])
    colorscale.append([0.05, f'rgb(150, 255, 255)'])
    colorscale.append([0.05, f'rgb(0, 189, 255)'])
    colorscale.append([0.1, f'rgb(0, 189, 255)'])
    colorscale.append([0.1, f'rgb(0, 50, 255)'])
    colorscale.append([0.2, f'rgb(0, 50, 255)'])
    colorscale.append([0.2, f'rgb(255, 150, 0)'])
    colorscale.append([0.3, f'rgb(255, 150, 0)'])
    colorscale.append([0.3, f'rgb(255, 230, 0)'])
    colorscale.append([0.5, f'rgb(255, 230, 0)'])
    colorscale.append([0.5, f'rgb(255, 0, 0)'])
    colorscale.append([0.8, f'rgb(255, 0, 0)'])
    colorscale.append([0.8, f'rgb(141, 0, 22)'])
    colorscale.append([1.0, f'rgb(141, 0, 22)'])
    return colorscale

@app.callback(Output('precipitation', 'figure'),
            [Input('interval-component', 'n_intervals')])
def plot_precip(n):
    global recent_data
    df, recent_data = get_dataframe(recent_data, load=False)
    figure = {
        'data':[
            go.Scattermapbox(
            lat = df['lat'],
            lon = df['lon'],
            mode = 'markers',
            marker = dict(
                size=10,
                opacity=1,
                showscale=True,
                color=df['precip'],    <--色の数値を与える
                cmin=0,    <--色の最小値
                cmax=100,    <--色の最大値
                colorscale=get_colorscale(),    <--カラースケール指定
                colorbar=dict(
                    tickmode="array",
                    tickvals=[0, 5, 10, 20, 30, 50, 80, 100],  <--表示する数値を指定
                    title=dict(text='[mm/h]')  <--カラーバータイトルを指定
                )
            ),
            ~~~~~~~~~~~~~~~~~~~~~~~
        ],
        'layout':
            ~~~~~~~~~~~~~~~~~~~~~~~
    }
    return figure
選択リセットボタンを設置

複数選択した点を一括で外すボタンを作成.
押すと(id=precipitation)のselectedDataを初期化することで実現した.
ここを参照
How to implement reset button to clear output - Dash - Plotly Community Forum

app.layout = html.Div(
    children=[
        ~~~~~~~~~~~~~~~~~~~~~~
        html.Div(
            [dcc.Graph(id ='precipitation', selectedData={'points': [{'customdata': 44132}]})],
            style={'width':'80%', 'display': 'inline-block'}
        ),
        html.Div(
            [html.Button('select reset',id='reset_button', n_clicks=0]
        ),
        ~~~~~~~~~~~~~~~~~~~~~~
    ],
    ~~~~~~~~~~~~~~~~~~~~~~
)

@app.callback(Output('precipitation', 'selectedData'),
            [Input('reset_button','n_clicks')])    <---クリックされたら
def update(reset):
    return {'points': [{'customdata': 44132}]}    <--selectedDataにこれを返す
定期的にデータを取得

dash_core_components.Intervalでインターバル指定ができる.
ここを参考に
Dash User Guide and Documentation - Dash by Plotly

app.layout = html.Div(
    children=[
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        dcc.Interval(
            id='interval-component',
            interval=300*1000, # in milliseconds
            n_intervals=0    <--interval間隔でこの値がインクリメントされる
        ),
        html.Div(id="loader")
    ],
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
)

@app.callback(Output('loader', 'children'),
            [Input('interval-component', 'n_intervals')])    <--n_intervalが変更されたら実行
def print_time(n):
    get_dataframe(recent_data=None, load=True)

終に

今回は気象庁データをDashでプロットするアプリケーションを作成した.
Dashの扱いに馴染んできたので次はTwitterデータかbitcoin販売所データで何か可視化をしてみたいと考えている.