Matplotlib 見栄えの良い凡例を作る

#### Matlotlibで見栄えがよくかつユーザーにとって有用な情報を凡例に出力する方法を紹介します。

この記事で使用するデータは下記のようなCSV形式の外気温データで１ファイルに約１４４件のデータが(１日分)存在します。

```t_weather_temp_out.csv
"measurement_time","temp_out"
"2023-12-06 00:08:09",1.5
"2023-12-06 00:17:52",1.4
"2023-12-06 00:27:36",1.4
"2023-12-06 00:37:20",1.3
"2023-12-06 00:47:04",1.1
...
"2023-12-06 23:30:13",7.4
"2023-12-06 23:39:57",7.3
"2023-12-06 23:49:41",7.2
"2023-12-06 23:59:25",7.0
```

下記は上記CSVを読み込んで外気温をプロットしたものです。

![PlotTempOut_1_0_basic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/d464e914-cb8e-b0c5-5dce-14c4f45765e0.jpeg)

最終的に下記に示す凡例を作成します。

![PlotTempOut_3_finalImage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/56c36c87-12f7-1c70-9816-1c51c0662ecc.jpeg)

### 実行環境

+ Linux (**特にRaspberry Pi OS Desktop**) を想定しています。  
+ 日本語の可変幅フォントはIPAフォントがインストールされていること。  
※OSにインストールされている日本語フォントであれば問題なく動作すると思います。  
[文字情報技術促進協議会 「IPAexフォントおよびIPAフォントについて」](https://moji.or.jp/ipafont/)
+ 日本語等幅フォントとして Source Han Code JP がインストールされていること。
[GitHub: adobe-fonts / source-han-code-jp](https://github.com/adobe-fonts/source-han-code-jp)
+ 別途作成したpython仮想環境に matplotlib と pandasをインストールして実行します。

### 参考にしたドキュメント

[matplotlib Using Matplotlib > Axes and subplots > Legent guid](https://matplotlib.org/stable/users/explain/axes/legend_guide.html#legend-guide)

[matplotlib API Reference matplotlib.patchers.Patch](https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html)


### 1.日本語フォントのインストール

#### 1-1 **.fonts** ディレクトリの中にダウンロードしたフォントファイルを解凍して格納する

【参考】Raspberry Pi OS Desktop にインストールした日本語フォントファイルの一覧

```~/.fonts/
pi@raspi-4:~ $ tree .fonts/
.fonts/
├── IPAexfont00401
│   ├── IPA_Font_License_Agreement_v1.0.txt
│   ├── Readme_IPAexfont00401.txt
│   ├── ipaexg.ttf
│   └── ipaexm.ttf
└── source_han_code_jp
    ├── LICENSE.txt
    ├── README-JP.md
    ├── SourceHanCodeJP-Bold.otf
    ├── SourceHanCodeJP-BoldIt.otf
    ├── SourceHanCodeJP-ExtraLight.otf
    ├── SourceHanCodeJP-ExtraLightIt.otf
    ├── SourceHanCodeJP-Heavy.otf
    ├── SourceHanCodeJP-HeavyIt.otf
    ├── SourceHanCodeJP-Light.otf
    ├── SourceHanCodeJP-LightIt.otf
    ├── SourceHanCodeJP-Medium.otf
    ├── SourceHanCodeJP-MediumIt.otf
    ├── SourceHanCodeJP-Normal.otf
    ├── SourceHanCodeJP-NormalIt.otf
    ├── SourceHanCodeJP-Regular.otf
    └── SourceHanCodeJP-RegularIt.otf

2 directories, 20 files
```

#### 1-2 fc-cache コマンドを実行する

```bash
$ fc-cache -fv
# ...コンソール出力は大量なので割愛します...
```

#### 1-3 fc-list コマンドでフォントを確認する

```bash
pi@raspi-4:~ $ fc-list :lang=ja
# ... .fonts のみ出力します
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-MediumIt.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP M,源ノ角ゴシック Code JP M:style=M It,Italic
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-Light.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP L,源ノ角ゴシック Code JP L:style=L,Regular
/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc: Noto Serif CJK SC:style=Regular
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-Regular.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP R,源ノ角ゴシック Code JP R:style=R,Regular
/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc: Noto Serif CJK TC:style=Regular
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-ExtraLight.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP EL,源ノ角ゴシック Code JP EL:style=EL,Regular
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-RegularIt.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP R,源ノ角ゴシック Code JP R:style=R It,Italic
/home/pi/.fonts/source_han_code_jp/SourceHanCodeJP-Normal.otf: 源ノ角ゴシック Code JP,Source Han Code JP,Source Han Code JP N,源ノ角ゴシック Code JP N:style=N,Regular
# こちらは IPAフォント
/home/pi/.fonts/IPAexfont00401/ipaexg.ttf: IPAexゴシック,IPAexGothic:style=Regular
```

#### 1-4. Matplotlibのキャッシュディレクトリを削除する  
※1 **cacheディレクトリ配下のmatplotlibサブディレクトリ**を削除します。
※2 削除し忘れるとインストールした日本語等倍フォントが Matplotlib に反映されず文字化けします。

```bash
pi@raspi-4:~ $ ls -l --time-style long-iso .cache/matplotlib/
合計 64
-rw-r--r-- 1 pi pi 64450 2023-12-16 12:04 fontlist-v330.json
```

### 2. 外気温のプロットと画像生成処理

ソースコードとCSVファイルの一覧

```bash
matplotlib_custom_legend/
├── PlotTempOut_basic.py          # 凡例なし画像プロットメインスクリプト
├── PlotTempOut_basicLegend.py    # 単純な凡例あり画像プロットメインスクリプト
├── PlotTempOut_customLegend.py   # Patchオブジェクト凡例の画像プロットメインスクリプト 
├── PlotTempOut_simpleTempStat.py # pandas統計情報を使用した画像プロットメインスクリプト
├── data
│   └── t_weather_temp_out.csv    # サンプルのCSVファイル
└── plot_weather
    ├── __init__.py
    └── plotter
        ├── __init__.py
        ├── pandas_statistics.py           # pandasを使用した統計情報計算モジュール
        ├── plotterweather_basic.py        # 凡例なし画像プロットモジュール
        ├── plotterweather_basicLenend.py  # 単純な凡例あり画像プロットモジュール
        ├── plotterweather_patch.py        # Patchオブジェクト凡例の画像プロットモジュール 
        └── plotterweather_simpleStat.py   # pandas 統計情報利用した画像プロットモジュール
```

この記事で紹介するソースは下記GitHubで公開しています。
[GitHub (pipito.yukio) qiita-posts /Matplotlib/matplotlib_custom_legend](https://github.com/pipito-yukio/qiita-posts/tree/main/Matplotlib/matplotlib_custom_legend)

#### 2-1. ライブラリーのインポート

オブジェクトの型を指定するとIDEのメソッド補完が有効になり開発効率が上がります。  

```plotterweather_basic.py
import base64
from datetime import datetime, timedelta
from io import BytesIO
from typing import Optional, Tuple

import pandas as pd
from pandas.core.frame import DataFrame

import matplotlib.dates as mdates
from matplotlib import rcParams
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from matplotlib.pyplot import setp
```

(1) インストール済みの日本語フォント(ゴシック系)を設定します

```python
# 日本語表示
rcParams['font.family'] = ["sans-serif"]
rcParams['font.sans-serif'] = ["IPAexGothic", "Noto Sans CJK JP"]
```

(2) CSVファイルのカラムを定数として定義 ※列名で何回も参照します。

```python
# カラム情報
COL_TIME: str = "measurement_time"
COL_TEMP_OUT: str = "temp_out"
```

#### 2-2. 外気温プロット処理

##### 2-2-1. CSVデータを pandas に読み込み DataFrameオブジェクトを取得  
　※**測定時刻列をインデックスに設定します。**

```python
def gen_plot_image(csv_full_path: str) -> Tuple[int, Optional[str]]:
    """
    観測データの画像を生成する
    """
    df_data: DataFrame = pd.read_csv(csv_full_path, parse_dates=[COL_TIME])
    if df_data.shape[0] == 0:
        # データなし
        return 0, None

    # 測定時刻列をインデックスに設定する
    df_data.index = df_data[COL_TIME]
```

##### 2-2-2. 図と外気温サブプロット領域を作成  

　【領域サイズ】640ピクセル x 480 ピクセル

ピクセルをインチに変換する方法は下記ドキュメントを参照
[matplotlib Examples > Subplots, axes and figures > Figure size in different units](https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_size_units.html)

※ HTMLの画像として出力するために matplotlib.figure.Figure オブジェクトを生成します。

```python
    # 観測データプロットグラフ生成
    # 図の生成
    px: float = 1 / rcParams["figure.dpi"]  # pixel in inches
    fig_width_px: float = 600 * px
    fig_height_px: float = 480 * px
    fig = Figure(figsize=(fig_width_px, fig_height_px), constrained_layout=True)

    # 外気温サププロット領域生成
    ax_temp: Axes = fig.subplots(nrows=1, ncols=1)
    # グリッド線設定 ※x軸,y軸
    ax_temp.grid(linestyle="dotted", linewidth=1.0)
```

##### 2-2-3. X軸 / Y軸のテキストに小さめのフォントサイズを設定

```python
    # 軸ラベルのフォントサイズを設定
    setp(ax_temp.get_xticklabels(), fontsize=9.)
    setp(ax_temp.get_yticklabels(), fontsize=9.)
```

##### 2-2-4. タイトルとY軸ラベルの設定

```python
    # 図のタイトルに表示する測定日をデータの先頭から取得
    pd_timestamp: pd.Timestamp = df_data.iloc[0][COL_TIME]
    py_datetime: datetime = pd_timestamp.to_pydatetime()
    curr_date: str = py_datetime.strftime("%Y-%m-%d")
    # 図のタイトル
    ax_temp.set_title(f"【測定日】{curr_date}")
    # y軸ラベル
    ax_temp.set_ylabel("外気温 (℃)", fontsize=10.)
```

##### 2-2-5. X軸の表示範囲 (set_xlim) の設定

　※1 表示範囲として当日の0時〜翌日の0時までを設定
　※2 軸ラベルは時間 (00,03,06,09,12,15,18,21,翌日の00)

![TempOut_x_range.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/e28f2db4-00be-3d16-bf35-cd3b73a88532.jpeg)

```python
    # x軸の範囲: 当日 00時 から 翌日 00時
    dt_curr: datetime = datetime.strptime(curr_date, "%Y-%m-%d")
    dt_next: datetime = dt_curr + timedelta(days=1)
    ax_temp.set_xlim(xmin=dt_curr, xmax=dt_next)
    # x軸フォーマット: 軸ラベルは時間 (00,03,06,09,12,15,18,21,翌日の00)
    ax_temp.xaxis.set_major_formatter(mdates.DateFormatter("%H"))
```

##### 2-2-6. Y軸の表示範囲 (set_ylim) の設定

```python
    # 外気温: 最低(-20℃)〜最大(40℃)
    ax_temp.set_ylim(ymin=-20.0, ymax=40.0)
```

##### 2-2-7. 外気温プロット ※凡例なし

```python
    #  外気温のプロット
    ax_temp.plot(
        df_data["measurement_time"], df_data["temp"], color="blue", marker=""
    )
```

##### 2-2-8. プロットイメージをBase64エンコード文字列に変換して呼出し元に返却

実装は下記ドキュメントのサンプルをほぼそのまま利用しています。

[matplotlib How-to: How to use Matplotlib in a web application server](https://matplotlib.org/3.1.1/faq/howto_faq.html#how-to-use-matplotlib-in-a-web-application-server)

```python
    # 画像をバイトストリームに溜め込む
    buf = BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight")
    # バイトデータをbase64エンコード変換
    data = base64.b64encode(buf.getbuffer()).decode("ascii")
    if logger is not None:
        logger.debug(f"data.len: {len(data)}")
    # HTML img tag src
    img_src: str = "data:image/png;base64," + data
    # 件数とHTMLのimgタグに設定可能な文字列を返却
    return df_data.shape[0], img_src
```

返却されるデータの画像イメージは一番最初に紹介した画像になります。

#### 2-3. スクリプトメイン処理

　HTMLテンプレートにプロット処理モジュールから返却されたbase64エンコード文字列を設定すると出力結果をブラウザで見れます。

```python
import os
from typing import List, Optional

from plot_weather.plotter.plotterweather_basic import gen_plot_image
# スクリプト名
script_name = os.path.basename(__file__)
# CSVファイル
CSV_PATH: str = os.path.join("data", "t_weather_temp_out.csv")

# 出力画層用HTMLテンプレート
OUT_HTML = """
<!DOCTYPE html>
<html lang="ja">
<body>
<img src="{}" alt="外気温データプロット画像" border="1" />
</body>
</html>
"""


def save_text(file, contents):
    with open(file, 'w') as fp:
        fp.write(contents)


if __name__ == '__main__':
    rec_count: int
    img_src: Optional[str]
    rec_count, img_src = gen_plot_image(CSV_PATH)
    if rec_count > 0:
        # 出力結果をHTMLテンプレートに設定する
        script_names: List[str] = script_name.split(".")
        save_name = f"{script_names[0]}.html"
        save_path = os.path.join("output", save_name)
        html: str = OUT_HTML.format(img_src)
        save_text(save_path, html)
        print(f"Saved {save_path}")
    else:
        print("TempOut record not found.")
```

### 3. 凡例のカスタマイズ

数値を含む凡例の項目出力では日本語等倍フォントを指定したほうが見やすくなります。  
※可視化した画像をスマホなどのデバイスで見る場合は特にそう感じます。

![CompareFixedFont.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/5a41f982-43c6-cdcf-8790-a92722d3c4aa.jpeg)

#### 3-1. 最低・最高・平均気温(横線)のラベルを凡例とする

+ 日本語等倍フォントの設定
[Matplotlib Text properties and layout: Text with non-latin glyphs](https://matplotlib.org/stable/users/explain/text/text_props.html#text-with-non-latin-glyphs)

```plotterweather_basicLenend.py
# モジュールのインポートは割愛

# 日本語表示
rcParams['font.family'] = ["sans-serif", "monospace"]
rcParams['font.sans-serif'] = ["IPAexGothic", "Noto Sans CJK JP"]
# 日本語等倍フォントの設定
rcParams['font.monospace'] = ["Source Han Code JP", "Noto Sans Mono CJK JP"]
```

+ **Axes.axhline** にそれぞれの値を設定したラベルを追加する

```python
    # 横線: 最低気温
    temper: float = round(df_data[COL_TEMP_OUT].min(), 1)
    ax_temp.axhline(temper, label=f"最低 {temper:4.1f} ℃",
                    color="", linestyle="dashed", linewidth=1.)
    # 横線: 最低気温
    temper = round(df_data[COL_TEMP_OUT].max(), 1)
    ax_temp.axhline(temper, label=f"最高 {temper:4.1f} ℃",
                    color="orange", linestyle="dashed", linewidth=1.)
    # 横線: 平均気温
    temper = round(df_data[COL_TEMP_OUT].mean(), 1)
    ax_temp.axhline(temper, label=f"平均 {temper:4.1f} ℃",
                    color="red", linestyle="dashdot", linewidth=1.)
    # 凡例作成
    ax_legend: Legend = ax_temp.legend(loc="best", title="外気温統計情報")
```

+ 日本語等倍フォントの設定  
 (1) 凡例オブジェクトを取得する (**ax_temp.get_legend()**)
 (2) 凡例オブジェクト内の全てのラベルに日本語等倍フォントに設定する
　**text.set_fontfamily("monospace") で "Source Han Code JP" が設定されます**

```python
    # 凡例のテキストラベルに日本語等倍フォントを設定する
    text: Text
    for text in ax_legend.get_texts():
        text.set_fontfamily("monospace")
```

出力画像は以下のようになります。

![PlotTempOut_1_1_basicLegend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/0c87c733-0c88-9474-2eb6-e60fb4395571.png)

#### 3-2. 最低・最高・平均気温に出現時刻も出力

+ pandas のDataFrameから**出現時刻を含めた統計情報を計算する**  
計算結果を格納するデータを dataclass として定義する

```pandas_statistics.py
from dataclasses import dataclass
from datetime import datetime

import numpy as np
import pandas as pd
from pandas.core.frame import DataFrame
from pandas.core.series import Series

"""
外気温統計情報計算モジュール for pandas
"""

# pandasのインデクス列
COL_TIME: str = "measurement_time"
# 外気温列
COL_TEMP_OUT: str = "temp_out"


@dataclass
class TempOut:
    """ 外気温情報 """
    # 出現時刻
    appear_time: str
    # 外気温
    temper: float


@dataclass
class TempOutStat:
    """ 外気温統計情報 """
    # 測定日
    measurement_day: str
    # 平均外気温
    average_temper: float
    # 最低外気温情報
    min: TempOut
    # 最高外気温情報
    max: TempOut
```

+ 引数のDataFrame は降順でソート済み

```python
def get_temp_out_stat(df_desc: DataFrame) -> TempOutStat:
    """ 外気温の統計情報 ([最低気温|最高気温] の気温とその出現時刻) を取得する """

    def get_measurement_time(pd_timestamp: pd.Timestamp) -> str:
        py_datetime: datetime = pd_timestamp.to_pydatetime()
        # 時刻部分は "時:分"までとする
        return py_datetime.strftime("%Y-%m-%d %H:%M")

    # 外気温列
    temp_out_ser: Series = df_desc[COL_TEMP_OUT]
    # 外気温列から最低・最高・平均気温を取得
    min_temper: np.float64 = temp_out_ser.min()
    max_temper: np.float64 = temp_out_ser.max()
    avg_temper: np.float64 = temp_out_ser.mean()
    # 全ての最低気温を取得する
    df_min_all: DataFrame = df_desc[temp_out_ser <= min_temper]
    # 全ての最高気温を取得する
    df_max_all: pd.DataFrame = df_desc[temp_out_ser >= max_temper]
    # それぞれ直近の１レコードのみ取得 ※行のSeriesオブジェクト
    min_first: Series = df_min_all.iloc[0]
    max_first: Series = df_max_all.iloc[0]
    # 最低気温情報
    min_measurement_datetime: str = get_measurement_time(min_first[COL_TIME])
    #   測定日は先頭 10桁分(年月日)
    measurement_day: str = min_measurement_datetime[:10]
    #   出現時刻は時分
    min_appear_time: str = min_measurement_datetime[11:]
    temp_out_min: TempOut = TempOut(min_appear_time, float(min_first[COL_TEMP_OUT]))
    # 最高気温情報
    max_measurement_datetime: str = get_measurement_time(max_first[COL_TIME])
    max_appear_time: str = max_measurement_datetime[11:]
    temp_out_max: TempOut = TempOut(max_appear_time, float(max_first[COL_TEMP_OUT]))
    return TempOutStat(
        measurement_day, average_temper=float(avg_temper),
        min=temp_out_min, max=temp_out_max
    )
```

最低気温、最高気温は1日でも複数回出現する可能性があります。 

※ 以下はPycharmでのデバック出力

```
df_min_all
 ['measurement_time', 'temp_out'] [measurement_time] 
 [2023-12-06 07:06:49 2023-12-06 07:06:49      -1.0] 
 [2023-12-06 06:57:03 2023-12-06 06:57:03      -1.0] 
 [2023-12-06 06:47:20 2023-12-06 06:47:20      -1.0]
```

```
df_max_all
 ['measurement_time', 'temp_out'] [measurement_time]
  [2023-12-06 14:54:12 2023-12-06 14:54:12      10.4]
```

最低気温の直近1レコード取得

```
min_first
 ('measurement_time', Timestamp('2023-12-06 07:06:49')) ('temp_out', -1.0)
```

最高気温の直近1レコード取得

```
max_first
 ('measurement_time', Timestamp('2023-12-06 14:54:12')) ('temp_out', 10.4)
```

統計情報のデバック出力 ※見やすくするために改行を入れています。

```
 stat = {TempOutStat} TempOutStat(
 measurement_day='2023-12-06', average_temper=4.3, 
 min=TempOut(appear_time='07:06', temper=-1.0), 
 max=TempOut(appear_time='14:54', temper=10.4)
 )
```

##### 3-2-1. 統計情報計算モジュールインポートと定数定義

```plotterweather_simpleStat.py
from plot_weather.plotter.pandas_statistics import (
    COL_TIME, COL_TEMP_OUT, TempOutStat, get_temp_out_stat
)

# カラー定数定義
COLOR_MIN_TEMPER: str = "darkcyan"
COLOR_MAX_TEMPER: str = "orange"
COLOR_AVG_TEMPER: str = "red"
```

##### 3-2-2. 統計情報の計算処理  

CSVから取得したDataFrameを降順でソートした新たなDataFrameを生成し統計情報計算モジュールに設定する

```python
def gen_plot_image(csv_full_path: str) -> Tuple[int, Optional[str]]:
    """
    観測データの画像を生成する
    """
    df_data: pd.DataFrame = pd.read_csv(csv_full_path, parse_dates=[COL_TIME])
    if df_data.shape[0] == 0:
        # データなし
        return 0, None

    # 測定時刻列をインデックスに設定する
    df_data.index = df_data[COL_TIME]

    # 直近の最低と最高気温を取得するために降順にソートする
    # Default inplace=False is new DataFrame.
    sorted_df: pd.DataFrame = df_data.sort_index(ascending=False)
    # 外気温の統計情報を取得
    stat: TempOutStat = get_temp_out_stat(sorted_df)
```

##### 3-2-3. プロット用内部関数の定義  

+ 横線プロットとラベル生成を一体化した関数 (**plot_hline_with_label**)

```python
def make_graph(df_data: DataFrame, stat: TempOutStat) -> Figure:
    """ 観測データのDataFrameからグラフを生成し描画領域を取得する """

    def plot_hline_with_label(axes: Axes, label: str, temper: float, line_color: str,
                              line_style: Optional[str] = None, appear_time: Optional[str] = None):
        """ ラベル付きの横線プロット """
        # axhilne()に引き渡すパラメータが長くなるので線種情報はDictに纏めて設定する
        line_style_dict: Dict = {"color": line_color, "linestyle": line_style, "linewidth": 1.}
        # 凡例に表示するラベル生成
        legend_label: str
        if appear_time is not None:
            # 最低気温と最高気温は出現時刻を含む
            legend_label = f"{label} {temper:4.1f} ℃ [{appear_time}]"
        else:
            # 平均気温は出現時刻なし
            legend_label = f"{label} {temper:4.1f} ℃"
        axes.axhline(temper, label=legend_label, **line_style_dict)
```

##### 3-2-4. 画像プロット処理 

+ 統計情報データごとに内部関数を呼び出し、横線と凡例に出力するラベルを出力する

```python
    # 図の作成、タイトル、X軸 / Y軸の設定は割愛
    #  外気温のプロット
    ax_temp.plot(df_data[COL_TIME], df_data[COL_TEMP_OUT], color="blue", marker="")
    

    # 横線: 最低気温と出現時刻 ※直近
    plot_hline_with_label(ax_temp, "最低", stat.min.temper, COLOR_MIN_TEMPER,
                          line_style="dashed", appear_time=stat.min.appear_time
                          )
    # 横線: 最高気温と出現時刻 ※直近
    plot_hline_with_label(ax_temp, "最高", stat.max.temper, COLOR_MAX_TEMPER,
                          line_style="dashed", appear_time=stat.max.appear_time
                          )
    # 横線: 平均気温
    plot_hline_with_label(ax_temp, "平均", stat.average_temper, COLOR_AVG_TEMPER,
                          line_style="dashdot", appear_time=None
                          )
    ax_legend: Legend = ax_temp.legend(loc="best", title="外気温統計情報")

    # 数値を含むラベルに日本語等倍フォントを設定する
    text: Text
    for text in ax_legend.get_texts():
        text.set_fontfamily("monospace")
    return fig
```

出力画像は以下のようになります。

![PlotTempOut_1_3_legendStatLineWithValue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/582d490b-4967-2a0d-6316-c4a03b23f018.png)


#### 3-3. 凡例の項目にPatchオブジェクトを使用する

凡例の項目としてPatchオブジェクトを使用すると凡例の見栄えが格段に良くなります。

![PatchObject.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/3058e7ef-c4c9-1b94-8f78-d8acded0fa94.jpeg)

##### 3-3-1. プロット用内部関数の定義  

+ Patchオブジェクトを生成する関数 (**make_patch**)
+ 横線をプロットする関数 (**plot_hline**)

```plotterweather_patch.py
def make_graph(df_data: DataFrame, temp_out_stat: TempOutStat) -> Figure:
    """ 観測データのDataFrameからグラフを生成し描画領域を取得する """

    def make_patch(label: str, temper: float, patch_color: str, appear_time: Optional[str]) -> Patch:
        """ 指定されたラベルと外気温統計の凡例を生成 """

        if appear_time is not None:
            # 最低気温と最高気温は出現時刻を含む
            patch_label: str = f"{label} {temper:4.1f} ℃ [{appear_time}]"
        else:
            # 平均気温は出現時刻なし
            patch_label: str = f"{label} {temper:4.1f} ℃"
        return Patch(color=patch_color, label=patch_label)

    def plot_hline(axes: Axes, temper: float, line_color: str, line_style):
        """ 指定された統計情報の外気温の横線を生成する """
        # axhilne()に引き渡すパラメータが長くなるので線種情報はDictに纏めて設定する
        line_style_dict: Dict = {"color": line_color, "linestyle": line_style, "linewidth": 1.}
        axes.axhline(temper, **line_style_dict)
```

##### 3-3-2. 画像プロット処理  

+ 統計情報の値ごとに内部関数 (make_patch) を呼び出して凡例に追加するPatchオブジェクト生成する
+ 統計情報の値ごとに内部関数 (plot_hline) を呼び出して横線をプロットする
+ **生成した全てのPatchオブジェクトをhandlesキーワード引数に設定する**
+ Patchオブジェクトのテキストラベルのフォントに日本語等倍フォントを設定する

```python
    # 図の作成、タイトル、X軸 / Y軸の設定は割愛
    #  外気温のプロット
    ax_temp.plot(df_data[COL_TIME], df_data[COL_TEMP_OUT], color="blue", marker="")

    # 凡例に追加する統計情報(Patch)を生成する
    # 1-1. 最低気温のPatchオブジェクト
    stat_min: TempOut = stat.min
    mim_patch: Patch = make_patch("最低", stat_min.temper, COLOR_MIN_TEMPER,
                                  appear_time=stat_min.appear_time)
    # 1-2. 最高気温のPatchオブジェクト
    stat_max: TempOut = stat.max
    max_patch: Patch = make_patch("最高", stat_max.temper, COLOR_MAX_TEMPER,
                                  appear_time=stat_max.appear_time)
    # 1-3. 平均気温のPatchオブジェクト
    avg_patch: Patch = make_patch("平均", stat.average_temper, COLOR_AVG_TEMPER,
                                  appear_time=None)
    # 2-1. 最低気温の横線
    plot_hline(ax_temp, stat_min.temper, COLOR_MIN_TEMPER, line_style="dashed")
    # 2-2. 最高気温の横線
    plot_hline(ax_temp, stat_max.temper, COLOR_MAX_TEMPER, line_style="dashed")
    # 2-3. 平均気温の横線
    plot_hline(ax_temp, stat.average_temper, COLOR_AVG_TEMPER, line_style="dashdot")
    # 凡例にPatchオブジェクトを追加する
    ax_legend: Legend = ax_temp.legend(
        handles=[mim_patch, max_patch, avg_patch], title="外気温統計情報"
    )

    # Patchオブジェクトのテキストラベルに日本語等倍フォントを設定する
    text: Text
    for text in ax_legend.get_texts():
        text.set_fontfamily("monospace")
    return fig
```

出力画像は以下のようになります。

![PlotTempOut_finalImage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/f496cca8-92b8-ddea-6e78-ba8452eeafde.jpeg)


#### 4. 結論

　凡例にPatchオブジェクトを使用するとそれなりにコーディングの手間はかかりますが見栄えは良くなります。

　サンプルデータは実際に運用しているデータで、今回の記事用に外気温だけを抜き出しCSVファイルに出力したものです。運用機はRaspberry Pi 4 Model Bで dokerコンテナで稼働するPostgreSQLに気象データを記録しています。


![Raspi4_systemOverview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/83c7c164-0e6a-f352-76aa-cb59df6ebb07.jpeg)

　下記画面は当日データのグラフ表示で左側は改良前、右側が今回の凡例にPatchを使用したものです。

![WeatherDataViewer_appGraph_1_today.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/13af94e7-6925-fbf4-73eb-488ec494a9db.jpeg)

　この画面は指定した期間データのグラフを表示したもので、最低気温と最高気温の出現時刻は日付も含んだものに改良しています。

![WeatherDataViewer_appGraph_2_range.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/15824993-8446-1d00-6f56-8b6fd030cdf1.jpeg)

Flaskアプリは下記GitHubでソースコードを公開しています。

[GitHub(pipito-yukio) 気象データ表示Webアプリケーション](https://github.com/pipito-yukio/plot_weather_flaskapp)

Androidアプリは下記GitHubでソースコードを公開しています。

[GitHub(pipito-yukio) IoT初歩の初歩 Androidアプリからラズパイに繫ぐ](https://github.com/pipito-yukio/weather_data_viewer)