JavaScript
React

SPA経県マップをつくろう (2) ~県ごとに経県で色わけできるところまで

はじめに

前回、SPA経県マップをつくろう (1) ~地方ごとに色わけできるところまでで地方単位に色を塗っていきましたが、今回は県ごとに色を塗っていきたいと思います。
前回リンクしたちゃいの経県値のURLからクエリパラメータは以下のなっていることがわかります。

  • 各県の経県値データ(MAP)
  • 名前(NAM)
  • タイトル(CAT)

今回は経県値データをダミーで作成して、それを表示するところまでやりたいと思います。

環境

OS: Windows7,10 & CentOS 7.2(VM)
node: v8.9.4

経県値データ

データは0~5の数値文字列が、北海道から沖縄までを表す47文字となります。数値の内訳はこんな感じです。

点数 記号 内容 カラー
5 居住(住んだ) #ff00ff
4 宿泊(泊まった) #ff0000
3 訪問(歩いた) #ffff00
2 接地(降り立った) #00ff00
1 通過(通過した) #00ffff
0 × 未踏(未経県) #f5f5f5

文字列の並びはどんなならびかなーって思っていたんですが、ISO 3166 2の順のようでした。47個なので目視確認です(笑)

$ cat src/japan_geo2topo.json | jq -c .objects.'"-"'.geometries[].properties" | [ .iso_3166_2, .name, .name_local ] " | sort
["JP-01","Hokkaido","北海道"]
["JP-02","Aomori","青森県"]
["JP-03","Iwate","岩手県"]["JP-46","Kagoshima","鹿児島県"]
["JP-47","Okinawa","沖縄県"]

別にマッピングテーブルもたなくてよかったです(^ ^)
先頭だったので sort 使っちゃいましたけど、jqで配列値にした後のソートってどうやるんでしょうね?そのうちまた調べましょう。
こんな感じでオリジナルのリクエストパラメータと同じダミーデータを作成して表示したいと思います。

アプリ更新

前回地方単位に色分けしていたところをパラメータによって色分けできるようにしていきましょう。

アプリはこの後編集も可能とするので状態を保持することになります。
Reactなのでflux使おうか考えましたが、簡単な1画面のアプリにしちゃおうかと思いますので App クラス内に state で作る方向でやろうと思います。なので、Appクラスで状態を管理、Mapをもう少しちゃんとコンポーネント化するように修正します。
今回やる内容はこんな感じです。

  • 経県定数定義
  • cssを地方から経県へ
  • Mapコンポーネントをもう少しコンポーネントっぽくしてパラメータをいただく
  • Appクラスで地図データ、及び経県情報を持つようにする(近い将来編集機能を入れるので)

ここで経県値情報はこんな感じのオブジェクトで持ちます。

{
  prefNames: [ "Hokkaido", ... , "Okinawa" ], // 北海道~沖縄までのName配列
  name, // 作成者名
  title, // 経県マップタイトル
  prefData: { // 経県値データ
    Hokkaido: {
      id: "ID_01", // ISO 3166 2値
      local: "北海道", // 日本語名
      experience: 4, // 経県値
    }
      :
  }
}

経県定数定義

経県値が表す :arrow_up: の表を定数化しておきます。
色はスタイルであてるので定義はしておきません。

EXPERIENCES配列を定義して、今回は name だけ使います。

定数定義
export const EXPERIENCES = [
  {
    // 0
    name: "unexplored",
    mark: "×",
    text: "未踏",
    subtext: "未経県"
  },
  {
    // 1
    name: "passage",
    mark: "▲",
    text: "通過",
    subtext: "通過した"
  },
  {
    // 2
    name: "earthing",
    mark: "△",
    text: "接地",
    subtext: "降り立った"
  },
  {
    // 3
    name: "visit",
    mark: "●",
    text: "訪問",
    subtext: "歩いた"
  },
  {
    // 4
    name: "stay",
    mark: "○",
    text: "宿泊",
    subtext: "泊まった"
  },
  {
    // 5
    name: "live",
    mark: "◎",
    text: "居住",
    subtext: "住んだ"
  }
];

css

前回の地方単位の定義を経県値単位のスタイルに修正します。

定数定義
.unexplored {
  fill: #f5f5f5;
}
.passage {
  fill: #00ffff;
}
.earthing {
  fill: #00ff00;
}
.visit {
  fill: #ffff00;
}
.stay {
  fill: #ff0000;
}
.live {
  fill: #ff00ff;
}

