地図について(シリーズ2)#1

これまで、ひつじかいさんの白地図描画におけるブログを手元環境で動かすことで確認してきました。続いて、同じひつじかいさんのブログにおける[D3.jsで空間分析にチャレンジ]も興味深く拝見させていただき、同じく手元環境で確認したく、その記録を記載したいと思います。

D3.jsで空間分析にチャレンジ(1)

なお今回、ひつじかいさんのブログでの紹介されている時点でのD3.jsはバージョン3であり、現時点での最新バージョンは5。そのままでは動かないようです。また東京都でのデータをサンプルとして扱われていますが、小社のある神奈川県でデータを表示したいと思います。なおD3.jsを扱った経験なく、紆余曲折であることは間違いなく、誤った記載ありましたらご指摘いただけると幸いです。

まず、白地図を扱うための県境および市区町村、それぞれのGeoJSONファイルを用意する必要があります。まず市区町村データですが、これは以前にダウンロードしたデータにGeoJSONファイルそのものがあるので、そのまま使用したいと思います。

次に県境のGeoJSONファイルですが、これも以前に構築したDBから次のプログラムにおいてデータを抜き出したいと思います。なお参照するDBは、データを間引く前の[t_PrefectureBorder]テーブルになります。

<?php
ini_set('error_reporting', ~E_WARNING);
ini_set('memory_limit', '1024M');

$errMsg;
$mysqli = connectDBi($errMsg);
if ($mysqli) {
    echo "MySQL接続OK<br>";
} else {
    echo "MySQL接続NG<br>" . $errMsg;
    exit();
}


$fc = new FeatureCollection;
$d2 = 0; // 配列coordinatesの第2層の要素。ポリゴンに穴がある場合は$d2>0となるが、今回扱うデータには存在しないため0で固定

// 神奈川県(14)のみ作成...
for ($PrefCode = 14; $PrefCode <= 14; $PrefCode++) {
    $formerPoint = null;
    $feature = new Feature;
    $strSQL = "SELECT t_PrefectureBorder.*, t_Prefecture.PrefectureName";
    $strSQL .= " FROM t_PrefectureBorder";
    $strSQL .= " INNER JOIN t_Prefecture";
    $strSQL .= " ON t_PrefectureBorder.PrefectureCode = t_Prefecture.PrefectureCode";
    $strSQL .= " WHERE t_PrefectureBorder.PrefectureCode = " . $PrefCode;
    $strSQL .= " ORDER BY BorderCode, PointNo DESC";
    $rst = $mysqli->query($strSQL);

    while ($col = $rst->fetch_array(MYSQLI_ASSOC)) {
        if (!isset($formerPoint)) {
            $cNo = 0;
            $feature->properties = new PrefInfo($col);
            $feature->geometry = new Geometry("MultiPolygon");

        } else if ($formerPoint["BorderCode"] <> $col["BorderCode"]) {
            //! 最初と最後のデータを確認する...
            if ($tmp1 <> $tmp2){
                array_push($feature->geometry->coordinates[$cNo][$d2], $tmp1);
            }
            $cNo++;
        }

        if (!isset($feature->geometry->coordinates[$cNo])) {
            $feature->geometry->coordinates[$cNo][$d2] = array();
            $tmp1 = array(floatval($col["Lng"]), floatval($col["Lat"]));
        }
        $tmp2 = array(floatval($col["Lng"]), floatval($col["Lat"]));
        array_push($feature->geometry->coordinates[$cNo][$d2], $tmp2);
        $formerPoint = $col;
    }
    //! 最初と最後のデータを確認する...
    if ($tmp1 <> $tmp2){
        array_push($feature->geometry->coordinates[$cNo][$d2], $tmp1);
    }
    $rst->close();
    array_push($fc->features, $feature);
}

if ($mysqli) {
    $mysqli->close();
}

$geojson = json_encode($fc);
$geojson = str_replace("}},", "}},\n", $geojson);

echo $geojson . "<br>";

$path = "../json/N03-19_14.geojson";
$fp = fopen($path, 'w');
fwrite($fp, $geojson);
fclose($fp);

echo "終了しました。";

class FeatureCollection
{
    public $type = "FeatureCollection";
    public $features = array();
}

class Feature
{
    public $type = "Feature";
    public $properties;
    public $geometry;
}

class Geometry
{
    public $type;
    public $coordinates = array();

    function __construct($type = "")
    {
        $this->type = $type;
    }
}

class PrefInfo
{
    public $prefcode;
    public $prefname;

    function __construct($p)
    {
        $this->prefcode = $p["PrefectureCode"];
        $this->prefname = $p["PrefectureName"];
    }
}

//MySQLへ接続
function connectDBi(&$err)
{
    //MySQL 接続情報
    $MySQL_SERVER = "localhost";
    $MySQL_USER = "[user_name]";
    $MySQL_PASSWORD = "[password]";
    $MySQL_DBNAME = "map";
    $err = "";
    $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
    if ($mysqli->connect_errno) {
        $err = "データベース接続に失敗しました。";
    }
    if (strlen($err) > 0) {
        return false;
    } else {
        return $mysqli;
    }
}

次に地図上に表示するデータなのですが、ひつじかいさんのブログではSPM(浮遊粒子状物質)の2%除外値としておりますが今回、PM2.5(微小粒子状物質)の年平均値を対象にしたいと思います。

データは、神奈川県のホームページ上でPDFファイルとして公開されています。

そのPDFファイルから当該部分のデータを抜き出し、エクセルにまとめ直します。なお環境基準は、15ug/m3であり2018年度、年平均値での環境基準はすべての測定局で達成されたようです(日平均35ug/m3以下の環境基準は未達成0.26%[全局有効測定日23,845日に対して未達成日61日]がありました)。

