LoginSignup
17
19

More than 5 years have passed since last update.

Django(D3.js)データビジュアライズことはじめ

Last updated at Posted at 2018-06-19

はじめに

さて、Python(Django + Anaconda)が、MySQLからデータを抜いて、テンプレートに渡して、htmlがテーブルとして表示する、というところまでできました!感動!:joy:
LAMP+Wordpress+Python(Django)のセットアップメモ2018

つぎは、データを絵にする工程が必要だ。ここで出てくるのが d3.js。
いっきにリファレンス見ながらやろうとするとなにが書いてあるかちっともわかんないよね。
ぐえぇぇ...!とかなってくるわ。は?なに書いてあるの?これ、みたいな。
Radar Chart d3 v4

なるほどね、「描く」処理を書かないといけないのね...。グラフでポン出しできないのね...。「おーい、なかじまー!C++でペイント.exe 作ろうぜー!」って言ってるようなもんだよなぁ...。でもほかのチャートライブラリってどこかダサいので d3 にたどり着いちゃうのよね...。あ~ん、どうしよう。jsファイルにギュッと隠蔽して見えないことにしてええんやろうか...。

いったん描画の基本をなんとなく押さえたうえで、
d3.js超初心者向け①→②を表現してみる

これをできるだけ簡略にしながら攻略していこう。
Radar Chart d3 v4

とりあえずコピペして置いちゃえ

わかんねーときはコピペだよなぁ~?:kissing_heart:
とりあえずtemplateフォルダに両方置いてみて動くのかな?ふつーに考えると、htmlがカレントディレクトリにおいたjsを読んでくれそうだよね。
image.png

CentOS(/home/op/app01/public_html/pj1)
$ tree
.
┝ app1
│ ┝ __init__.py
│ ┝ __pycache__
│ ┝ admin.py
│ ┝ apps.py
│ ┝ migrations
│ ┝ models.py
│ ┝ static
│ ┝ templates
│ │ ┝ index.html      #New
│ │ ┝ radarChart.js   #New
│ │ └ index.html.org  #いままでのやつはいったん退避
│ ┝ tests
│ │ ┝ __init__.py
│ │ ┝ __pycache__
│ │ └ tests.py
│ ┝ urls.py
│ └ views.py
┝ manage.py
└ pj1
    ┝ __init__.py
    ┝ __pycache__
    ┝ settings.py
    ┝ urls.py
    └ wsgi.py
index.html
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/ >
        <title>Smoothed Radar Chart</title>

        <!-- Google fonts -->
        <link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'>
        <link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>

        <!-- D3.js -->
        <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
        <script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script>
        <script src="radarChart.js" charset="utf-8"></script>
        <style>
            body {
                font-family: 'Open Sans', sans-serif;
                font-size: 11px;
                font-weight: 300;
                fill: #242424;
                text-align: center;
                text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
                cursor: default;
            }

            .legend {
                font-family: 'Raleway', sans-serif;
                fill: #333333;
            }
        </style>
    </head>
    <body>
        <div class="radarChart" style="display: inline-flex;"></div>
        <div class="radarChart2" style="display: inline-flex;"></div>
        <script>
            //////////////////////////////////////////////////////////////
            //////////////////////// Set-Up //////////////////////////////
            //////////////////////////////////////////////////////////////

            var margin = { top: 50, right: 80, bottom: 50, left: 80 },
                width = Math.min(700, window.innerWidth / 4) - margin.left - margin.right,
                height = Math.min(width, window.innerHeight - margin.top - margin.bottom);

            //////////////////////////////////////////////////////////////
            ////////////////////////// Data //////////////////////////////
            //////////////////////////////////////////////////////////////

            var data = [
                { name: 'Allocated budget',
                    axes: [
                        {axis: 'Sales', value: 42},
                        {axis: 'Marketing', value: 20},
                        {axis: 'Development', value: 60},
                        {axis: 'Customer Support', value: 26},
                        {axis: 'Information Technology', value: 35},
                        {axis: 'Administration', value: 20}
                    ]
                },
                { name: 'Actual Spending',
                    axes: [
                        {axis: 'Sales', value: 50},
                        {axis: 'Marketing', value: 45},
                        {axis: 'Development', value: 20},
                        {axis: 'Customer Support', value: 20},
                        {axis: 'Information Technology', value: 25},
                        {axis: 'Administration', value: 23}
                    ]
                }
            ];

            //////////////////////////////////////////////////////////////
            ////// First example /////////////////////////////////////////
            ///// (not so much options) //////////////////////////////////
            //////////////////////////////////////////////////////////////
            var radarChartOptions = {
              w: 290,
              h: 350,
              margin: margin,
              levels: 5,
              roundStrokes: true,
                color: d3.scaleOrdinal().range(["#26AF32", "#762712"]),
                format: '.0f'
            };

            // Draw the chart, get a reference the created svg element :
            let svg_radar1 = RadarChart(".radarChart", data, radarChartOptions);

            //////////////////////////////////////////////////////////////
            ///// Second example /////////////////////////////////////////
            ///// Chart legend, custom color, custom unit, etc. //////////
            //////////////////////////////////////////////////////////////
            var radarChartOptions2 = {
              w: 290,
              h: 350,
              margin: margin,
              maxValue: 60,
              levels: 6,
              roundStrokes: false,
              color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]),
                format: '.0f',
                legend: { title: 'Organization XYZ', translateX: 100, translateY: 40 },
                unit: '$'
            };

            // Draw the chart, get a reference the created svg element :
            let svg_radar2 = RadarChart(".radarChart2", data, radarChartOptions2);
        </script>
    </body>
