LoginSignup
7
5

More than 5 years have passed since last update.

D3.jsを用いてUSの州昇格を時系列でグラフ化してみた

Last updated at Posted at 2017-05-08

はじめに

私は以前からWEB上のグラフはもっとインタラクティブであるべきだと考えています。理由は、3つあります。

1つ目は2次元の枠内の制限があること。
2つ目は時間軸の導入が可能なこと。
3つ目はマウスオーバーなどのユーザのアクションを導入できること。

1つ目の理由は、3次元のグラフを導入できなくもないのですが、奥行きのあるグラフは見づらくなるため、どうしても2次元のディスプレイの制約から逃れるのは容易ではないと考えています。

2つ目と3つ目はWEBなのですから動作させることもできますし、マウスでユーザの操作も可能です。率先して動かさない理由はないはずです。

このような理由により私はユーザ操作可能なWEB上のグラフはもっとインタラクティブであるべきではないかと思っていました。

じゃ、作れよとなりますが、けど作りたいものがなかったのですが、最近AAで学ぶ南北戦争への道を読んで(まだ読み切っていない)意外に独立戦争から南北戦争までのアメリカの歴史って面白いものだなと思い、じゃ、州昇格のタイミングがわかるものをマップ(グラフ)を作成したら案外面白いのではないかと思い作成してみました。

準備

グラフ化するにあたり、以下を考えました。

  • D3.jsを使ってマップ表記する
  • 準州と州昇格を記載する
  • 各種イベントを表記する

D3.jsは説明するほどではなく多様なグラフを行うためのライブラリです。公開されているデータの中にUSのマップもあるためそれを使用します。

USの各州の昇格年準州への登録などはwikipediaを調べました。

ただし、準州と州は同じ領域ではないケースもあり、且つマップのデータが現在の州のデータのため、準州を正確に表現することはできません。

また、既存の州から分離した場合は、元の州が昇格した年に青色を塗り、分離した年に州名を表示するようにしました。この辺りは州に昇格した地域がどこにあたるのか知りたかったためです(個人的な理由)。

さらに、アメリカは戦争が多い国の一つですし、時系列データにはその背景となるイベントがあるものだと私は考えています。wikipediaを調べ、各年ごとに大きなイベント(対外戦争や経済的イベント)なども一緒に掲載しました。この辺りは私はもっとグラフに載せるべきだと考えている派(後述)です。この辺りは素人なのと稼働からあまり正しく入れられたとは思えていません。

イメージ

実際に動くものをUS州昇格マップに置きました。実際に動かしてみてください。

usmap.png

Startボタンを押すと1765年から始まって1959年にアラスカとハワイの州昇格の年までです。Stopボタンで停止、Initボタンを押すと最初から始められます。1年が1.5秒間隔で更新されます。

ソース

ソースはus_map_statesに置いてあります。これをgit cloneしてindex.htmlを開いていただければ動きます。

ここから少し解説を。

まずはusのマップデータを読み込みます。

データ読み込み

d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
  if (error) throw error;
  g_us = us;
   省略
  //us map描画
  draw_us_map(false);
  // start/stopボタンのdisablesを解除
  var element_init = document.getElementById('button_main');
  element_init.disabled = false;

});

次に描画開始を行います。念のため、無限ループ防止を入れておきます。私はトップにループ防止を入れるのが好きです。理由は途中でいろいろと変更を行ってもここに入れておけばとりあえず無限ループを防止できると考えているためです。

描画開始

