Django のmodel から集計した統計情報をChartjs で円グラフを表示できるjson 形式に整形する

# 環境について

この記事は以下の環境を基に記述しています。

- Windows 7 Home Premium sp1 64 bit
- Python 3.6.1
- django 1.11.2
- jquery 3.2.1
- Chart.js 2.6.0

筆者のdjangoアプリ環境は[こちら](http://qiita.com/kaki_k/items/511611cadac1d0c69c54#_reference-0e5ｋ81a008ca55f2e05c4)を基して作成させていただきました。筆者の環境もモデル名やフィールド名は違えど概ねこれに沿っています。



##背景
django でweb家計簿アプリを自作しています。登録してある家計簿データから一ヶ月の間でカテゴリごとに出費がいくらだったかを集計して、どのカテゴリが一ヶ月の間何%出費であったかを円グラフで表示したところまでを紹介します。カテゴリとは例えば「食費」,「交際費」,「交通費」…などです。また、グラフ描画の部分は簡易的で動きのあるグラフを描画できる[Chart.js](http://www.chartjs.org/)を使用しています。

django のプロジェクトのルートは kakeibo です。ざっくりとツリー表示で以下です。


```
kakeibo
│  db.sqlite3
│  manage.py
│
├─api
│    admin.py
│    apps.py
│    models.py
│    tests.py
│    urls.py
│    views.py
│    __init__.py
│
├─cms
│    admin.py
│    apps.py
│    forms.py
│    models.py
│    tests.py
│    urls.py
│    views.py
│    __init__.py
│  
├─kakeibo
│    settings.py
│    urls.py
│    views.py
│    wsgi.py
│    __init__.py
│  
│  
└─static
    ├─css
    └js
          Chart.js
          jquery-3.2.1.min.js          
```

# model

以下のモデルを使用します。

- Category … 食費、交際費、交通費・・・その他諸々
- Kakeibo … 文字通り家計簿を表すモデル

Kakeibo は外部キーとしてCategory を参照します。家計簿ということで、何年何月何日、どのカテゴリに、いくら出費したかというデータがつらつら記録されています。

```python:cms/model.py
from django.db import models

class Category(models.Model):

    name = models.CharField(u'名称', max_length=30)

    def __str__(self):
        return self.name


class Kakeibo(models.Model):

    IN_OR_OUT = (
        ('IN', '収入'),
        ('OUT', '支出')
    )
    kakeibo_type = models.CharField(u'収支', max_length=10, choices=IN_OR_OUT)
    price = models.IntegerField(u'金額')
    date = models.DateField(u'日付')
    category = models.ForeignKey(Category)
    detail = models.TextField(u'備考', max_length=300,null=True, default=None, blank=True)

    def __str__(self):
        return self.kakeibo_type + "_" + self.category.name + "_" + self.date.strftime(u'%Y%m%d')
```

## グラフを表示する部分のhtml

グラフを表示するhtml には以下のような記述をします。chartjsではhtml5から導入された[\<canvas\> 要素](https://developer.mozilla.org/ja/docs/Web/HTML/Canvas)にグラフ描画します。その下にjavascript & jQuery を書いています。

*$.getJSON("api/get_summary/2017/04")*の部分で、2017年4月の家計簿のカテゴリごとの集計データと表示形式json 文字列を取得してChartjs で表示ということをしています。

```html:show_pie.html
<!-- 必要なところのみ記載 -->

<head>
  <meta charset="UTF-8">
  <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
  <script src="{% static 'js/Chart.js' %}"></script>  
</head>


<canvas id="myChart"></canvas>
<script type="text/javascript">
    $(function() {
        $.getJSON("/api/get_summary/2017/04", function(data) {
            var canvas = document.getElementById("myChart");
            var chart = new Chart(canvas.getContext('2d'), data);
        });
    });
</script>
```

# python からjson文字列の取得
表示する前にまず、Chart.js で表示できる文字列の構造を理解する必要があります。円グラフを表示する場合のconfig は以下のような感じです。下の構造のjson文字列を頑張ってpythonで整形しました。


```js:demo.json
    type: 'pie',
    data: {
        datasets: [{
            data: [
                300,
                50,
                100,
                150,
            ],
            backgroundColor: [
                "#F7464A",
                "#46BFBD",
                "#FDB45C",
                "#C4B45C",
            ],
            label: 'TEST DATA'
        }],
        labels: [
            "aaa",
            "iii",
            "uuu",
            "eee"
        ]
    },
    options: {
        responsive: true,
        title: {
            display: true,
            text: 'yyyy年MM月の収支'
        }
    }
```

最後にjson 文字列を送信するコードを記述します。まずは url関係から。
アプリ自体のurlの指定は /api/~ だったら api/urls を見てよー(include)としてあります。


```python:kakeibo/urls.py
#coding:utf-8

from django.conf.urls import url
from django.conf.urls import include

urlpatterns = [
    url(r'^api/',include('api.urls', namespace='api')),
]
```

api/urls.py は取りあえず以下です。"get_summary/yyyy/MM" 形式で受け取るようにします。

```python:api/urls.py
#coding:utf-8

from django.conf.urls import url
from api import views

urlpatterns = [
    url(r'^get_summary/(\d{4})/(\d{2})', views.get_summary,name='get_summary_month'),
]
```
さて、上のようにすると、api/views.py の get_summary(request, y=today.year, m=today.month)が動く様になります。(\d{4})の部分が y(例: y=2017)、 (\d{2})が(例 : d=07)という感じです。y, m に初期値として、以下のように本日の年、本日の月がそれぞれ入ります。ここからがmodel の集計、json形式の文字列の整形と、送信までを行います。


```python:api/views.py
from datetime import date
import sys
import json

from django.shortcuts import render
from django.http import HttpResponse
from django.http import JsonResponse
from django.core import serializers
from django.db.models import Sum

from cms.models import Kakeibo
from cms.models import Category

color_labels = ["#FF0000", "#00FFFF", "#00FF00", "#FF00FF", "#FF00FF", "#0000FF",
                "#FFFF00", "#80FF00", "#FF8000", "#FF0080", "#8000FF", "#0080FF", "#00FF80",
                "#cc0000", "#00cccc", "#00cc00", "#cc00cc", "#cc00cc", "#0000cc", ]

today = date.today()


def get_summary(request, y=today.year, m=today.month):
    status = None
    kakeibo_summary = Kakeibo.objects.filter(kakeibo_type='OUT',
                                             date__year=y, date__month=m).values(
        'category',).annotate(sum_price=Sum('price'))

    data = []
    labels = []
    backgroundColor = []

    for k in kakeibo_summary:
        c_pk = k["category"]
        c = Category.objects.get(pk=c_pk).name

        data.append(k["sum_price"])
        labels.append(c)
        backgroundColor.append(color_labels[c_pk])

    label = str(y) + "年" + str(m) + "月の収支"
    config = create_chartjs_json_dict(
        typestr='pie', data=data, backgroundColor=backgroundColor, label=label, labels=labels, text=label)
    json_str = json.dumps(config, ensure_ascii=False, indent=2)
    return _respose_json(request=request, json_str=json_str, status=status)


def create_chartjs_json_dict(typestr, data, backgroundColor, label, labels, text):
    data_dict = {}
    datasets_dict = {}
    datasets_list = []
    options_dict = {}
    title_dict = {}

    config = {}
    config["type"] = typestr
    datasets_dict["data"] = data
    datasets_dict["backgroundColor"] = backgroundColor
    datasets_dict["label"] = label
    datasets_list.append(datasets_dict)
    data_dict["datasets"] = datasets_list
    data_dict["labels"] = labels
    config["data"] = data_dict
    options_dict["responsive"] = "true"
    title_dict["display"] = "true"
    title_dict["text"] = text
    options_dict["title"] = title_dict
    config["options"] = options_dict

    return config

def _respose_json(request, json_str, status):
    callback = request.GET.get('callback')
    if not callback:
        callback = request.POST.get('callback')
    if callback:
        json_str = "%s(%s)" % (callback, json_str)
        response = HttpResponse(
            json_str, content_type='application/javascript; charset=UTF-8', status=status)
    else:
        response = HttpResponse(
            json_str, content_type='application/javascript; charset=UTF-8', status=status)

    return response
```
以上を用いて上述のhtmlで表示すると、グラフ表示されました。結果は以下のようになります。まだまだ途中ですがご容赦ください。
![qiita.png](https://qiita-image-store.s3.amazonaws.com/0/194584/ec741bd9-19a9-0e06-ab58-28814be4b917.png)

# 最後に

今回は、model からデータを集計してのグラフ表示するところまで書きました。json文字列の整形の部分でコードが読みにくいため今後改善したいです。色々と盛りだくさんで書きましたが、ご参考になればと思います。