</html>

動かない

んー、いろいろ考えたけどsourceはあってるとしよう(そこを疑うとなにもできん)ってことは、jsへのパスが悪いんだよな?ってことで試行錯誤したら下記2か所の修正の必要があった。

1.(Djangoのパス指定文化で)jsへのパスを指定する。
2.Djangoのstaticキーワードを使えるようにする
3.js ファイルの static/js フォルダへの移動

解決方法はここに書いてあった。ここわかりやすいなぁ
Python Django チュートリアル(6) ~htmlデザイン~

1.index.html(読み込み場所を変更。ソースを「radarChart.js」で検索)
<script src="{% static 'js/radarChart.js' %}" charset="utf-8"></script>
2.index.html(2行目に新規追加)
{% load staticfiles %}
3.CentOS(jsファイルの移動)
$ tree
.
┝ app1
│ ┝ __init__.py
│ ┝ __pycache__
│ ┝ admin.py
│ ┝ apps.py
│ ┝ migrations
│ ┝ models.py
│ ┝ static
│ │ └ js
│ │     └ radarChart.js  #move
│ ┝ templates
│ │ ┝ index.html
│ │ └ index.html.org
│ ┝ tests
│ │ ┝ __init__.py
│ │ └ tests.py
│ ┝ urls.py
│ └ views.py
┝ manage.py
└ pj1
    ┝ __init__.py
    ┝ __pycache__
    ┝ settings.py
    ┝ urls.py
    └ wsgi.py

とりあえず動いた!

image.png

勘所としては、RadarChartが呼ばれると「.radarChart」クラスをグラフが置き換える。という流れのようだ。
image.png

「.radarChart」クラスはここにある(radarChart2も同じように置き換えられる)。
image.png

よろしい。「radarChart.js」キミはブラックボックスだ:relaxed:

HTMLファイルの合成

グラフサンプルhtmlが動いたのを確認したところで、今度は最初に作った「index.htm.org」と、グラフサンプルhtmlを合成しよう。

headの理解

index.html.orgの気になるところ
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">

charset

これは文字コードだよな

<meta charset="utf-8">

http-equiv

edge?俺はchromeだ!とかいう宗教戦争は置いといて、、。AをBに置き換えていいかが知りたい。なんとなく一緒だもんね。

A)index.html.org
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
B)index.html
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/ >

meta要素のhttp-equiv属性で動作を指示しよう
要素は「http-equiv属性」が指定されると プラグマ指示子(pragma directive)となります。プラグマ指示子とは、ブラウザに対して HTML文書の「状態・動作」を指示するもの。
例えば「http-equiv="refresh"」で時限リダイレクトとして動作します。
OK. そういう仕込みの場所ね。

X-UA-Compatible(IE のレンダリング方式を 標準モードに)
IEの互換表示設定の指示です。「content属性」の値を「IE=Edge」とすることで、IE がそのバージョンの標準モードでレンダリングするよう促します。CSSを読み込んでいる link要素、Javascriptを読み込んでいる script要素より前に書く必要があります。

<meta http-equiv="X-UA-Compatible" content="IE=Edge">

content-type(文字エンコーディング)
HTML5での文字エンコーディングの指定は 要素の「charset属性」を使います。この「http-equiv属性」を使う方法は、その代替でしかありません。「charset属性」の文字エンコーディング指定と一緒に使ってはいけません(どっちか1つ)。

<meta http-equiv="content-type" content="text/html; charset=UTF-8">

結論:AはBに置き換えられそう!

Viewport

もう逃げない。HTMLのviewportをちゃんと理解する
スマホでの映り方ってことか。