// flag: ループの有無
function draw_us_map(flag) {
  // 描画終了判定
  if (sleep_count >= sleep_max_count) {
  // 描画終了したのでボタンなどの整理
    var element_init = document.getElementById('button_init');
    element_init.disabled = false;
    var element_main = document.getElementById('button_main');
    element_main.disabled = true;
    return;
  }
  // 毎回描画前に無駄に消しています(あほです)...ここは変更該当の箇所だけで十分なのに...
  svg.selectAll('g').remove();
  svg.selectAll('text').remove();
  svg.selectAll('path').remove();

州の領域の描画箇所。よくよく考えると州昇格時の情報はここでとらなくてもいい気がしてきた。取得したデータに、州名や昇格年など追加したほうがデータ管理が容易だったかもしれない(データ構造を整理してから実行すべきでした)。D3.jsの箇所は理解せずにコピペしています。

// 昇格時に表示するテキストデータの初期化
var text_array = [];
// 州領域描画
svg.append("g")
  .selectAll("path")
  .data(topojson.feature(g_us, g_us.objects.states).features)
  .enter().append("path")
    .attr("d", path)
    .style("fill", function(d) {
      for (var i = 0; i < state_info_array.length; i++) {
      // 州昇格時の州名と位置の取得
      // 表示位置は描画pathの一番最初のx,yを使用
      // 分離を考慮
      if (d.id == state_info_array[i].id) {
        if (state_info_array[i].separation == null &&
          state_info_array[i].state == Math.floor(sleep_count / year_bin) + start_year) {
        text_array.push({name: state_info_array[i].name, x: d.geometry.coordinates[0][0][0][0], y:d.geometry.coordinates[0][0][0][1]});
        }
        if (state_info_array[i].separation != null &&
          state_info_array[i].separation == Math.floor(sleep_count / year_bin) + start_year) {
        text_array.push({name: state_info_array[i].name, x: d.geometry.coordinates[0][0][0][0], y:d.geometry.coordinates[0][0][0][1]});
        }

        // ここで州、準州、それ以外で色を塗り分けています
        if (state_info_array[i].state <=  Math.floor(sleep_count / year_bin) + start_year) {
          return "#00f";
        } else if (state_info_array[i].territory != null &&
             state_info_array[i].territory <=  Math.floor(sleep_count / year_bin) + start_year) {
          return "#aaf";
        } else {
          return "#ccc";
        }
      }
      }
    })
    // tooltipの表示。jqueryを使用。
    .on("mousemove", function(d) {
      var html = "";
      var state_name = '';
      var state_year = '';
      for (var i = 0; i < state_info_array.length; i++) {
        if (d.id == state_info_array[i].id) {
        state_name = state_info_array[i].name;
        if (state_info_array[i].separation == null) {
          state_year = state_info_array[i].state;
        } else {
          state_year = state_info_array[i].separation;
        }

        break;
        }
      }
      // 州名と昇格年の実
      html += "<div class=\"tooltip_key\">";
      html += "<span class=\"tooltip_key\">";
      html += state_name;
      html += "</span>";
      html += "<span class=\"tooltip_value\">";
      html += state_year;
      html += "";
      html += "</span>";
      html += "</div>";

      $("#tooltip-container").html(html);
      $(this).attr("fill-opacity", "0.8");
      $("#tooltip-container").show();

      var coordinates = d3.mouse(this);

      var map_width = 960;
      if (d3.event.layerX < map_width / 2) {
        d3.select("#tooltip-container")
        .style("top", (d3.event.layerY + 15) + "px")
        .style("left", (d3.event.layerX + 15) + "px");
      } else {
        var tooltip_width = $("#tooltip-container").width();
        d3.select("#tooltip-container")
        .style("top", (d3.event.layerY + 15) + "px")
        .style("left", (d3.event.layerX - tooltip_width - 30) + "px");
      }
    })
    .on("mouseout", function() {
        $(this).attr("fill-opacity", "1.0");
        $("#tooltip-container").hide();
      });

州の境目の記載。ここはコピペです。

  svg.append("path")
    .attr("class", "state-borders")
    .attr("d", path(topojson.mesh(g_us, g_us.objects.states, function(a, b) { return a !== b; })));

先ほど登録した州昇格時の州名表示をここで行う。


  if (text_array.length != 0) {
    for (var i = 0; i < text_array.length; i++) {
      svg.append("text")
      .attr("x", text_array[i].x)
      .attr("y", text_array[i].y)
      .attr("fill", "#000")
      .attr("text-anchor", "start")
      .attr("font-weight", "bold")
      .text(text_array[i].name);
    }
  }

最後にトップに今の年とその年の重要なイベントを記載。


  var top_text = '';
  for (var i = 0; i < event_array.length; i++) {
  if (event_array[i].year == Math.floor(sleep_count / year_bin) + start_year) {
      top_text += event_array[i].event;
    }
  }
  svg.append("text")
  .attr("x", 350)
  .attr("y", 30)
  .attr("fill", "#000")
  .attr("font-size", "24pt")
  .attr("text-anchor", "start")
  .attr("font-weight", "bold")
  .text((Math.floor(sleep_count / year_bin) + start_year) + '' + top_text);

本当の最後にアニメーションのためにループします。requestAnimationFrameの残骸が残っていますが、やった後に後悔しました。


  sleep_count += 1;
  if (flag == true) {
    //requestID = requestAnimationFrame(draw_us_map);
    requestID = setTimeout(draw_us_map, timeout, true); 
  }

us mapデータだけでは州の昇格年や年のイベントなどがありません。この2つのデータを手で作成して組み込んであります。

以下が州の昇格年や州名などのデータです。


// state info
// state: 州昇格年もしくは分離前に所属していた州が昇格した年
// territory: 準州設定年
// separation: 分離した年・
// id: us mapのid
var state_info_array = [
   {name: 'デラウェア州', state:1787, territory:null, separation: null, id: '10'}
  ,{name: 'ペンシルベニア州', state:1787, territory:null, separation: null, id: '42'}
  ,{name: 'ニュージャージー州', state:1787, territory:null, separation: null, id: '34'}
  ,{name: 'ジョージア州', state:1788, territory:null, separation: null, id: '13' }
省略

以下が年毎のイベントです。当初は入植からスタートだろ常考と思いましたが、独立戦争まで相当間が空くために泣く泣く1700年代中盤から表示に切り替えました。このため入植開始は表示されません。


// 各種イベント
// year: 発生年
// event: イベント内容
var event_array = [
  {year: 1607, event: ' 入植開始'}
  ,{year: 1767, event: ' タウンゼンド諸法成立'}
  ,{year: 1770, event: ' ボストン虐殺事件'}
  ,{year: 1773, event: ' ボストン茶会事件'}
省略

jsonのデータは別に切り出して配布するのがベストなのかもしれませんが、データが大きくないので一緒にしています。もっとイベントを増やしたり、イベントをマップ上で表示させたり、州昇格時の表示位置などもデータに組み込めばよりいいものができるかもしれません。

作成するにあたり問題

作成するにあたりいくつか問題がありました。

1つ目は、州昇格時に州名を表示させようとしたのですが、表示位置を正確にとることはできませんでした。このため、usの地図のデータの最初に出てくる座標に州名を表示させたのですが、けっこうばらばらで。調整すればもっといけるのかもしれませんが、稼働を考えて諦めました。

2つ目は、https://d3js.org/us-10m.v1.jsonを使っているのですが、このデータの中に州を判別できる情報がidしかありません。そのidもアルファベット順でもなく州昇格の順番でもありませんでしたし、01から始まって56まであり、途中で数字がぬけているのもあります。私にはそのidの法則がわからず一つずつどの州に該当するか地道に調べるしかありませんでした。なんだろう、これ。

3つ目は準州と州の関係です。これは上に書いた通り、準州がそのまま州に昇格するわけではないため、準州の取り扱いは正しくないかもしれません。この辺りどうすべきか悩みましたが、簡易にマップデータなど用意できるわけではありません。このため、準州のデータは正しく表示することはあきらめました。この辺りはマップ系の難しさではないかと思います。

4つ目はイベントの精査です。wikipediaのアメリカの戦争と外交政策を見るとは、戦争しまくりなわけです。特にインディアンとの戦争(?)は準州や州昇格に関連性が大いにあるのですが、全部コピペするのが面倒なのでやめました。もし修正してみたい方はgit cloneして修正してみてください。

反省

当初はD3.jsを用いてベンチマークっぽいものをついでに作成できないかと思いrequestAnimationFrameを使用してアニメーションを行ってみたのですが、svgで大量に描いているためか2コア4スレッドのThinkPadにも拘わらず常に60%もCPUを使用するただただ重いマップ表示スクリプトになってしまいました。

これではさすがにアウトだと思いsetTimeoutで1.5s周期で切り替えました。ケースバイケースだと痛感しました。SVGは割と重いですし、データが多いですからね。

文字も表示する関係上それほどfpsもいらないですしね(色を少しずつ変更しようかとも思ったのですが)。

また、svgデータを描いているにも関わらず変更箇所だけ修正すればいいのに、毎回全部再描画しています。この辺りを州ごとにIDを付与して、修正箇所だけ修正すればもっと軽くなりそうな感じがしています。またz軸などを決めれば、境界線も一度描けば再描画は不要な気がしています。

この辺りはD3.jsの不慣れとSVGが久しぶりのためあまり作りこめませんでした。

また後で気づいたのですが、南北戦争前ならば奴隷州と自由州で色を分けて表示されればよりミズリー妥協や南北戦争がよりわかりやすいかもしれません。明確なデータがあれば追加することは簡単ではないかと。

最後に

イベントをグラフのトップに記載しましたが、私は割とこれは重要だと考えているタイプです。理由は、グラフや資料などは、あそこ参照しろ、ここに行ってデータ見ろとよくありますがそれは見る側はとても負荷が高いと思うのです。

じゃ、それ1ページでまとめろよとなりますと、紙などでは物理的スペースで困難です。けどWEBならばもう少しやりようがあると思うのです。ツールチップとかフィルターとかメニューとかアニメーションとか駆使すれば情報の表示や精査はできるのではないかと思うのです。

D3.jsにラッパーを重ねればそのあたりはいい感じで作れるのではないかと思ったりしています。汎用的にするには大変かもしれませんけどね。

このため、WEB上のグラフなどは単なる表示するだけではなくもう少しデータを盛り込んでくれないかなと思ったりしています。

じゃ、お前が作れと言われそうなので今回は少し作ってみた次第です。

修正

ソースにシンタックスを入れました(入れ方知りませんでした。すいません)(2017/05/09)。

また、南北戦争中にアメリカ連合国に分かれていることを知りました。この辺りをデータに入れたいと思います(予定は未定)。

リンク

D3.jsを用いてUSの州昇格を時系列でグラフ化してみた その2(スライダーバーとか導入)

7
5
2

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
7
5