可視化ツールDashのチュートリアル - Part 2: 対話形式 -

# はじめに

[前回のチュートリアル](https://qiita.com/pinocookie/items/ddc46adcbd6332511b92)で簡単なデータの可視化手法を学びました。
しかし、前回までの内容ならば他のライブラリでも可能です。そこで今回は、図に対して操作を行なえる対話形式のアプリを作成することを目指します。

[公式HPのチュートリアル](https://dash.plot.ly/getting-started-part-2)

なお今回はPythonのデコレータについての知識が必要になるので、以下を参照すると理解しやすいでしょう。

- [Pythonのデコレータを理解するための12Step](https://qiita.com/_rdtr/items/d3bc1a8d4b7eb375c368)
- [Pythonのデコレータについて](https://qiita.com/mtb_beta/items/d257519b018b8cd0cc2e)
- [Python デコレータ再入門　 ~デコレータは種類別に覚えよう~](https://qiita.com/macinjoke/items/1be6cf0f1f238b5ba01b)

---

# 対話形式の図を作成する

## 入力した値を反映させる

まずは以下のコードと結果をご覧ください。

```python
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

app = dash.Dash()

app.layout = html.Div([
    dcc.Input(
        id='my-id', 
        value='initial value', 
        type='text'
    ),
    html.Div(id='my-div')
])


@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input(component_id='my-id', component_property='value')]
)
def update_output_div(input_value):
    return 'You have entered "{}"'.format(input_value)


if __name__ == '__main__':
    app.run_server()
```

![01.gif](https://qiita-image-store.s3.amazonaws.com/0/194085/9360dd21-7e20-90f5-181d-5b1e0b37040a.gif)

上の図を見てわかる通り、`Input`ボックスの中に入力した値が、ボックスの直下にあるDivタグ内の値に反映されています。

この対話形式の描画を可能にしたのは`app.callback`デコレータで追加した部分です。

```python
@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input(component_id='my-id', component_property='value')]
)
def update_output_div(input_value):
    return 'You have entered "{}"'.format(input_value)
```

Dashでは入力と出力は、単に特定のコンポーネントのプロパティにすぎません。
上の例では、入力は`my-id`のIDをもつコンポーネントの`value`プロパティであり、出力は`my-div`のIDをもつコンポーネントの`children`プロパティになります。

デコレートされた関数は、入力プロパティが変更されるたびに自動的に呼び出されます。
Dashでは、入力プロパティに新しく入力された値を入力引数として関数に提供し、関数の返り値を出力コンポーネントの出力プロパティに更新します。

なお`my-div`では初期設定で`children`に何も設定していませんが、これはDashが起動すると、出力コンポーネントの初期値を入力コンポーネントから呼び出すため、設定していたとしても上書きされるからです。

## スライダーを使って図を操作する

```python
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go

df = pd.read_csv(
    'https://raw.githubusercontent.com/plotly/'
    'datasets/master/gapminderDataFiveYear.csv')
```

このデータフレームは以下のようになります。

![02.PNG](https://qiita-image-store.s3.amazonaws.com/0/194085/a0451686-4c0d-460c-fe6c-f5711a5a05c1.png)

アプリの外観を決定します。

```python
app = dash.Dash()

app.layout = html.Div([
    # 最初グラフには何も設定しない。（上書きされるため）
    dcc.Graph(id='graph-with-slider'),
    dcc.Slider(
        # 入力コンポーネントとしてIDを設定
        id='year-slider',
        # スライダーの最小値・最大値を設定
        min=df['year'].min(),
        max=df['year'].max(),
        # 今回の入力値(初期値)
        value=df['year'].min(),
        step=None,
        # スライダーのラベルを設定
        marks={str(year): str(year) for year in df['year'].unique()}
    )
])
```

次に入力値と出力値を設定し、年ごとのデータを出力します。

```python
@app.callback(
    dash.dependencies.Output('graph-with-slider', 'figure'),
    [dash.dependencies.Input('year-slider', 'value')])
def update_figure(selected_year):
    # 入力値には`value`、つまり年のデータが送られる。
    # 年ごとのデータを抽出する。
    filtered_df = df[df.year == selected_year]
    # figureを格納する空リストを作成
    traces = []
    # 大陸別にグループ分けを行う
    for i in filtered_df.continent.unique():
        df_by_continent = filtered_df[filtered_df['continent'] == i]
        traces.append(go.Scatter(
            x=df_by_continent['gdpPercap'],
            y=df_by_continent['lifeExp'],
            text=df_by_continent['country'],
            mode='markers',
            opacity=0.7,
            marker={
                'size': 15,
                'line': {'width': 0.5, 'color': 'white'}
            },
            name=i
        ))

    return {
        'data': traces,
        'layout': go.Layout(
            xaxis={'type': 'log', 'title': 'GDP Per Capita'},
            yaxis={'title': 'Life Expectancy', 'range': [20, 90]},
            margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
            legend={'x': 0, 'y': 1},
            hovermode='closest'
        )
    }


if __name__ == '__main__':
    app.run_server()
```

![03.gif](https://qiita-image-store.s3.amazonaws.com/0/194085/c3d169a6-4b5d-05d6-b436-aa54681b922c.gif)


今回は`Slider`の`value`プロパティが入力値として使用されており、出力値は`Graph`の`figure`プロパティです。

データをロードする作業`df = pd.read_csv(...)`はコストのかかる作業ですが、以上のようにコードを実行すると、データのロードは最初に行われるだけであり、ユーザーが対話式に操作する際はすでにデータはメモリ内に格納されています。

注意点は、**`callback`がスコープ外の変数に手を加えない**ようにしておくことです。これを守らなかった場合、ユーザーがデータそのものに手を加えることが可能になってしまいます。

## 複数の入力値をとる

今回は、`Dropdown`から2つ、`RadioItems`から2つ、`Slider`から1つの合計5つの入力値を受けてグラフを操作することを目指します。

```python
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go

app = dash.Dash()

df = pd.read_csv(
    'https://gist.githubusercontent.com/chriddyp/'
    'cb5392c35661370d95f300086accea51/raw/'
    '8e0768211f6b747c0db42a9ce9a0937dafcbd8b2/'
    'indicators.csv')

available_indicators = df['Indicator Name'].unique()
```

このデータフレームは以下のようになります。

![04.PNG](https://qiita-image-store.s3.amazonaws.com/0/194085/834c64f1-41e8-f6a1-4f64-0af3ea97569b.png)

次にアプリの外観を決定します。

```python
app.layout = html.Div([
    html.Div([

        html.Div([
            dcc.Dropdown(
                id='xaxis-column',
                # available_indicator変数を選択肢として設定
                options=[{'label': i, 'value': i} for i in available_indicators],
                # 初期値
                value='Fertility rate, total (births per woman)'
            ),
            dcc.RadioItems(
                id='xaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                # 初期値、ここでは1つだけ選択する
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ],
        # 上の2つの要素をひとまとめにstyleを設定する
        style={'width': '48%', 'display': 'inline-block'}),

        html.Div([
            dcc.Dropdown(
                id='yaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                # 初期値
                value='Life expectancy at birth, total (years)'
            ),
            dcc.RadioItems(
                id='yaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                # 初期値
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        # 上の2つの要素をひとまとめにstyleを設定する
        ],style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
    ]),

    # ここに出力する図を挿入する
    dcc.Graph(id='indicator-graphic'),

    dcc.Slider(
        id='year--slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        # 初期値
        value=df['Year'].max(),
        step=None,
        marks={str(year): str(year) for year in df['Year'].unique()}
    )
])
```

次に複数の入力値と出力値を設定し、図に反映させましょう。

```python
@app.callback(
    dash.dependencies.Output('indicator-graphic', 'figure'),
    [dash.dependencies.Input('xaxis-column', 'value'),
     dash.dependencies.Input('yaxis-column', 'value'),
     dash.dependencies.Input('xaxis-type', 'value'),
     dash.dependencies.Input('yaxis-type', 'value'),
     dash.dependencies.Input('year--slider', 'value')])
def update_graph(xaxis_column_name, yaxis_column_name,
                 xaxis_type, yaxis_type,
                 year_value):
    dff = df[df['Year'] == year_value]

    return {
        'data': [go.Scatter(
            x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
            y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
            text=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'],
            mode='markers',
            marker={
                'size': 15,
                'opacity': 0.5,
                'line': {'width': 0.5, 'color': 'white'}
            }
        )],
        'layout': go.Layout(
            xaxis={
                'title': xaxis_column_name,
                'type': 'linear' if xaxis_type == 'Linear' else 'log'
            },
            yaxis={
                'title': yaxis_column_name,
                'type': 'linear' if yaxis_type == 'Linear' else 'log'
            },
            margin={'l': 40, 'b': 40, 't': 10, 'r': 0},
            hovermode='closest'
        )
    }


if __name__ == '__main__':
    app.run_server()
```

![05.gif](https://qiita-image-store.s3.amazonaws.com/0/194085/bb6e28d0-007c-16ce-05a3-4eea8fd801da.gif)

## 複数の出力値をとる

Dashの`callback`関数では出力は1つしか取れません。そこで複数の出力値を設定する場合は、複数の関数を定義します。

```python
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
```

次にアプリの外観を決定します。

```python
app = dash.Dash()

app.layout = html.Div([
    dcc.RadioItems(
        id="dropdown-a",
        options=[{"label": i, "value": i} for i in ["Canada", "USA", Mexico]],
        value="Canada",
    ),
    html.Div(id="output-a"),

    dcc.RadioItems(
        id="dropdown-b",
        options=[{"label": i, "value": i} for i in ["MTL", "NYC", "SF"]],
        value="MTL"
    ),
    html.Div(id="output-b")
])
```

次に複数の出力値を設定するために複数の関数を定義する。

```python
# 1つ目の出力
@app.callback(
    Output("output-a", "children"),
    [Input("dropdown-b", "value")]
)
def callback_a(dropdown_value):
    return "You have selected {}".format(dropdown_value)

# 2つ目の出力
@app.callback(
    Output("output-b", "children"),
    [Input("dropdown-b", "value")]
)
def callback_b(dropdown_input):
    return "You have selected {}".format(dropdown_input)


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

![06.gif](https://qiita-image-store.s3.amazonaws.com/0/194085/e68dc06b-72bc-52c4-743d-852d71115af3.gif)


callback関数からの出力値は、他のcallback関数への入力値として扱うこともできます。

これは1つの入力値が、他の入力値へ影響を与えるような動的なUIを作成する場合に使われています。

```python
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
```

次にアプリの外観を決定します。

```python
app = dash.Dash()

all_options = {
    "America": ["New York City", "San Francisco", "Cincinnati"],
    "Canada": ["Montreal", "Toronto", "Ottawa"]
}

app.layout = html.Div([
    dcc.RadioItems(
        id="countries-dropdown",
        options=[{"label":i, "value":i} for i in all_options.key()],
        value="America"
    ),
    html.Hr(),

    dcc.RadioItems(id="cities-dropdown"),

    html.Hr(),

    Html.Div(id="display-selected-values)
])
```

次に入力値と出力値を設定する。

```python
@app.callback(
    Output("cities-dropdown", "value"),
    [Input("countries-dropdown", "value")]
)
def set_cities_options(selected_country):
    return [{"label":i, "value":i} for i in all_options[selected_country]]

@app.callback(
    Output("cities-dropdown", "value"),
    [Input("cities-dropdown", "options")]
)
def set_cities_value(selected_options):
    return available_options[0]["value"]

@app.callback(
    Output("display-selected-values", "children"),
    [Input("countries-dropdown", "value"),
    Input("cities-dropdown", "value")]
)
def set_display_children(selected_country, selected_city):
    return "{} is a city in {}".format(selected_city, selected_country)

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

![07.gif](https://qiita-image-store.s3.amazonaws.com/0/194085/40153fc7-ad43-3f1a-8e40-fe406b770da8.gif)


最初のコールバックは、最初のRadioItemsコンポーネントで選択された値に基づいて、2番目のRadioItemsコンポーネントで使用可能なオプションを更新します。

2番目のコールバックは、optionsプロパティが変更されたときに初期値を設定します

options配列の最初の値に設定します。

最後のコールバックには、各コンポーネントの選択された値が表示されます。各国のRadioItemsコンポーネントの値を変更すると、最終コールバックを呼び出す前にcitiesコンポーネントの値が更新されるまでDashが待機します。これにより、コールバックが "USA"や "Montreal"のような一貫性のない状態で呼び出されるのを防ぎます。