Pythonistaでリアルタイムグラフ表示（pyplot vs ui.WebView+Google Charts）

#概要

Pythonista でリアルタイムグラフ表示を行おうとして、最初 pyplot を試したところ結構遅かったので ui.WebView 上で Google Charts を使ったグラフ表示と速度比較を行ってみました。

##環境

- iPad mini 4 iOS 12.1.1
- [Pythonista3 v3.2](http://omz-software.com/pythonista/)

##実行結果

表示対象のデータは Raspberry Pi に接続した BME280 の温度と湿度の値で、1秒間隔で取得して最新の300件(５分間)のグラフ表示の更新にどれだけかかるかを計測しています。

上側の pyplot による表示は更新に 0.521 秒
下側の ui.WebView + Google Charts による表示は更新に 0.233 秒

元々やりたかったのは 0.05 秒間隔で送られてくるデータの表示で、グラフの更新間隔は遅くするとしても、更新中に送られてくるデータを取りこぼしてしまうのが悩みのタネです。

![IMG_0300.png](https://qiita-image-store.s3.amazonaws.com/0/133064/d1a00dce-ac0e-d803-2981-077a0a6b552b.png)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Pythonistaでリアルタイムグラフ表示。もっと更新が速い方式はないかな。<br>「Pythonistaでリアルタイムグラフ表示（pyplot vs ui.WebView+Google Charts）」<a href="https://t.co/egYyRcYdD8">https://t.co/egYyRcYdD8</a><a href="https://twitter.com/hashtag/Pythonista?src=hash&amp;ref_src=twsrc%5Etfw">#Pythonista</a> <a href="https://t.co/Fqu91np7wV">pic.twitter.com/Fqu91np7wV</a></p>&mdash; 稲澤祐一 (@inasawa) <a href="https://twitter.com/inasawa/status/1079389945913958400?ref_src=twsrc%5Etfw">2018年12月30日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

##Pythonista プログラム

```python:realtime_charts.py
from matplotlib import pyplot as plt
from matplotlib import ticker
import io
import requests
import time
import ui

HTML = '''
<html>
    <head>
        <title>温度・湿度 - Google Charts</title>
        <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
        <script type="text/javascript">
            google.charts.load('current', {'packages':['line', 'corechart']});
            google.charts.setOnLoadCallback(setupChart);
            
            var optionLineTH = {
                    title: '温度＆湿度の変化',
                    width:  1024,
                    height: 350,
                    series: {
                        0: {targetAxisIndex: 0,
                            color: 'red'},
                        1: {targetAxisIndex: 1,
                            color: 'blue'}
                    },
                    vAxes: {
                        0: {title: '温度',
                            format: '#.#℃'},
                        1: {title: '湿度',
                            format: '#.#%'}
                    },
                    hAxis: {
                        format: '#.#'
                    },
                    vAxis: {
                        titleTextStyle: {italic: false}
                    //  viewWindow: {
                    //    max: 30
                    //  }
                    }
                };

            var dataLineTH;
            var chartLineTH;

            function setupChart() {
                dataLineTH = new google.visualization.DataTable();
                dataLineTH.addColumn('number', '秒');
                dataLineTH.addColumn('number', '温度');
                dataLineTH.addColumn('number', '湿度');
                
                //chartLineTH = new google.charts.Line(document.getElementById('line_chart_temp_hum'));
                chartLineTH = new google.visualization.LineChart(document.getElementById('line_chart_temp_hum'));
            }
            function updateChart(sec, temperature, humidity) {
                dataLineTH.addRow([sec, temperature, humidity / 100])
                if (dataLineTH.getNumberOfRows() > 300)
                    dataLineTH.removeRow(0)
                
                //chartLineTH.draw(dataLineTH, google.charts.Line.convertOptions(optionLineTH));
                chartLineTH.draw(dataLineTH, optionLineTH);
            }
        </script>
    </head>
    <body>
        <div>
            <div id="line_chart_temp_hum"></div>
        </div>
    </body>
</html>
'''

class MyView(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def will_close(self):
        global action_loop
        action_loop = False


frame_w = 1024
frame_h =  700
main_view = MyView(frame=(0, 0, frame_w, frame_h))
main_view.background_color = 'white'

plt_view = ui.ImageView(frame=(0, 0, frame_w, int(frame_h / 2)))
plt_info_view = ui.Label(frame=(0, 0, 150, 30))
main_view.add_subview(plt_view)
main_view.add_subview(plt_info_view)

web_view = ui.WebView(frame=(0, int(frame_h / 2), frame_w, int(frame_h / 2)))
web_info_view = ui.Label(frame=(0, int(frame_h / 2), 150, 30))
main_view.add_subview(web_view)
main_view.add_subview(web_info_view)

main_view.present()

plt.rcParams['axes.labelsize'] = 8
plt.rcParams['xtick.labelsize'] = 8
plt.rcParams['ytick.labelsize'] = 8
fig = plt.figure(figsize=(9, 3))
fig.suptitle('Temperature & Humidity')

ax1 = fig.add_subplot(111)
ax1.grid(True)
ax1.set_ylabel('Temp')
ax2 = ax1.twinx()
ax2.grid(True)
ax2.set_ylabel('Hum')

e_time = []
data_t = []
data_h = []
line_t, = ax1.plot(e_time, data_t, label='Temp', color='red')
line_h, = ax2.plot(e_time, data_h, label='Hum', color='blue')
ax1.legend(bbox_to_anchor=(0.11, 1.12), frameon=False)
ax2.legend(bbox_to_anchor=(0.21, 1.12), frameon=False)
ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1fC'))
ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1f%%'))

web_view.load_html(HTML)

s = requests.Session()
action_loop = True
start_time = time.time()
while action_loop:
    event_time = time.time()

    # Web Server に対する GET で以下のようなデータが返るようにしてあります。
    # {"timestamp": "Sun Dec 30 23:27:33 2018", "temperature": 20.00649283626699, "pressure": 1018.4915113213154, "humidity": 46.925826193156354}
    r = s.get('Web Server URL')
    e = r.json()

    # pyplot によるグラフ表示
    plot_start = time.time()
    e_time.append(event_time - start_time)
    if len(e_time) > 300:
        e_time.pop(0)
    data_t.append(e['temperature'])
    if len(data_t) > 300:
        data_t.pop(0)
    data_h.append(e['humidity'])
    if len(data_h) > 300:
        data_h.pop(0)
    line_t.set_data(e_time, data_t)
    line_h.set_data(e_time, data_h)
    ax1.set_xlim(e_time[0], e_time[-1]+0.1)
    ax1.set_ylim(min(data_t), max(data_t)+0.1)
    ax2.set_ylim(min(data_h), max(data_h)+0.1)
    with io.BytesIO() as bIO:
        plt.savefig(bIO)
        plt_view.image = ui.Image.from_data(bIO.getvalue())
        del bIO
    plt_info_view.text = ' {:6.3f} sec/upd'.format(time.time() - plot_start)

    # Google Charts によるグラフ表示
    plot_start = time.time()
    web_view.evaluate_javascript(
        'updateChart({},{},{});'.format(
            event_time - start_time,
            e['temperature'],
            e['humidity']))
    web_info_view.text = ' {:6.3f} sec/upd'.format(time.time() - plot_start)

    # 1秒間隔での更新となるように sleep
    time.sleep(max(0, 1 - (time.time() - event_time)))
```