血圧と体重管理アプリを作ってみた

### 背景
健康診断で血圧が高いと判断されたので、再受診しました。その時に血圧測定手帳はあるのですが、せっかくなので、アプリで管理したいと思い、作ってみた

### 完成形

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/6bef4a91-6a90-6db6-e2f0-d938f56f6e3e.png)

### 要件
* 血圧、心拍数、体重の入力ができること
* グラフ表示できること
* 画像保存できること

### ポイント解説
* matplotlibは比較的に人気のあるライブラリなのですが、kivyとの相性はあまりよくない（というより情報が散乱していた）ため、なかなかグラフ表示までが長かった
* subplotsという使い方もグラフ出力が可能となりました

### requirements.txt
```YML
$ pip list
Package                Version
---------------------- -----------
blinker                1.7.0
certifi                2024.2.2
charset-normalizer     3.3.2
click                  8.1.7
colorama               0.4.6
contourpy              1.2.0
cycler                 0.12.1
docutils               0.20.1
Flask                  3.0.2
fonttools              4.49.0
idna                   3.6
inputs                 0.5
itsdangerous           2.1.2
Jinja2                 3.1.3
Kivy                   2.3.0
kivy-deps.angle        0.4.0
kivy-deps.glew         0.3.1
kivy-deps.sdl2         0.7.0
Kivy-examples          2.3.0
Kivy-Garden            0.1.5
kivy_matplotlib_widget 0.10.0
kivymd                 1.2.0
kiwisolver             1.4.5
MarkupSafe             2.1.5
matplotlib             3.7.2
numpy                  1.26.4
packaging              23.2
pillow                 10.2.0
pip                    24.0
pygame                 2.5.2
Pygments               2.17.2
pyparsing              3.0.9
pypiwin32              223
python-dateutil        2.9.0.post0
pywin32                306
requests               2.31.0
setuptools             58.1.0
six                    1.16.0
urllib3                2.2.0
watchdog               4.0.0
Werkzeug               3.0.1
```
### Main.py
```py

# import os
# os.environ["KIVY_NO_CONSOLELOG"] = "1"
import datetime

from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.uix.floatlayout import FloatLayout
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

from data_config import Config

#Build our app

class Matty(FloatLayout):
    def __init__(self,**kwargs):
        super().__init__(**kwargs)

        self.create_blood()

        box=self.ids.box
        box.add_widget(FigureCanvasKivyAgg(self.plt.gcf()))

    def get_json_data(self):
        ins = Config({})
        data = ins.get_json_info()
        # リストの最初の要素を使用して、すべてのキーを抽出します。
        keys = data[0].keys()

        # 各キーに対して、リストを作成し、そのキーの値をリストに追加します。
        data_by_key = {key: [item[key] for item in data] for key in keys}
        return data_by_key


    def create_blood(self):
      
        data = self.get_json_data()

        # Example blood pressure data sets
        blood_pressure_data_set1 = data['high']
        blood_pressure_data_set2 = data['low']
        weight_data = data['weight']
        pulse_data = data['pulse']

        # Create a list of dates from the start_date to the end_date
        dates = [datetime.datetime.strptime(date_string, "%Y-%m-%d").date() for date_string in data['date']]

        # Plot the first set of blood pressure data
        plt.plot_date(dates, blood_pressure_data_set1, marker='o', linestyle='--', label='High Blood Pressure', color='crimson')

        # Annotate the plotted values
        for date, value in zip(dates, blood_pressure_data_set1):
            plt.annotate(str(value), (date, value), textcoords="offset points", xytext=(-10,10), ha='center', color='crimson')

        # Plot the second set of blood pressure data
        plt.plot_date(dates, blood_pressure_data_set2, marker='o', linestyle='--', label='Low Blood Pressure', color='tomato')

        #   # Annotate the plotted values
        for date, value in zip(dates, blood_pressure_data_set2):
            plt.annotate(str(value), (date, value), textcoords="offset points", xytext=(-10,10), ha='center', color='tomato')

        plt.plot_date(dates, pulse_data, marker='o', linestyle='--', label='pulsation', color='deeppink')

        for date, value in zip(dates, pulse_data):
            plt.annotate(str(value), (date, value), textcoords="offset points", xytext=(-10,10), ha='center', color='deeppink')

        #   # Add annotations for normal values
        plt.axhline(y=135, color='crimson', linestyle='--', label='Normal Upper Limit: 135')
        plt.axhline(y=80, color='tomato', linestyle='--', label='Normal Lower Limit: 80')
        plt.fill_between(dates, 135, 180, color='tomato', alpha=0.2)
        plt.fill_between(dates, 80, 135, color='lightskyblue', alpha=0.2)
        
        plt.legend(loc='upper left')

        # Set labels and title
        plt.title('Blood Pressure/pulsation Measurements')
        plt.xlabel('Date')
        plt.ylabel('Blood Pressure (mmHg)')
        
        plt.ylim(60, 180)

        ax2 = plt.gca().twinx()

        ax2.plot_date(dates, weight_data, marker='o', linestyle='--', label='weight', color='g')
        for date, value in zip(dates, weight_data):
            ax2.annotate(str(value), (date, value), textcoords="offset points", xytext=(15,-10), ha='center')

        ax2.axhline(y=70, color='g', linestyle='--', label='Normal weight: 70')

        ax2.legend(loc='upper right')

        ax2.set_ylabel('Weight (kg)', rotation=90, labelpad=15)
        ax2.yaxis.set_label_position('right')
        ax2.set_ylim(65,85)

        # Format the x-axis to display dates
        plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
        plt.gca().xaxis.set_major_locator(mdates.DayLocator())
        plt.gcf().autofmt_xdate()

        self.plt = plt

    def saveit(self):
        td = datetime.date.today().strftime("%Y-%m-%d_%H_%M_%S")
        self.plt.savefig(f'graph_{td}.png') 
        print('finish Save it to PNG.')
   
    def create_data(self):
        # get data
        data = {
            "dt": datetime.date.today().strftime("%Y-%m-%d"),
            "high": int(self.ids.bld_high_in.text),
            "low": int(self.ids.bld_low_in.text),
            "pulse": int(self.ids.pulse_in.text),
            "weight": float(self.ids.weight_in.text)
        }
        ins = Config(data)
        ins.add_new_data()
        print(f'finish add new data of {data}')

class MainApp(MDApp):
    def build(self):
        self.theme_cls.primary_palette="Blue"
        self.theme_cls.primary_hue = "A700"
        self.theme_cls.theme_style = "Light"
        Builder.load_file('mappy.kv')
        return Matty()

MainApp().run()
```