<meta name="viewport" content="width=device-width, initial-scale=1">

結論:横幅倍率をデバイス依存の倍率1にする、なのでそのまま放置

stylesheet

Bootstrap4移行ガイド
コードにはCDNからのクロスサイトスクリプティング(XSS)を防ぐため integrity 属性と crossorigin 属性を追加する。
:thinking:要はセキュリティ上必要ってことか。ってことは、ここは見えないものとしたほうが理解が早そうだ。英語の勉強のときも余計な修飾語は抜いて考えるでしょう?以下のようにすると、CDNでスタイルシート呼んでるだけだから視認性もいいね。

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
CentOS(/home/op/app01/public_html)
$ mv pj1/app1/templates/index.html.org pj1/app1/templates/index.html
$ systemctl restart httpd 

Yes!:relaxed:
image.png

んん??文字がすっげぇ重なってるぞ??:thinking:
そういえば「Hello, Bootstrap world!」も勝手に真ん中にいったし...
image.png

お前か~!Bodyに対してエフェクトかかってるね。これは消そう。「.legend」はグラフの「汎用」の文字のハズなのでいまは消さなくてもいいけどBodyのやつはまるごとイラネ。
image.png

グラフも左に寄ったけど、まぁあとにしよう。
image.png

データバインディング

SQL組み立て

CentOS(/home/op/app01/public_html)
$ vi pj1/app1/views.py

1.ナマSQLの場合

僕はこちらのほうが経験値があるので細かい説明は省いちゃうけど
・MySQLへのコネクションを確立して
・カーソルを取得して
・SQLを作って
・抽出(fetchall)して
・カーソルを手離して
・MySQLへのコネクションを手放す

views.py(SELECE対象テーブルをグラフ用に作ったテーブルに変更&SQL組み立て)
from django.shortcuts import render
import mysql.connector

def index(request):
  conn = mysql.connector.connect(user='python', password='python123', database='pythondb')
  cur = conn.cursor()
  cur.execute("SELECT DATE_FORMAT(ym, '%Y%m') AS YM, field_homogeneity AS homogeneity, plowing, biological, chemical, soil_hardness_distribution AS hardness FROM w_evaluation WHERE (SELECT MAX(ym) FROM w_evaluation);") #ここ
  data = cur.fetchall()
  colname = cur.column_names
  cur.close
  conn.close

  return render(
    request=request,
    template_name='index.html',
    context={'recordset': data, 'colname': colname})

2.ORMの場合(推奨)

Object-relational mapping(オーアールマッパー)を使う場合は
setting.pyに接続情報を書いて、models.pyにテーブル定義を書いているので、ここではSQLを指定するだけだが、これも メソッドチェーン でいける

views.py
from django.shortcuts import render
from .models import evaluation

def index(request):
    #SELECT
    qry = evaluation.objects.order_by('-ym')[:1].values('ym','homogeneity','plowing','biological','chemical','hardness')
    return render(request, 'index.html', {'rs': qry, 'cs':qry[0].keys()})

テンプレートの編集

CentOS(/home/op/app01/public_html)
$ vi pj1/app1/templates/index.html
index.html(テーブルのとこだけリファクタリングした)
<!-- csとrsは views.py から来たんだよ -->
<thead>
  <tr>
  {% for c in cs %}
    <td>{{ c }}</td>
  {% endfor %}
  </tr>
</thead>
<tbody>
  <tr>
  {% for r in rs %}
    <td>{{ r.ym }}</td>
    <td>{{ r.homogeneity }}</td>
    <td>{{ r.plowing }}</td>
    <td>{{ r.biological }}</td>
    <td>{{ r.chemical }}</td>
    <td>{{ r.hardness }}</td>
  {% endfor %}
  </tr>
</tbody>

rsとcsのデータの持ち方の違い

なんでrs(データ部)とcs(項目名部)でデータの持ち方違うのよ?一緒のデータ構造にしろよって思った人は鋭い!これは、index.htmlにおいてd3連携するときに、1つずつ値を取り出す必要があって、qry[0].values()で取り出すよりもデータの取り回しが良いのだ。改善の余地ありかも。
テンプレート言語で変数の代入はどうやって記述するのでしょうか?
ここ見ても、意図的にできないようになってるみたいだし。

jsの配列操作

キモはこれだよなぁ~。これってjsonじゃないの?連想配列?
JSONとJavaScriptオブジェクトの違い
javascriptの連想配列と配列の違い
あぁ、JSONと連想配列は同じなのか(同じというよりは親クラス子クラスの関係)。要は配列。連想配列は結構ゆるいsyntaxでいいけどJSONはsyntaxにシバりがある。ってことはこれは連想配列なのね
image.png
くっそハマったんだけど、ひょっとしてブラックボックスにした js に配列の読み方書いてあるんじゃね?ってエウレカがきて見てみたら書いてあった!!

