matplotlibにおけるデザインとデータの分離

[1]:https://docs.python.org/ja/3/library/configparser.html?highlight=configparser#configparser.ConfigParser

# 概要

## 課題

私は**Python**から**matplotlib**を使用してグラフを描くことが多いです。
**Python**アプリケーションでデータを生成、整形、**matplotlib**によるグラフ出力、のような流れで行います。

このとき、以下のような課題を抱えていました。

* グラフの**デザイン**が**統一**されていない。
* **同じような記述**をそれぞれの**アプリケーション**で行っている。
* **デザイン**と**コード（Pythonファイルの記述）**が**視覚的**、**言語的**に結びついていない。

## 原因

**データを可視化**するために記述するコードと、**デザインを管理**するために記述するコードが
1つのアプリケーション内に書かれているからだと考えました。

## 対応

**デザインを管理**するためのコードを**設定ファイル**として分離してみました。

# 詳細

課題として記載した3つのうち、`グラフのデザインが統一されていない`というのは、
縦軸や横軸に割り当てた**ラベルの大きさ**や**凡例**、**プロットされる点**等が統一されていないということです。

これは2番目の課題とも関連するのですが、**同じような記述**を別のPythonファイルで行っているからでした。

**統一**されておらず、その場その場で過去のPythonファイルを参照して、**コピペ**をしていたことが問題でした。

どうして**コピペ**を繰り返してしまうのか、それは私が求めている**デザイン**と**コード**が
結びついていない、少なくとも直感的ではない、からだと思います。これは3つ目の課題に繋がります。

これら3つの課題の原因は、**データ**と**デザイン**が**分離**されていないからだと、私は考えました。


そうであるのならば、話は単純で**デザイン**を**設定ファイル**へ分離すればよいのです。

## デザインを設定ファイルに言葉で表現

以下の表のようにデザインと言葉を対応させてみました。

各項目の意味は、

- デザインの分類
    - 設定ファイルのパラメータ
        - パラメータの意味
        - 対応するmatplotlibのコード

です。


- **Size（大きさを設定する）**
    - figure_x
        - 図の横方向の大きさ
        - pyplot.figure(figsize=(figure_x, figure_y))
    - figure_y
        - 図の縦方向の大きさ
        - figure_xと同じ
    - font_title
        - タイトルのフォントの大きさ
        - pyplot.title(fontsize=font_title)
    - font_x_label
        - X軸のフォントの大きさ
        - pyplot.figure.add_subplot().ax.set_xlabel(fontsize=font_x_label)
    - font_y_label
        - Y軸のフォントの大きさ
        - pyplot.figure.add_subplot().ax.set_ylabel(fontsize=font_y_label)
    - font_tick
        - 軸のメモリのフォントの大きさ
        - pyplot.tick_params(labelsize=font_tick)
    - font_legend
        - 凡例のフォントの大きさ
        - pyplot.legend(fontsize=font_legend)
    - marker
        - データをプロットする際のマーカーの大きさ
        - pyplot.figure.add_subplot().ax.plot(markersize=marker)
        
- **Position（位置を設定する）**
    - subplot
        - グラフを配置する場所
        - pyplot.figure.add_subplot(subplot)
    - legend_location
        - 凡例を配置するグラフ内の場所
        - pyplot.legend(loc=legend_location)


## 具体的な設定ファイルの表現方法

設定ファイルはいくつかの表現方法、jsonやyaml等を検討しました。

結果、Pythonに標準である**[configparser][1]**を使うことにしました。

jsonやyamlでもよいとは思うのですが、階層的に表現できるよりも、**直感的に使いやすい**ほうがいいと考えました。

**configparser**で設定ファイルを表現すると次のようになります。

```config.ini
[Size]
figure_x=8
figure_y=8
font_title=20
font_x_label=18
font_y_label=18
font_tick=10
font_legend=15
marker=10

[Position]
subplot=111
legend_location=upper right

[Markers]
0=D
1=>
2=.
3=+
4=|

[Color]
0=red
1=blue
2=green
3=black
4=yellow
```

**デザインの分類**に当たる項目は**[Size]**のように**角括弧**で囲います。これは**セクション**と呼ばれます。
その下に**設定ファイルのパラメータ=値**を書いていきます。これは**キー**と呼ばれます。

上記の設定ファイルには**Size**、**Position**の他に、**Markers（プロットするマーカーの種類）**や**Color（プロットされるマーカーや線の色）**も表現されています。

## 設定ファイルに記載されたパラメータをPythonコードから使用

設定ファイルのパラメータへPythonアプリケーションがアクセスするには、次のようにコードを記述します。

