Dashの細かい使いどころ

[Dash](https://dash.plotly.com/)はPythonでデータアプリケーションを作成するためのフレームワークです。フロントエンドに関する知識はほぼ不要で、Pythonのみでアプリケーションを作成することができるので、特にPython以外の言語に馴染の薄いデータサイエンティストが、サクッとプロトタイプを作成するのに重宝します。類似のフレームワークに[streamlit](https://streamlit.io/)がありますが、Dashの方がコード量は多くなるもののカスタマイズ性に優れ、より見栄えの良いアプリケーションを作成することができます。

今回はDashでアプリケーションを作成してみて学んだ細かい使いどころを備忘録としてまとめました。コールバックなどの基本的な使い方は他に優れた記事がたくさんにあるのでそちらをご覧ください。

## レイアウト関連

### マルチページ
https://dash.plotly.com/urls

Dashはマルチページに対応しています。作り方はとても簡単で以下の設定を行うだけです。

- appの宣言時に`use_pages=True`と設定する
- 各ページに対応するスクリプトを`pages/`に配置する
    - `dash.register_page(__name__)`でページとして登録する
    - `layout`に各ページのレイアウトを設定する

```python
# app.py
app = Dash(__name__, use_pages=True)  # use_pages=Trueと設定
```

```python
# pages/page1.py
dash.register_page(__name__)  # ページとして登録
layout = html.H1("Hello")  # 変数layoutにレイアウトを設定
```

### CSS
https://dash.plotly.com/external-resources

各コンポーネントのスタイルは`style`属性を通して個別にカスタマイズできますが、全体的なスタイルを整えたいときはCSSを利用するのが便利です。Dashでは`assets/`にcssファイルを配置すると自動的に読み込まれ、スタイルに反映されます。

### Dash Bootstrap Components

Dash Bootstrap Components (dbc) はBootstrapフレームワークを利用して、優れたデザインのコンポーネントを提供してくれる3rd-party製ツールです。dbcを利用するだけで、簡単に見栄えの良いアプリケーションに仕立て上げることができます。

#### テーマ

https://dash-bootstrap-components.opensource.faculty.ai/docs/themes/

dbcでは様々なデザインのスタイルシートが提供されており、これを読み込ませることで全体のテーマを自分好みにカスタマイズできます。利用できるテーマの一覧は上記リンクのAvailable themesから確認できます。

```python
app = Dash(__name__, external_stylesheets=[dbc.themes.FLATLY])  # スタイルシートを読み込む
```

#### レイアウト

https://dash-bootstrap-components.opensource.faculty.ai/docs/components/layout/

dbcを利用することで、各コンポーネントを美しく整列することができます。`dbc.Container`の下に`dbc.Row`、さらにその下に`dbc.Col`という階層で記述します。`dbc.Container`の引数として`fluid=True`を与えることで、不要な余白を除去して表示します。

```python
layout = dbc.Container(
    [
        dbc.Row(
            [
                dbc.Col(html.Div("Hello"), width=2),
                dbc.Col(html.Div("World"), width=10),
            ],
        ),
    ],
    fluid=True,
)
```

#### コンポーネント

https://dash-bootstrap-components.opensource.faculty.ai/docs/components/

その他、dbcではさまざまなコンポーネントが用意されており、これらを組み合わせることでよりリッチなレイアウトを作成できます。アクションアイテムはDash組み込みのコンポーネント（Dash Core Component: dbc）と同様にコールバック利用も可能です。

例えば、以下のようなコンポーネントが用意されています。

[**ナビゲーションバー**](https://dash-bootstrap-components.opensource.faculty.ai/docs/components/navbar/)

![dbc_navbar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988692/7ee35cd2-ed8f-9a97-f2d3-e284a295a040.png)

[**フォーム**](https://dash-bootstrap-components.opensource.faculty.ai/docs/components/form/)

![dbc_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988692/907d6593-b4ae-db4b-c794-ca3ae56f54ee.png)


### グラフの状態の保持

https://community.plotly.com/t/preserving-ui-state-like-zoom-in-dcc-graph-with-uirevision-with-dash/15793?page=4

デフォルトの振る舞いでは、ユーザーがグラフの表示範囲を変更したのち、適当なタイミングでコールバックが呼ばれると、表示範囲がデフォルトの状態にリセットされてしまいます。これは、場合によってはユーザー体験を損ねる原因になります（例えば、地図上である範囲にフォーカスしてインタラクティブな操作を行いたい場合など）。

figureの`layout`属性で`uirevision=True`と設定すればコールバックが呼ばれても状態を保持することができます（詳細は上記リンク参照）。

## コールバック関連

### コールバックへの複数のインプット

https://dash.plotly.com/basic-callbacks

コールバックに複数の`Input`を与えることも可能です。これにより、`Input`に紐づけられた複数のコンポーネントのいずれかが操作されたタイミングで発火し、`Output`に紐づけられたコンポーネントの状態が変化します。

```python
@callback(
    Output(component_id="figure", component_property="figure"),
    Input(component_id="dropdown", component_property="value"),
    Input(component_id="checklist", component_property="value"),
    State(component_id="state", component_property="data"),
)
def update_figure(files: List[str], checklist: List[str], state: str):
    ....
```

### コールバック間のデータのやり取り

https://dash.plotly.com/sharing-data-between-callbacks

Dashでは、複数のコールバック間でデータのやり取りをするための機能として`dcc.Store`が提供されています。例えば、アップロードしたデータを重い処理にかけたのち、その結果を複数の図で表示させたい場合、コールバックごとに行うのではなく、`dcc.Store`を介してデータを共有することで効率的に実行することができます。

```python
app.layout = html.Div([
    dcc.Graph(id='graph'),
    dcc.Dropdown(id='dropdown'),
    dcc.Store(id='intermediate-value')
])

@callback(
    Output('intermediate-value', 'data'),
    Input('dropdown', 'value')
)
def convert_data(value):
     df = processing(value)
     return df.to_json(date_format='iso', orient='split')

@callback(
    Output('graph', 'figure'),
    Input('intermediate-value', 'data')
)
def update_graph(data):
    df = pd.read_json(data, orient='split')
    figure = create_figure(df)
    return figure
```

ただし、`dcc.Store`はデータをユーザーのブラウザセッション上に保存するため、重いデータを保存するのには向いていません（ネットワークコストが高くつく）。また、データをやり取りするためにjson形式にフォーマットする必要があり、やや手間がかかります。

そのため、場合によってはサーバーのファイルシステムなどに処理結果を保存し、`dcc.Store`には処理が完了したことを知らせるシグナルとしての役割を任せる、などの使い方の方が望ましいかもしれません（詳細は上記のリンクを参照ください）。

## その他

### ファイルのアップロード

Dash組み込みのアップローダーとしては`dcc.uploader`が提供されていますが、ファイルサイズに制限がある、ファイルの種類にかかわらずbase64形式の文字列でエンコーディングされるためパースが必要、などの点でややクセがあります。そこで、代替として3rd-party製の[dash-uploader](https://github.com/fohrloop/dash-uploader/tree/dev)がオススメです。dash-uploaderの方が作りがシンプルで、かつ

- プログレスバーの表示機能
- 指定したディレクトリ（`/tmp/`など）へのファイルの保存機能

などの機能をデフォルトでサポートしており使い勝手がよいです。

```python
import dash_uploader as du

app = dash.Dash(__name__)
du.configure_upload(app, "/tmp/uploads")  # ファイルをアップロードするディレクトリを設定

app.layout = html.Div([
    du.Upload(),  # コンポーネントを設定
])
```

:::note warn
dash-uploaderは依存関係によってはマルチページだとうまく動作しない問題があるようです。対処法は下のリンクを参照ください。
:::

https://community.plotly.com/t/how-to-configure-du-configure-upload-in-dash-uploader-for-dash-multi-page-apps/75653

</aside>

### 認証

https://dash.plotly.com/authentication

Dashでは[dash-auth](https://github.com/plotly/dash-auth)という簡易的な認証の仕組みもサポートされています。

```python
VALID_USERNAME_PASSWORD_PAIRS = {'hello': 'world'}  # ユーザー名とパスワードの組

app = Dash(__name__)
auth = dash_auth.BasicAuth(app, VALID_USERNAME_PASSWORD_PAIRS)  # 認証を設定
```

### デプロイ

https://dash.plotly.com/deployment

デプロイ時には、gunicornをインストールしエントリーポイントとして`gunicorn app:server`を実行します。

```python
# app.py
app = Dash(__name__)
server = app.server  # これが必要
app.layout = html.H1('Hello World')

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

```python
# Procfile
web: gunicorn app:server
```