Edited at

Pythonの可視化ライブラリDashを使う 3 マウスホバーを活用する

前の記事ではCallbackの機能で、dash_core_componentsにあるツールを使って、表示する要素を変更しました。

これでグラフがかなり動的になります。もう一つ活用したいのがマウスアクションに反応して、グラフが動かせる機能です。

これは、プログラミングを40歳のちょっと手前からはじめた私にとって、かなりの衝撃でした。


マウスのホバーデータを表示する

というわけで、マウスホバーを利用して動かす方法を見てみましょう。

まずは、マウスホバーでどのようなデータが取られているのか見てみます。前回の記事の北海道のチャートのタイトルの下に、データを表示するようにします。

Image from Gyazo

付け足したのは2つ。html.H1()とコールバックです。H1はグラフ同様、IDだけを付けてます。その後のコールバックで、hoverDataのjsonを返すようになっています。

import dash  

import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import pandas as pd
import json

df = pd.read_csv('./data/longform.csv', index_col=0)
dfhokkaido = df[df['area']=='北海道']

app = dash.Dash(__name__)

app.layout = html.Div(children=[
html.Div(
html.H1('北海道のGDP、人口、一人あたりGDPの推移',
style = {'textAlign': 'center'})
),
# 付け足し①
html.Div(
html.H1(id='add-hover-data')
),
dcc.RadioItems(
id = 'dropdown-for-hokkaido',
options = [{'label': i, 'value': i} for i in dfhokkaido.item.unique()],
value = 'GDP'
),
dcc.Graph(
id="hokkaidoGraph",
)
])

@app.callback(
dash.dependencies.Output('hokkaidoGraph', 'figure'),
[dash.dependencies.Input('dropdown-for-hokkaido', 'value')]
)
def update_graph(factor):
dff = dfhokkaido[dfhokkaido['item'] == factor]

return {
'data': [go.Scatter(
x = dff['year'],
y = dff['value']
)]
}

# 付けたし②
@app.callback(
dash.dependencies.Output('add-hover-data', 'children'),
[dash.dependencies.Input('hokkaidoGraph', 'hoverData')]
)
def return_hoverdata(hoverData):
return json.dumps(hoverData)

if __name__ == '__main__':
app.run_server(debug=True)

こんな簡単に、マウスの動きのデータが取得できるようになります。


欲しいデータを選択して表示する

次に欲しいデータを選択して表示してみましょう。

先程の例だと、xに年度が表示され、yにそのデータが表示さていました。なので、タイトルの下にホバーしている年度とデータを表示するようにします。

真ん中に表示させたいのとQiita色(ライムグリーンはちょっと違う感じがありますが・・)をまた使いたいと思うので、styleを付け加え、コールバックの部分でデータを選択して、それが文字列で返されるようにします。

Try、Exceptの部分はなくても動いていましたが、Nonetypeのエラーが吐かれていたので、黙らすためにやりました。なにか良い処理方法がございましたら、教えていただけますと幸いです。

Image from Gyazo

import dash  

import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import pandas as pd
import json

df = pd.read_csv('./data/longform.csv', index_col=0)
dfhokkaido = df[df['area']=='北海道']

app = dash.Dash(__name__)

app.layout = html.Div(children=[
html.Div(
html.H1('北海道のGDP、人口、一人あたりGDPの推移',
style = {'textAlign': 'center'})
),
html.Div(
html.H1(id='add-hover-data',
# 付け足し③
style={'textAlign': 'center',
'color': 'limegreen'})
),
dcc.RadioItems(
id = 'dropdown-for-hokkaido',
options = [{'label': i, 'value': i} for i in dfhokkaido.item.unique()],
value = 'GDP'
),
dcc.Graph(
id="hokkaidoGraph",
)
])

@app.callback(
dash.dependencies.Output('hokkaidoGraph', 'figure'),
[dash.dependencies.Input('dropdown-for-hokkaido', 'value')]
)
def update_graph(factor):
dff = dfhokkaido[dfhokkaido['item'] == factor]

return {
'data': [go.Scatter(
x = dff['year'],
y = dff['value']
)]
}

@app.callback(
dash.dependencies.Output('add-hover-data', 'children'),
[dash.dependencies.Input('hokkaidoGraph', 'hoverData')]
)
def return_hoverdata(hoverData):
# 付けたし④
try:
showData = '{}年: {}'.format(hoverData['points'][0]['x'], hoverData['points'][0]['y'])
return showData
except:
pass

if __name__ == '__main__':
app.run_server(debug=True)


もう少し複雑なデータでマウスホバーを扱う

上のようなデータでは「マウスのホバーのデータ出して何が嬉しいねん、あほか」って感じで、いまいち有り難みが分かりませんが、これがもう少し大きなデータだと効力を発揮してくれます。

さて、これまでは北海道のデータしか使っていませんでしたが、ここでは全部のデータを使います。それでどんなものを作ったかというと、下のようなものです。

利用データは下のリンク先にあります。

https://github.com/mazarimono/keizai-dash/blob/master/data/longform.csv

Image from Gyazo