Mapコンポーネント化

前回はMapクラスに全て詰め込んでましたが、パラメータで受けるようにします。
変更内容的には以下のとおりです。

  • パラメータで幅、高さ、スケール、地図の経県値情報をもらう。
  • 頂いた経県値情報で色塗りする
  • center 計算
Map.js
  // クラスの先頭に PropTypes 定義追加
  static propTypes = {
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    scale: PropTypes.number.isRequired,
    mapState: PropTypes.object.isRequired,
  };

  drawMap() {

    const w = this.props.width;
    const h = this.props.height;

    const jpn = mapJson;
    const geoJp = topojson.feature(jpn, jpn.objects['-']);

    const center = d3.geoCentroid(geoJp); // center計算

    // 地図の投影図法を設定する.
    var projection = d3.geoMercator()
        .center(center)
        .scale(this.props.scale)
        .translate([w / 2, h / 2]);

    // GeoJSONからpath要素を作る.
    var path = d3.geoPath().projection(projection);

    console.log(geoJp);
    const svg = d3.select(this.node);
    svg.append('rect') // うーみ
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', w)
      .attr('height', h)
      .attr('style', 'fill: #3987c9;');
    svg.attr('height', h)
      .attr('width', w)
      .selectAll("path")
      .data(geoJp.features)
      .enter()
      .append("path")
        .attr("class", d => this._name2Class(d.properties.name))
        .attr("d", path);
  }

  // 県名から経県値を取得して、対応した css クラス名を返す
  _name2Class(name) {
    console.log(this.props.mapState.prefData[name].experience);
    return EXPERIENCES[this.props.mapState.prefData[name].experience].name;
  }

やってたら PropTypes がないって最初おこられてしまいました(^ ^;
前と違って PropTypes は別になってるんですね。
インストールして、import PropTypes from 'prop-types';を追加して解決です。

Appクラスで state管理

ここでは

  • 初期のダミー経県値情報作成
  • パラメータで Map を呼び出す

の修正を行います。

App.js
import { EXPERIENCES } from './constants';

// geo2topo output
import japanMap from './japan_geo2topo.json';

class App extends Component {

  constructor() {
    super();
    // 都道府県数分の 0-5 の文字列をランダムに作成
    const dummy = Array.from(Array(japanMap.objects['-'].geometries.length))
      .map(d => Math.floor(Math.random() * EXPERIENCES.length) + '').join('');
    this.state = this._createState(dummy);
  }

  // 経県値文字列、名前、タイトルから state に設定するオブジェクトを作成
  _createState(prefStateStr = '', name = '', title = '') {
    const st = {
      prefNames: null, // 北海道~沖縄までのNameの配列をいれる
      name, // your name
      title, // title
      prefData: { // nameをキーにした状態Map
      }
    };
    st.prefNames = japanMap.objects['-'].geometries
      .sort((a, b) => b.properties.iso_3166_2.substr(2) - a.properties.iso_3166_2.substr(2) ) // 'ID-xx' の xx の数値でソート
      .map((pref, idx) => {
        const name = pref.properties.name;
        st.prefData[name] = {
          id: pref.properties.iso_3166_2,
          local: pref.properties.name_local,
          experience: this._extractExperience(prefStateStr, idx)
        };
        return name;
      });
    return st;
  }

  // 経県値の47文字のデータから idx 番目を返す
  _extractExperience(prefStateStr, idx) {
    const plen = prefStateStr.length;
    const e = idx < plen ? prefStateStr.substr(idx, 1) : '0';
    return (e - 0) % EXPERIENCES.length;
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <Map width="1300" height="960" scale="2000" mapState={{...this.state}} />
      </div>
    );
  }
}

javascriptって Range 演算子標準でないですよね。47配列つくって map するやり方をちょこっと忘れてました。
今回は Array.from(Array(数値)).map(...) ってやって解決したんですが、Array.form がなんで必要なのかいまいちわからないのがもやもやするところ。

_createStateはこのあとリクエストパラメータをうけて初期状態を作る時につかえるようにとパラメータを受けるようにしときます。

アプリ起動

おきまりの npm start ですね。

react_keikenti_dummy.png

県ごとの色分けできましたですねー(^ ^)/
次回、アプリとして編集していくところまでつくっていきたいかと思います。