DjangoなHighcharts.js

2019-08-01Python,TIPSDjango,JavaScript

Highchartsは細部まで凝った綺麗なチャートやグラフを作成できる便利なJavaScriptライブラリです。 OSSですが商用利用は有料になります(そこそこお高い)
※ 個人、学校などの非営利目的であればFreeライセンスが取得できます。

(Highcharts.js 公式)
https://www.highcharts.com/products/highcharts/

Highchartsでグラフを描画するにはバックエンドからのデータをJavaScriptで直接変換するか、Ajaxを使用するなどして渡してやる必要があります。

JavaScriptを直接書いてしまう方が簡単ですがJSONでのレンダリングをさせた方が速度面で有利なケースが多いです。

Highchartsの詳しい説明については公式Documentを見ていただき、ここでは割愛致します。
早速実装していきましょう。

ライブラリの読み込み

ライブラリをダウンロードするか以下のようにCDNを利用してテンプレートに読み込ませます。

<script src="https://code.highcharts.com/highcharts.src.js"></script>

使用するデータの準備

テスト用に適当なデータを用意します。
Pythonicな方はすでに機械学習用のデータセットを色々持っていると思いますので適当にModelを作成してください。

データセットを持っていない方は機械学習の入門でよく使われるタイタニック号の生存者 + 行方不明者のCSVがここから入手できます。
今回はこのタイタニック号のデータセットを使用していきます。

(Titanic: Machine Learning from Disaster)
https://www.kaggle.com/c/titanic/data

ページの中腹右手に設置されているDownload Allを押下してください。
会員登録後無償で入手できます。
下記の様なModelを用意してみました。

class TitanicPassenger(models.Model):
    name = models.CharField()
    sex = models.CharField()
    survived = models.BooleanField()
    age = models.FloatField()
    fare = models.PositiveSmallIntegerField()
    embarked = models.CharField()

JavaScriptな実装例

まずは公式Documentの方法を踏襲します。

https://www.highcharts.com/docs/getting-started/your-first-

性別ごとの生存者数をQuerySetで取得してテンプレートへ渡します。

Views.py

from django.db.models import Count, Q
from django.shortcuts import render
from .models import TitanicPassenger

def sex_view(request):
    dataset = TitanicPassenger.objects \
        .values('sex') \
        .annotate(survived_count=Count('sex', filter=Q(survived=True)),
                  not_survived_count=Count('sex', filter=Q(survived=False))) \
        .order_by('sex')

    return render(request, 'sex.html', {'dataset': dataset})

sex.html

<div id="container"></div>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>
  Highcharts.chart('container', {
      chart: {
          type: 'column'
      },
      title: {
          text: '男女別の生存者数'
      },
      xAxis: {
          categories: [
            {% for entry in dataset %}'{{ entry.sex }} Class'{% if not forloop.last %}, {% endif %}{% endfor %}
          ]
      },
      series: [{
          name: '生存',
          data: [
            {% for entry in dataset %}{{ entry. entry.survived_count }}{% if not forloop.last %}, {% endif %}{% endfor %}
          ],
          color: 'green'
      }, {
          name: '死亡',
          data: [
            {% for entry in dataset %}{{ entry.not_survived_count }}{% if not forloop.last %}, {% endif %}{% endfor %}
          ],
          color: 'red'
      }]
  });
</script>

いい感じで生成されましたね。

この方法はView内ではバックデータを取ってくるだけでデータ整形などはテンプレート内でJavaScriptを使用して処理をしています。
簡単お手軽に実装できますが、テンプレート側でフォーマットに気を使わないとならずメンテナンス性もあまりよくないのでもう少しデータ処理をView側に渡してみましょう。

views.py

import json
from django.db.models import Count, Q
from django.shortcuts import render
from .models import TitanicPassenger

def sex_view_2(request):
    dataset = TitanicPassenger.objects \
        .values('sex') \
        .annotate(survived_count=Count('sex', filter=Q(survived=True)),
                  not_survived_count=Count('sex', filter=Q(survived=False))) \
        .order_by('sex')

    categories = list()
    survived_series = list()
    not_survived_series = list()

    for entry in dataset:
        categories.append('%s Class' % entry['sex'])
        survived_series.append(entry['survived_count'])
        not_survived_series.append(entry['not_survived_count'])

    return render(request, 'sex_2.html', {
        'categories': json.dumps(categories),
        'survived_series': json.dumps(survived_series),
        'not_survived_series': json.dumps(not_survived_series)
    })

sex_2.html

