DashでグラフをLive Updateする(ver0.37へ対応)

Live Updateしようとしたら、つまずいたので備忘のためのメモとして残すことにしました。

## この記事の概要

* Dash(plolty)で定期的にグラフを更新しようとしたら、エラーが出た
* 原因：'Event'が無くなっていた
* 対応：Interval componentをInputにする

## Dashとは
Python製のWebアプリケーションのフレームワークです。Plotlyを使用して簡単に可視化ができます。
詳細は、[公式][2]を見てください。

## 経緯
この[Plotly Dashで簡単に可視化ができるWebアプリを作る][1]という記事を参考に、グラフのLive Updateを試そうとしたところ、 
以下のエラーが出て、少し困りました。。
```ImportError: cannot import name 'Event'```

参考にした記事での該当部分は、以下の様なデコレータの箇所です。

```python
@app.callback(Output('live-update-text', 'children'),
              events=[Event('interval-component', 'interval')])
def update_graph_live():
```

### 原因
Google先生に聞くと、何やらDashのversion 0.37から'Event'が無くなっているそうです。
他にも困ってる人いました。
[How to fix ImportError: cannot import name 'Event' in Dash from plotly (python)?][3]
どうしたらえぇや？と調べてると、ちゃんと公式にやり方が書いてありました。

### 対応
対応方法は、公式の[Live Updating Components][4]通りで、EventをInputに書き換えます。
具体的には、上記の該当部分を以下の様に編集しました。

```python
@app.callback(Output('live-update-text', 'children'),
              [Input('interval-component', 'n_intervals')])
def update_graph_live(n):
```

参考記事の「システムモニタ」をver0.37で動作するように修正すると、以下の様になります。

```python
import jupyterlab_dash #今回はJupyterlabを使用
import dash
import dash_html_components as html
#from dash.dependencies import Input, Output, Event
from dash.dependencies import Input, Output #Eventはもういない。。
import dash_core_components as dcc
import datetime
import plotly
import numpy as np
import pandas as pd
import psutil

viewer = jupyterlab_dash.AppViewer()

colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

app = dash.Dash(__name__)
app.layout = html.Div(children=[
    html.Div([
        html.H4('システムモニタ'),
        html.Div(id='live-update-text'),
        dcc.Graph(id='live-update-graph'),
        html.H4('プロセスリスト'),
        html.Div(id='live-update-proc'),
        dcc.Interval(
            id='interval-component',
            interval=1*1000, # in milliseconds
            n_intervals=0 #公式に沿って追加
        )
    ])
],
    style={'backgroundColor': colors['background'], 'color': colors['text']}
)


class Context:
    def __init__(self):
        self.t = []
        self.cpu = []
        self.per_cpu = [[] for x in range(psutil.cpu_count())]
        self.mem = []

    @classmethod
    def append_data(cls, d1, d2):
        n = len(d1)
        if n > 100:
            del d1[0:n - 99]
        d1.append(d2)


context = Context()

# The `dcc.Interval` component emits an event called "interval"
# every `interval` number of milliseconds.
# Subscribe to this event with the `events` argument of `app.callback`


@app.callback(Output('live-update-text', 'children'),
              #events=[Event('interval-component', 'interval')])
              [Input('interval-component', 'n_intervals')])
#def update_metrics():
def update_metrics(n):
    now = datetime.datetime.now()
    hour, minute, second = now.hour, now.minute, now.second
    style = {'padding': '5px', 'fontSize': '16px'}
    return [
        html.Span('CPU: {}%'.format(context.cpu[-1]), style=style),
        html.Span('Memory: {}%'.format(context.mem[-1]), style=style)
    ]


# Multiple components can update everytime interval gets fired.
@app.callback(Output('live-update-graph', 'figure'),
              #events=[Event('interval-component', 'interval')])
              [Input('interval-component', 'n_intervals')])
#def update_graph_live():
def update_graph_live(n):
    # global context
    context.append_data(context.t, datetime.datetime.now())
    context.append_data(context.cpu, psutil.cpu_percent())
    for data, pct in zip(context.per_cpu, psutil.cpu_percent(percpu=True)):
        context.append_data(data, pct)
    context.append_data(context.mem, psutil.virtual_memory().percent)

    # Create the graph with subplots
    fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2)
    fig['layout']['margin'] = {
        'l': 30, 'r': 10, 'b': 30, 't': 10
    }
    fig['layout']['plot_bgcolor'] = colors['background']
    fig['layout']['paper_bgcolor'] = colors['background']
    fig['layout']['font'] = {'color': colors['text']}
    fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'}
    fig['layout']['yaxis1'].update(range=[0, 100])
    fig['layout']['yaxis2'].update(range=[0, 100])

    fig.append_trace({
        'x': context.t,
        'y': context.cpu,
        'name': 'cpu',
        'mode': 'lines',
        'type': 'scatter',
    }, 1, 1)
    for i, y in enumerate(context.per_cpu):
        fig.append_trace({
            'x': context.t,
            'y': y,
            'name': 'cpu {}'.format(i),
            'mode': 'lines',
            'type': 'scatter',
        }, 1, 1)
    fig.append_trace({
        'x': context.t,
        'y': context.mem,
        'name': 'memory',
        'mode': 'lines',
        'type': 'scatter',
        'fill': 'tonexty',
    }, 2, 1)

    return fig


def get_proc_df():
    def get_proc(proc):
        try:
            pinfo = proc
        except psutil.NoSuchProcess:
            pass
        return (pinfo.pid, pinfo.name(), pinfo.memory_percent(), pinfo.cpu_percent())

    data = [get_proc(proc) for proc in psutil.process_iter()]
    df = pd.DataFrame(data, columns=['pid', 'name', 'memory', 'cpu'])
    df['memory'] = df['memory'].map(lambda x: '{:.2f}%'.format(x))
    df['cpu'] = df['cpu'] / psutil.cpu_count()
    df['cpu'] = df['cpu'].map(lambda x: '{:.2f}%'.format(x))
    return df.sort_values('cpu', ascending=False)


@app.callback(Output('live-update-proc', 'children'),
              #events=[Event('interval-component', 'interval')])
              [Input('interval-component', 'n_intervals')])
#def generate_table():
def generate_table(n):
    df = get_proc_df()
    max_rows = 10
    return html.Table(
        # Header
        [html.Tr([html.Th(col) for col in df.columns])] +

        # Body
        [html.Tr([
            html.Td(df.iloc[i][col], style={'width': '8em'}) for col in df.columns
        ]) for i in range(min(len(df), max_rows))]
    )

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

viewer.show(app)
```




[1]: https://qiita.com/driller/items/49e3b6084ec034353dc1
[2]: https://dash.plot.ly/gallery
[3]: https://stackoverflow.com/questions/54807868/how-to-fix-importerror-cannot-import-name-event-in-dash-from-plotly-python
[4]: https://dash.plot.ly/live-updates