### Subplotの場合
```py
import matplotlib
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.clock import Clock
import matplotlib.pyplot as plt
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
from kivy.lang import Builder
import random

class MyRootWidget(FloatLayout):
    def __init__(self, **kwargs):
        super(MyRootWidget, self).__init__(**kwargs)
        self.fig, self.ax = plt.subplots(2,1)
       
        self.i = 1
        self.ch1 = random.randint(15, 20)
        self.ch2 = random.randint(12, 16)
        self.line = [self.i]
        self.cards = [self.ch1]
        self.cards_02 = [self.ch2]
        Clock.schedule_interval(self.update_02, 0.5)
        box = self.ids.box
        box.add_widget(FigureCanvasKivyAgg(self.fig))
        
    # argsはClock描画用の引数として渡される
    def update_02(self, *args):
        if self.i > 6:
            pass
        else:
            self.data_set_01 = [1,2,3,4,5,6]
            y = self.data_set_01[0:self.i]
            self.ax[0].bar(self.line, y, color='r')
            self.i += 1
            self.line.append(self.i)
            
        self.fig.canvas.draw_idle()

class MyApp(App):
    def build(self):
        Builder.load_file('plot.kv')
        return MyRootWidget()

MyApp().run()
```
### 追加情報
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/d885ac25-139f-70c3-0f3c-d9c6f093ffe8.png)

UIを日本語化し、さらにデータ更新とグラフの更新も追加

グラフの更新手順は以下に示す。
1. グラフを描画するメソッドを新規作成
1. initから描画メソッドコールする
1. init関数はclass初期化する時、一度のみしかコールされないため、切り出す必要がある
1. データ更新メソッドから再描画メソッドをコール
1. 再描画メソッドは以下の通り
```py
# 再描画
def review(self):
    # matplotlibの描画をクリア
    plt.clf()
    # ヴィジェットを削除
    self.box.remove_widget(self.fig)
    # グラフ初期描画メソッドをコール
    self.init_graph()
    
# グラフ初期描画メソッド
def init_graph(self):
    # 血圧描画
    self.create_blood()
    # 描画結果をfigに代入
    self.fig = FigureCanvasKivyAgg(self.plt.gcf())　#ここでself.canvasという変数を使うとエラー
    self.box=self.ids.box
    # ヴィジェットに追加
    self.box.add_widget(self.fig)

```

### 感想
こういうアプリがたくさんあると思うのですが、こんな感じで2、3時間で作れてしまう時代ってめっちゃすごいと思いました。