ここでは、1955年から2014年までの都道府県別のGDP,一人あたりGDP、人口の3つのデータを使って、左側にスキャッターチャートを、右側に3つの各要素のヒストリカルを見るチャートを作っています。

左の大きなチャートでは、すべての地域のある年の3つのデータがプロットされています。x軸が一人あたりのGDP、縦軸がGDP,円の大きさが人口の大きさを示しています。

そしてそれが下のスライダーで動き、表示できる年度を変えられるようになっています。

右側にはマウスのホバーで得られた都道府県名から、選択された都道府県の3つのデータのヒストリカルが見られるようになっています。

このようにデータを見ることにより、全体的な流れとともに、個別の都道府県のヒストリカルデータが見られるようになります。

このようにデータを見ると、これまで見えてなかったような情報がデータから得られることは間違えないでしょう。これまでのツールでは平面でしか情報が見られていませんでしたが、このようなツールを作ることにより、データを立体的に見られるようになると思います。

これだけ動かすということは、凄いコードの量になるのでは、、、って思われるかもしれませんが、私の無駄だらけのコードでも130行に満たないくらいです。

Dash凄い!!!!


まとめ

これが一番すごいなぁと思っているところなのですが、少しでも伝わりましたでしょうか?次の記事では、地図データを扱ってみることと、ウェブに上げるのがどれほど簡単かというのをやってみたいと思います。

herokuに上げるのですが、これは今ひとつherokuが凄いのか、Dashが凄いのか、ちょっと私には判断が付きかねるところでございます。

参考までにリンク先に以前書いた記事があります。

https://www.mazarimono.net/entry/2018/12/07/heroku

import pandas as pd 

import numpy as np
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import json

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

df = pd.read_csv('./data/longform.csv', index_col = 0)

app.layout = html.Div([
html.Div([
html.H3('都道府県別人口とGDP,一人当たりGDP', style={
'textAlign': 'center'
}),
html.Div([
dcc.Graph(id = 'scatter-chart',
hoverData = {'points': [{'customdata': '大阪府'}]},
),
dcc.Slider(
id = 'slider-one',
min = df['year'].min(),
max = df['year'].max(),
marks = {i: '{}'.format(i) for i in range(int(df['year'].min()), int(df['year'].max())) if i % 2 == 1},
value = 1955, # 初期値の設定を忘れていたので付け加えました。
)
], style={
'display': 'inline-block',
'width': '60%',
}),
html.Div([
dcc.Graph(id='chart-one'),
dcc.Graph(id='chart-two'),
dcc.Graph(id='chart-three'),
],style={
'display': 'inline-block',
'width': '39%'
})
])
])

@app.callback(
dash.dependencies.Output('scatter-chart', 'figure'),
[dash.dependencies.Input('slider-one', 'value')]
)
def update_graph(selected_year):
dff = df[df['year'] == selected_year]
dffper = dff[dff['item']=='pergdp']
dffgdp = dff[dff['item']== 'GDP']
dffpop = dff[dff['item']== 'popu']

return {
'data': [go.Scatter(
x = dffper[dffper['area']==i]['value'],
y = dffgdp[dffgdp['area']==i]['value'],
mode = 'markers',
customdata = [i],
marker={
'size' : dffpop[dffpop['area']==i]['value']/100,
'color': dffpop[dffpop['area']==i]['value']/10000,
},
name=i,
)for i in dff.area.unique()],
'layout': {
'height': 900,
'xaxis': {
'type': 'log',
'title': '都道府県別一人当たりGDP(log scale)',
'range':[np.log(80), np.log(1200)]
},
'yaxis': {
'type':'log',
'title': '都道府県別GDP(log scale)',
'range':[np.log(80), np.log(8000)]
},
'hovermode': 'closest',
}
}

def create_smallChart(dff, area, name):
return {
'data':[go.Scatter(
x = dff['year'],
y = dff['value']
)],
'layout':{
'height': 300,
'title': '{}の{}データ'.format(area, name)
}
}

@app.callback(
dash.dependencies.Output('chart-one', 'figure'),
[(dash.dependencies.Input('scatter-chart', 'hoverData'))]
)
def createGDP(hoverdata):
areaName = hoverdata['points'][0]['customdata']
dff = df[df['area']==areaName]
dff = dff[dff['item'] == 'GDP']
return create_smallChart(dff, areaName, 'GDP')

@app.callback(
dash.dependencies.Output('chart-two', 'figure'),
[(dash.dependencies.Input('scatter-chart', 'hoverData'))]
)
def createPerGDP(hoverdata):
areaName = hoverdata['points'][0]['customdata']
dff = df[df['area']==areaName]
dff = dff[dff['item'] == 'pergdp']
return create_smallChart(dff, areaName, 'pergdp')

@app.callback(
dash.dependencies.Output('chart-three', 'figure'),
[(dash.dependencies.Input('scatter-chart', 'hoverData'))]
)
def createPopu(hoverdata):
areaName = hoverdata['points'][0]['customdata']
dff = df[df['area']==areaName]
dff = dff[dff['item'] == 'popu']
return create_smallChart(dff, areaName, 'popu')

if __name__=="__main__":
app.run_server(debug=True)