radarChart.js(該当部分のみ抜き出し)
maxValue = data[j].axes[i]['value'];
index.html(結局こういう代入のしかたになる。慣れたら配列作成時の直接代入でもいいかも)
// Djangoからの代入
data[1].axes[0].value = parseFloat("{% for r in rs %}{{ r.homogeneity }}{% endfor %}");
data[1].axes[1].value = parseFloat("{% for r in rs %}{{ r.plowing }}{% endfor %}");
data[1].axes[2].value = parseFloat("{% for r in rs %}{{ r.biological }}{% endfor %}");
data[1].axes[3].value = parseFloat("{% for r in rs %}{{ r.chemical }}{% endfor %}");
data[1].axes[4].value = parseFloat("{% for r in rs %}{{ r.hardness }}{% endfor %}");
CentOS
$ systemctl restart httpd 

きたぁああああああ!苦労したぜぇ~まったくよー:heart_eyes:
image.png

これでデータをビジュアル化できた。

ソース(フル)

index.html
<!DOCTYPE html>
{% load staticfiles %}
<html lang="ja">

  <head>
    <!-- Stylesheet and meta necessary for Bootstrap -->
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">

    <!-- Google fonts -->
    <link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'>
    <link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>

    <!-- D3.js -->
    <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
    <script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script>
    <script src="{% static 'js/radarChart.js' %}" charset="utf-8"></script>
    <style>
        .legend {
            font-family: 'Raleway', sans-serif;
            fill: #333333;
        }
    </style>
    <title>Anaconda!</title>

  </head>

  <body>

    <h1>Hello, Bootstrap world!</h1>
    <table class="table table-dark table-hover">
      <thead>
        <tr>
        {% for c in cs %}
          <td>{{ c }}</td>
        {% endfor %}
        </tr>
      </thead>
      <tbody>
        <tr>
        {% for r in rs %}
          <td>{{ r.ym }}</td>
          <td>{{ r.homogeneity }}</td>
          <td>{{ r.plowing }}</td>
          <td>{{ r.biological }}</td>
          <td>{{ r.chemical }}</td>
          <td>{{ r.hardness }}</td>
        {% endfor %}
        </tr>
      </tbody>
    </table>

   <div class="radarChart" style="display: inline-flex;"></div>
    <script>
        //////////////////////////////////////////////////////////////
        //////////////////////// Set-Up //////////////////////////////
        //////////////////////////////////////////////////////////////
        var margin = { top: 50, right: 80, bottom: 50, left: 80 },
        width = Math.min(700, window.innerWidth / 4) - margin.left - margin.righ
t,
        height = Math.min(width, window.innerHeight - margin.top - margin.bottom);

        //////////////////////////////////////////////////////////////
        ////////////////////////// Data //////////////////////////////
        //////////////////////////////////////////////////////////////
        var data = [
        { name: 'Mean',
            axes: [
            {axis: 'homogeneity', value: 40},
            {axis: 'plowing', value: 40},
            {axis: 'biological', value: 40},
            {axis: 'chemical', value: 40},
            {axis: 'hardness', value: 40}
            ]
        },
        { name: 'You',
            axes: [
            {axis: 'homogeneity', value: 0},
            {axis: 'chemical', value: 0},
            {axis: 'hardness', value: 0}
            ]
        }
        ];

        // Djangoからの代入
        data[1].axes[0].value = parseFloat("{% for r in rs %}{{ r.homogeneity }}{% endfor %}");
        data[1].axes[1].value = parseFloat("{% for r in rs %}{{ r.plowing }}{% endfor %}");
        data[1].axes[2].value = parseFloat("{% for r in rs %}{{ r.biological }}{% endfor %}");
        data[1].axes[3].value = parseFloat("{% for r in rs %}{{ r.chemical }}{% endfor %}");
        data[1].axes[4].value = parseFloat("{% for r in rs %}{{ r.hardness }}{% endfor %}");

        var radarChartOptions = {
          w: 290,
          h: 350,
          margin: margin,
          maxValue: 60,
          levels: 6,
          roundStrokes: false,
          color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]),
        format: '.0f',
        legend: { title: 'TOTAL EVALUATION', translateX: 100, translateY: 40 }
        };
        RadarChart(".radarChart", data, radarChartOptions);

    </script>
    <!-- jQuery necessary for Bootstrap -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
  </body>
</html>
17
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19