D3.jsのWord CloudにAjaxを実装する on Django


# 概要
クリックすると変化するWord Cloudを作った↓
![ajax_word_cloud.gif](https://qiita-image-store.s3.amazonaws.com/0/199902/97c09c1e-505d-01fc-65c3-167b438ed156.gif)

こちらのWebサービスで実際に触れます
[【Where to Visit ?】](https://gae2.fk-management.com/photo/)

上記のWebサービスについては以下記事参照↓
[GAE×DjangoでWebサービスを開発してみた](https://qiita.com/tfuruya/items/f935b37e4007ea890574)

## 使った技術
- D3.js
 - [d3-cloud](https://github.com/jasondavies/d3-cloud)を利用して、WordCloudを作成した
- Ajax
 - ページ遷移なしにWordCloudを変化させる 
- Python3, Django
 - バックエンド 


# 説明
## python, Django
DjangoをWebサービスのバックエンドとして利用している。
Ajaxも、基本的にはViewと同じで、word cloudのデータの受け渡しをする。
AjaxはJSONで送るので、datetime型はstrにして渡すなど、取扱に注意が必要。

```py
# view用のメッソド
def photo(request):
    # 省略
    # ...
    wordcloud_list = [
        {
            "word": "日本",  # 表示される文字
            "property": "country",  # 種別
            "count": 10,  # prefecture=word
        },
        {
            "word": "日本",  
            "property": "country",
            "count": 10,  
        },
        #...
    ]
    output = {
        "wordcloud_list": wordcloud_list,
    }
    return TemplateResponse(request, "ra/photo.html", output)

# ajax用のメソッド
def ajax(request):
    if request.method == "GET":
        # 省略
        # datetimeはstrにして渡す
        # ...
        # response in JSON
        res_data = {
            "wordcloud_list": wordcloud_list,
        }
        response = json.dumps(res_data)  # JSON形式に直して・・
        return HttpResponse(response, content_type="text/javascript")  
    else:
        raise Http404
```


## HTML・javascript
view関数やajax用関数から受け取ったデータをもとに、D3.jsでWordCloudを描写する。
各WordはonclickをトリガーとしてWordCloudを描写する関数を再帰関数的に呼び出すようにしている。

```html
<!-- 該当部分以外省略しています -->
<head>
  <!-- Spinner -->
  <link rel="stylesheet" href="{% static 'css/loading.css' %}">
</head>

<!--Spinner-->
<div id="overlay" class="hide">
  <div class="cv-spinner">
    <span class="spinner"></span>
  </div>
</div>

<!--WordCloud-->
<div class="row">
  <div class="col-12">
    <div class="collapse show" id="collapseExample">
      <div class="card">
        <div id="wordcloud"></div>
      </div>
    </div>
  </div>
</div>

<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<!-- 以下URL参照 -->
<!-- https://github.com/jasondavies/d3-cloud-->
<script src="{% static 'js/d3.layout.cloud.js' %}"></script>
<script>
var TARGET_ELEMENT_ID = '#wordcloud'; // 描画先

// Word Cloudを作成して、id="wordcloud"の要素に描写する関数
function draw_wc(data){
  var random = d3.randomIrwinHall(2); // アーウィンホール分布
  var countMax = d3.max(data, function(d){ return d.count} );
  var sizeScale = d3.scaleLinear().domain([0, countMax]).range([10, 100])
  var words = data.map(function(d) {
    return {
    text: d.word,
    property: d.property,
    size: sizeScale(d.count) //頻出カウントを文字サイズに反映
    };
  });
  d3.layout.cloud().size([w, h])
    .words(words)
    .rotate(function() { return (~~(Math.random() * 6) - 3) * 30; })
    .font("Impact")
    .fontSize(function(d) { return d.size; })
    .on("end", draw) //描画関数の読み込み
    .start();
  return words;

  // wordcloud 描画実行部分
  function draw(words) {
    d3.select(TARGET_ELEMENT_ID)
      .append("svg")
        .attr("class", "ui fluid image") // style using semantic ui
        .attr("viewBox", "0 0 " + w + " " + h )  // ViewBox : x, y, width, height
        .attr("width", "100%")    // 表示サイズの設定
        .attr("height", "100%")   // 表示サイズの設定
      .append("g")
        .attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
      .selectAll("text")
        .data(words)
      .enter().append("text")
        .style("font-size", function(d) { return d.size + "px"; })
        .style("font-family", "Impact")
        .style("fill", function(d, i) { return d3.schemeCategory10[i % 10]; })
        .attr("text-anchor", "middle")
        .attr("transform", function(d) {
          return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
        })
        .text(function(d) { return d.text; })
        // onclickをトリガーにでAjaxでWord Cloudを更新する
        .on("click", function (d, i){
          // Ajax
          $.ajax({
            url: "{% url 'ra:ajax' %}", //ajaxのurlはDjangoのurls.pyから取得
            method: "GET",
            data: {
              text: d.text,
              prop: d.property,
            },
            timeout: 10000,
            dataType: "json",
            //リクエストが完了するまで実行される
            beforeSend: function(){
              $('#overlay').removeClass('hide');
            }
          })
          .done(function(data_ajax) {
            // 写真・モーダルの更新
            // 省略
            // ...
            // 既存Word Cloudを削除
            d3.select(TARGET_ELEMENT_ID).select('svg').remove();
            // 再帰関数的に呼び出して、再描写する
            draw_wc(data_ajax.wordcloud_list);
            // Spinner終了
            $('#overlay').addClass('hide');
          })
        });
  }
}

// 初回実行はDjangoから受け取ったデータを引数に、関数draw_wcを呼び出し
var data_list = [
  {% for td in wordcloud_list %}
  {
    "word": "{{td.word}}",
    "count": {{td.count}},
    "property": "{{td.property}}",
  },
{% endfor %}
]
words = draw_wc(data_list)
</script>
```






## CSS
Ajax実行中はグルグル回るスピナーを表示させたかったので、CSSもを利用して実装した。
参考：https://pinkmonky.net/detail/?id=100

```css:loading.css
.hide {
  display: none;
}

#overlay{
    position: fixed;
    top: 0;
    margin: 0;
    padding: 0;
    z-index: 100;
    width: 100%;
    height:100%;
    background: rgba(0,0,0,0.6);
}
.cv-spinner {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
.spinner {
    width: 40px;
    height: 40px;
    border: 4px #ddd solid;
    border-top: 4px #2e93e6 solid;
    border-radius: 50%;
    animation: sp-anime 0.8s infinite linear;
}
@keyframes sp-anime {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(359deg);
    }
}
.is-hide{
    display:none;
}
```

# 困ったこと（解決できていない）
## データ数が少ないと正しく表示されない
どうもデータ数が少ないと、一部データが欠落して、全件正しく表示されないようだ。
→暫定対策として、同じデータを重複させることでデータ数を無理やり増やした。。。見栄えは良くない。。。

# ちなみに
今回の[Webサービス](https://gae2.fk-management.com)では、WordCloudだけではなく、写真などの入れ替えもAjaxで実装しています。
そのへんも今後記事にしていきたい。