<div id="container"></div>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>
  Highcharts.chart('container', {
      chart: {
          type: 'column'
      },
      title: {
          text: '男女別の生存者数'
      },
      xAxis: {
          categories: {{ categories|safe }}
      },
      series: [{
          name: '生存',
          data: {{ survived_series }},
          color: 'green'
      }, {
          name: '死亡',
          data: {{ not_survived_series }},
          color: 'red'
      }]
  });
</script>

今度はViewで男女別のリストを生成し値を追加してからJSONにしました。
テンプレート側では渡されたデータセットをそのままカテゴリーにするだけです。
ただしまだ注意点があります。

Laravelなどと同じ様にDjangoテンプレートにもデフォルトでXSS対策として自動エスケープが備わっています。
カテゴリーにセットする名称次第ではこのエスケープが働いてしまう可能性があるので最後にデータセットの整形を完全にView側へ渡してみましょう。

views.py

import json
from django.db.models import Count, Q
from django.shortcuts import render
from .models import TitanicPassenger

def sex_view_3(request):
    dataset = TitanicPassenger.objects \
        .values('sex') \
        .annotate(survived_count=Count('sex', filter=Q(survived=True)),
                  not_survived_count=Count('sex', filter=Q(survived=False))) \
        .order_by('sex')

    categories = list()
    survived_series_data = list()
    not_survived_series_data = list()

    for entry in dataset:
        categories.append('%s Class' % entry['sex'])
        survived_series_data.append(entry['survived_count'])
        not_survived_series_data.append(entry['not_survived_count'])

    survived_series = {
        'name': '生存',
        'data': survived_series_data,
        'color': 'green'
    }

    not_survived_series = {
        'name': '死亡',
        'data': not_survived_series_data,
        'color': 'red'
    }

    chart = {
        'chart': {'type': 'column'},
        'title': {'text': '男女別の生存者数'},
        'xAxis': {'categories': categories},
        'series': [survived_series, not_survived_series]
    }

    dump = json.dumps(chart)

    return render(request, 'sex_3.html', {'chart': dump})

sex_3.html

<div id="container"></div>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>
  Highcharts.chart('container', {{ chart|safe }});
</script>

これでスッキリしました。チャートのフォーマット生成を完全にバックエンド側で処理しています。

{{ chart|safe }}

という記述がありますが、これはDjangoテンプレートの自動エスケープを部分的に無効にしています。

Ajaxな実装例

最後にAjaxを使用しチャートをレンダリングさせてみましょう。
Highchartsだけでなく様々なJavaScriptライブラリでこちらの手法の方が速度面で優れていることが多いです。

今回はJsonResponseを返すためにルートを2つ作成します。

urls.py

from django.urls import path

from titanic_passengers import views

urlpatterns = [
    path('ajax/', views.ajax, name='ajax'),
    path('ajax/data/', views.chart_data, name='chart_data'),
]

Views.py

def ajax(request):
    return render(request, 'ajax.html')

def chart_data(request):
    dataset = TitanicPassenger.objects \
        .values('embarked') \
        .exclude(embarked='') \
        .annotate(total=Count('embarked')) \
        .order_by('embarked')

    port_display_name = dict()
    for port_tuple in TitanicPassenger.PORT_CHOICES:
        port_display_name[port_tuple[0]] = port_tuple[1]

    chart = {
        'chart': {'type': 'pie'},
        'title': {'text': ''港別乗船員数"},
        'series': [{
            'name': '人数',
            'data': list(map(lambda row: {'name': port_display_name[row['embarked']], 'y': row['total']}, dataset))
        }]
    }

    return JsonResponse(chart)

港名ごとの人数をmapで取得しJsonResponseでテンプレートへ渡しました。
先ほどまでの棒グラフから円グラフへchart typeを変更しています。

ajax.html

<div id="container" data-url="{% url 'chart_data' %}"></div>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
  $.ajax({
    url: $("#container").attr("data-url"),
    dataType: 'json',
    success: function (data) {
      Highcharts.chart("container", data)
    }
  });
</script>

ajaxからchart_dataを呼び出し、データセットをJsonResponseで返します。
テンプレート側で値を受け取るとsuccess内でレンダリングされます。
シンプルですね。

結果はこんな感じで表示されます。

結論

実装方法としてはHighchartsに限らずCharts.jsなど他のチャートライブラリでも同じ様な概念で対応できます。

またDjangoテンプレート内にJavaScriptを書き込むのはなるべく避けた方が良いです。
例えば新しいバージョンのJavaScriptであればJsonフォーマット内のカンマの数が仮に多かったとしても許容してくれますが、古いバージョンではエラーを返します。
つまりブラウザによって挙動が変わってしまう可能性があるので要注意です。

今回のコードはGitHubにもアップしてあります。
https://github.com/mila411/django-highcharts

2019-08-01Python,TIPSDjango,JavaScript

Posted by Kenny