```python

import configparser


rule_file = configparser.ConfigParser()
rule_file.read("configファイルのパス", "UTF-8")

hogehoge = rule_file["セクション名"]["キー名"]
```

注意点として、読み込んだ値は**文字列**になります。


## 実際の使用例

以下のコードは、**渡されたデータ**と**デザインの設定ファイル**をもとに折れ線グラフを作成します。

```python:make_line_graph.py
""" 折れ線グラフ作成関数

渡されたデータを使用して折れ線グラフを描画し、画像データとして保存する。

"""


import configparser
import matplotlib.pyplot as plt


def make_line_graph(data, config="config.ini"):
    """折れ線グラフ描画

    渡されたデータを使用して折れ線グラフを作成する。
    デザインは別のconfigファイルから読み取る。

    Args:
        data(dict): プロットするデータが格納されている
        config(str): configファイルの名前

    Returns:
        bool: Trueなら作成完了、Falseなら作成失敗

    Note:
        引数のdataに含めるべきkeyとvalueについて以下に記載する。

        key         : value
        ------------------------
        title(str): グラフのタイトル名
        label(list): 凡例の説明
        x_data(list): x軸のデータ
        y_data(list): y軸のデータ
        x_ticks(list): x軸のメモリに表示する値
        y_ticks(list): y軸のメモリに表示する値
        x_label(str): x軸の名前
        y_label(str): y軸の名前
        save_dir(str): 保存ファイルパス
        save_name(str): 保存ファイル名
        file_type(str): 保存ファイル形式
    """

    rule_file = configparser.ConfigParser()
    rule_file.read("./conf/{0}".format(config), "UTF-8")

    fig = plt.figure(figsize=(int(rule_file["Size"]["figure_x"]), int(rule_file["Size"]["figure_y"])))

    ax = fig.add_subplot(int(rule_file["Position"]["subplot"]))
    ax.set_xlabel(data["x_label"], fontsize=int(rule_file["Size"]["font_x_label"]))
    ax.set_ylabel(data["y_label"], fontsize=int(rule_file["Size"]["font_y_label"]))

    for index in range(len(data["x_data"])):
        ax.plot(data["x_data"][index],
                data["y_data"][index],
                label=data["label"][index],
                color=rule_file["Color"][str(index)],
                marker=rule_file["Markers"][str(index)],
                markersize=int(rule_file["Size"]["marker"]))

    plt.title(data["title"], fontsize=int(rule_file["Size"]["font_title"]))

    if "x_ticks" in data.keys():
        plt.xticks(data["x_ticks"][0], data["x_ticks"][1])

    if "y_ticks" in data.keys():
        plt.yticks(data["y_ticks"][0], data["y_ticks"][1])

    plt.tick_params(labelsize=int(rule_file["Size"]["font_tick"]))
    plt.legend(fontsize=rule_file["Size"]["font_legend"], loc=rule_file["Position"]["legend_location"])
    plt.savefig("".join([data["save_dir"], "/", data["save_name"], ".", data["file_type"]]))

```

データを渡すPythonファイルは以下のような感じになります。

```python:main.py

from make_line_graph import make_line_graph

data = {
    "title": "hogehoge",
    "label": ["A", "B"],
    "x_data": [x_data1, x_data2],
    "y_data": [y_data1, y_data2],
    "x_ticks": [x_ticks1, x_ticks2],
    "y_ticks": [y_ticks1, y_ticks2],
    "x_label": "hogehoge",
    "y_label": "hogehoge",
    "save_dir": "保存したいフォルダのパス",
    "save_name": "保存したいファイル名",
    "file_type": "拡張子",
}

make_line_graph(data, config="config.ini")
```

## 使ってみた感想

### 良かった点

デザインが変えやすくなりました。
特に、各種フォントサイズはプロットするデータやラベルに入れる文字数によって変わります。

また設定ファイルを複製してカスタマイズすることで、グラフデザインを変えたくなったときのPythonファイル変更量が少なくなりました。
読み込む設定ファイル名だけ変えればよし。

グラフデザインと設定ファイルが結びついているので、どのデザインがどのコードと対応しているのか忘れても大丈夫です。


### 悪かった点

どこまで汎用性をもたせるかが難しいです。

作った**make_line_graph.py**は折れ線グラフ作成関数ですが、似たようなPythonファイルが増えるとよろしくないのでできる限り汎用的にしました。
ただこれではうまくグラフが描画出来ず、それに対応するために別の折れ線グラフ作成関数が乱立すると、振り出しに戻ってしまいそうです・・・。

汎用性を考えればきりがないのかなとも思ったりしてますが・・・。