さて困ったことに、神奈川県のホームページ上では測定局名に対応した住所や緯度・経度情報がないようです。そこで環境省のそらまめ君の測定局データから住所を抜き出し、その情報からジオコードを実施して緯度・経度を求める必要がありそうです。なお神奈川県HPとそらまめ君くんとの測定局名は必ずしも一致しておらず、どうしても手動で調整する部分がありました。EXCELのVLOOKUP関数などを用いて作成したCSVファイルは次の通りです。なおPointCodeは環境省のコードを用いています。

これで漸くにデータが揃い、これをバージョン5のD3.jsを用いて表示させるのですが、これが難儀しました。なにせ初めてのD3.js(Ver5)。ひつじかいさんのブログの内容を参考にさせていただき、紆余曲折しながら次のHTMLを記載し表示できたようです(正しいのかは半信半疑…)。

<!DOCTYPE html>
<html lang="jp">

<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <title>神奈川県 大気汚染常時監視測定局</title>
</head>

<body>
    <div id="top_bar" style="background-color:#003366;padding:10px;">
        <form id="f_paintmenu" name="f_paintmenu">
            <table>
                <tr>
                    <th class="map_title" id="map_title" name="map_title" style="font-size: 16px;color: #FFFFFF;">
                        神奈川県 大気汚染常時監視測定局(2018年度データ) ~データは神奈川県HPから引用</td>
                </tr>
            </table>
        </form>
    </div>
    <script>
        // svgのサイズ
        var width = 1200;
        var height = 800;

        // 描きたい緯度経度の領域(日本周辺)
        var west = 138.8;
        var east = 140.5;
        var north = 38.6;
        var south = 32;

        // svg要素を追加
        var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);

        // projectionを定義
        var projection = d3.geoMercator()
            .center([(west + east) / 2, (north + south) / 2]) // 緯度経度領域の中心
            .translate([width / 2, height / 2]) // svgの中心
            .scale(width / (2 * Math.PI * ((east - west) / 360)));

        // pathを定義
        var path = d3.geoPath(projection);

        // GeoJSONを読み込み、描画. ここでthenを使うのがv4との違い.
        d3.json("./json/N03-19_14_190101.geojson").then(function (json) {
            svg.append("g").selectAll("path")
                .data(json.features)
                .enter()
                .append("path")
                .attr("d", path) // GeoJSONのgeometryの情報をpath関数で変換
                .attr("stroke", "#C0C0C0") // 線の色
                .attr("fill", "none") // 塗りつぶしの色
                .attr("stroke-width", "0.3");   // 線の幅
        })

        // GeoJSONを読み込み、描画. ここでthenを使うのがv4との違い.
        d3.json("./json/N03-19_14.geojson").then(function (json) {
            svg.append("g").selectAll("path")
                .data(json.features)
                .enter()
                .append("path")
                .attr("d", path) // GeoJSONのgeometryの情報をpath関数で変換
                .attr("stroke", "#C0C0C0") // 線の色
                .attr("fill", "none") // 塗りつぶしの色
                .attr("stroke-width", "2");   // 線の幅
        })

        // CSV
        d3.csv("./csv/Kanagawa_pref.csv").then(function (csv){
            svg.append("g").selectAll("path")
                .data(csv)
                .enter()
                .append("circle")
                .attr("cx", function (d, i) {
                    return path.projection()([d.Lng, d.Lat])[0];
                })
                .attr("cy", function (d, i) {
                    return path.projection()([d.Lng, d.Lat])[1];
                })
                .attr("r", 3)
                .style("fill", "#0000ff")
                .on('mouseover', function (d) {
                    d3.select(this).style("fill", "#ff0000");
                    curText = d.PointName + " " + parseFloat(d.PM25).toFixed(1);
                    // curText = d.PointName;
                    SetTooltip(path.projection()([d.Lng, d.Lat]), curText, "3");
                })
                .on('mouseout', function () {
                    d3.select(this).style("fill", "#0000ff");
                    var tooltip = svg.select(".tooltip")
                    if (!tooltip.empty()) {
                        tooltip.style("visibility", "hidden")
                    }
                });
        })

        function SetTooltip(tipXY, tipText) {

            var tooltip = svg.select(".tooltip")
            var fontSize = 12;
            var rectWidth = tipText.length * fontSize + 5;
            var rectHeight = 20;

            if (tooltip.empty()) {
                tooltip = svg
                    .append("g")
                    .attr("class", "tooltip")

                tooltip
                    .append("rect")
                    .attr("height", rectHeight)
                    .style("stroke", "none")
                    .style("fill", "#ffffff")
                    .style("opacity", "0.8")

                tooltip
                    .append("text")
                    .attr("text-anchor", "left")
                    .attr("id", "tooltiptext")
                    .style("font-size", fontSize + "px")
                    .style("font-family", "sans-serif")
            }

            var tipleft = tipXY[0] + 2;
            if (tipleft + rectWidth > width) {
                tipleft = width - rectWidth;
            }
            var tiptop = tipXY[1] - rectHeight - 2;

            tooltip
                .style("visibility", "visible")
                .attr("transform", "translate(" + tipleft + "," + tiptop + ")")

            tooltip.select("text")
                .text(tipText + "ug/m")
                .attr("transform", "translate(5," + (fontSize + (rectHeight - fontSize) / 2) + ")")
                .append("tspan")
                .attr("id", "super")
                .attr("dy", "-2")
                .style("font-size", (fontSize - 2) + "px")
                .text("3")

            tooltip.select("rect")
                .attr("width", rectWidth)
        }
    </script>
</body>

</html>

こちらは公開しても問題ないかと思い、こちらのページからご確認いただけます。