前回はD3.jsのメソッドを利用してボロノイ図を作成しましたが、今の所D3.jsには他の空間分析系の関数は見当たりませんので、自ら作成するしかありません。

 基本的に空間分析は「R」言語とセットになっているようで、これがないと何も出来ないような状況ですので、まずはRをインストール。
 以下ページに丁寧な説明があり、特に問題なくインストール出来ました。 
   R とパッケージの簡単インストール

 同時にRと空間分析の書籍を購入。
   地理空間データ分析(Rで学ぶデータサイエンス 7)共立出版

 まだまだ学習中で全然読み進んでいないのですが、本書で紹介されている「カーネル密度推定法」というものが比較的導入し易そうなので、今回はこれを試すことにしました。カーネル密度推定法は犯罪発生マップ等に利用されているようです。
 利用例:大阪府警察犯罪発生マップ
       ※カーネル密度推定は「車上ねらい等」のマップに適用されています。
 
 カーネル密度推定とはイベント発生点の分布状況から全体の分布状況を推定する手法の1つで、次の計算式によって求めます。
  カーネル密度推定数式
  n:標本点数  Xi(i=1,2,…,n):各点の値(座標) x:密度推定点の値(座標)
  h:バンド幅(標本点からの広がりの範囲)  K:カーネル関数
  
 このうちKのカーネル関数に適用される関数にはガウス関数、イパネクニコフ関数、四次関数など数種類あり、どれを採用すれば良いのか判断が難しいですが、前記の書籍によれば「採用するカーネル関数による影響よりも、バンド幅(h)の選択による影響の方が大きい」とありますので、今回はガウス関数の1つである次の標準正規分布を採用することにしました。
  カーネル関数(標準正規分布)
 この式を上のカーネル密度推定数式に当てはめると、
  カーネル密度推定数式(ガウス関数を適用)
 となります。
 これをjavascript+D3.jsに実装。標本としたデータは、アメダス(東京都府中市)の日平均気温(2013年)です。

<!DOCTYPE html>
<meta charset="utf-8">
<title>カーネル密度推定 アメダスデータ(東京都府中市 日平均気温 2013年)</title>
<style>

   .axis text {
      font-family: sans-serif;
      font-size: 11px;
   }
   .axis path,
   .axis line {
      fill: none;
      stroke: #000000;
      stroke-width: 1px;
      shape-rendering: crispEdges;
   }
   .legend text {
      font-family: sans-serif;
      font-size: 11px;
   }

</style>
<script type="text/javascript" src="js/d3.js"></script>
<script type="text/javascript" src="js/queue.v1.js"></script>
<script>

   var svg;
   var width = 800;
   var height = 400;
   var marginLeft = 100;
   var marginTop = 20;
   var xInterval = 1;
   var xRange = [-5,35];

   var xScale = d3.scale.linear()
      .domain([xRange[0], xRange[1]])
      .range([0, width])

   var yScale = d3.scale.linear()
      .domain([0, 0.07])
      .range([height, 0])

   var xAxis = d3.svg.axis()
      .scale(xScale)
      .orient("bottom")
      .ticks(10)

   var yAxis = d3.svg.axis()
      .scale(yScale)
      .orient("left")
      .ticks(10)

   var line = d3.svg.line()
      .x(function(d) { return marginLeft + xScale(d[0]); })
      .y(function(d) { return marginTop + yScale(d[1]); })

   var histogram = d3.layout.histogram()
      .frequency(false)
      .bins(d3.range(xRange[0],xRange[1]+0.001,xInterval));

   function initialize() {

      svg = d3.select("body").append("svg")
         .attr("id", "svg")
         .attr("width", width+200)
         .attr("height", height+150)

      queue()
         .defer(d3.csv, "csv/temperature_fuchu2013.csv?=" + new Date().getTime())
         .await(ready);
   }

   function ready(error, data) {

      var x = new Array();
      data.forEach(function(d) {
         x.push(parseFloat(d.Temperature));
      });

      var hist = histogram(x);

      svg.selectAll(".hist")
         .data(hist)
         .enter()
         .append("rect")
         .attr("class", "hist")
         .attr("x", function(d) { return marginLeft + xScale(d.x); })
         .attr("y", function(d) { return marginTop + yScale(d.y); })
         .attr("width", width/(xRange[1]-xRange[0])*xInterval)
         .attr("height", function(d) { return height - yScale(d.y); })
         .attr("fill", "#E0E0E0")
         .attr("stroke", "#909090")
         .attr("stroke-width", "0.5px")

      var gx = d3.range(xRange[0],xRange[1]+0.001,xInterval/10);

      var kde = new Array();
      var bandwidth = [0.5, 2.0, 5.0, 10.0];
      bandwidth.forEach(function(d) {
         kde.push(GaussianKernelDensityEstimation(x,gx,d));
      });
      var lineColor = ["#0000C0","#00C000","#C00000","#F0F000"];

      var lines = svg.append("g")
         .attr("class","lines")
         .attr("fill", "none")
         .attr("stroke-width", "2px")

      lines.selectAll("path")
         .data(kde)
         .enter()
         .append("path")
         .attr("id", function(d,i) { return "kde"+i; })
         .attr("d", function(d,i) { return line(d); })
         .attr("stroke", function(d,i) { return lineColor[i]; })

      drawAxis();
      drawLegend(bandwidth,lineColor);

   }

   function drawAxis(){

      d3.select("#svg")
         .append("g")
         .attr("class", "axis")
         .attr("transform", "translate("+marginLeft+", "+marginTop+")")
         .call(yAxis)
         .append("text")
            .attr("class", "label")
            .attr("x", -50)
            .attr("y", height/2)
            .attr("fill", "#000000")
            .style("writing-mode", "tb")
            .style("text-anchor", "middle")
            .text("確率密度");

      d3.select("#svg")
         .append("g")
         .attr("class", "axis")
         .attr("transform", "translate("+marginLeft+", "+(marginTop+height)+")")
         .call(xAxis)
         .append("text")
            .attr("class", "label")
            .attr("x", width/2)
            .attr("y", 35)
            .attr("fill", "#000000")
            .style("text-anchor", "middle")
            .text("日平均気温(℃)");

   }

   function drawLegend(bw,cl){

      var offsetY = 40;

      var legendline = svg.append("g")
             .attr("fill", "none")
             .attr("stroke-width", "2px")
             .attr("class", "legend")

      legendline.selectAll("path")
         .data(bw)
         .enter()
         .append("path")
         .attr("d", function(d,i) { 
            return "M"+ (marginLeft+width-100)+","+(marginTop+height+offsetY+i*15)+"L"+ (marginLeft+width-50)+","+(marginTop+height+offsetY+i*15);
         })
         .attr("stroke", function(d,i) { return cl[i]; })

      var format = d3.format(".1f");

      legendline.selectAll("text")
         .data(bw)
         .enter()
         .append("text")
         .attr("x", marginLeft+width-25)
         .attr("y", function(d,i) { 
            return marginTop+height+offsetY+4+i*15;
         })
         .style("text-anchor", "end")
         .attr("fill", "#000000")
         .text(function(d,i) { return format(d); })

      legendline.append("text")
         .attr("x", marginLeft+width-115)
         .attr("y", function(d,i) { 
            return marginTop+height+offsetY+4;
         })
         .style("text-anchor", "end")
         .attr("fill", "#000000")
         .text("バンド幅")

      svg.append("text")
         .attr("x", marginLeft+50)
         .attr("y", marginTop+20)
         .style("font-size","12px")
         .text("東京都府中市 2013年 日平均気温の分布状況  ※出典:気象庁 アメダスデータ")

   }

   function GaussianKernelDensityEstimation(x, gx, h) {

      var kde = [];
      var dd;

      if(typeof h === 'undefined') {
         h = SilvermanBandWidth(x);
      }

      for (i in gx) {
         dd = 0;
         for (ii in x) {
            dd += (Math.exp(-0.5*Math.pow((gx[i]-x[ii])/h,2)))/Math.sqrt(2 * Math.PI);
         }
         kde.push([gx[i],dd/(x.length*h)])
      }
      return kde;

   }

   function SilvermanBandWidth(data,mode) {   
      var n = 0;
      var Sxx = 0;
      var Ex,Sd;
      var d = [].concat(data);
      var IQR = d3.quantile(d.sort(d3.ascending), 0.75) - d3.quantile(d.sort(d3.ascending), 0.25);

      Ex = d3.mean(d);
      for (var i in d) {
         Sxx += Math.pow(Ex - d[i],2);
      }
      Sd = Math.sqrt(Sxx/(d.length-1));

      if (mode == 0) {
         return 0.9 * d3.min([IQR/1.34,Sd]) * Math.pow(d.length,-0.2);  //Silverman's rule
      } else {
         return 1.06 * d3.min([IQR/1.34,Sd]) * Math.pow(d.length,-0.2); //Scott's rule
      }
   }

</script>
<body onload="initialize()">

 4通りのバンド幅(0.5,2.0,5.0,10.0)にてそれぞれ計算しています。また、ヒストグラムも作成しています。
 これをブラウザで表示させると、下図のようなグラフとなります。

東京都府中市の日平均気温分布(2013年)

東京都府中市の日平均気温分布(2013年)
このグラフを表示


 バンド幅の設定によって密度分布の形が大きく変わることがよく分かります。また、標本データの存在しない所でも、その周辺に標本点があれば密度推定値はゼロにはならないことも確認出来ます(上図の31~32℃の箇所)。

 と、ここまでは1次元の場合。今回は地図上に表現したいので2次元で計算する必要があるのですが、その計算式が分からない・・・Rや統計学の参考書を調べてみても、何の説明もなく用意された関数を使用しているものがほとんど。たまに説明されているものを見つけても専門的過ぎる上に計算例が記載されていないので理解出来ません。仕方がないのでRの2次元カーネル密度推定関数の構造を解析しました。
 Rには無数のライブラリ(パッケージと呼ぶようです)があり、2次元カーネル密度推定関数もいくつかあるのですが、その中でも最もシンプルな式を採用していると思われるkde2d(MASSパッケージに収納)を対象としました。当関数では次のような計算をしています。
  カーネル密度推定数式(2次元)
  n:標本点数  Xi,Yi(i=1,2,…,n):各点の値(X座標,Y座標)
  x,y:密度推定点の値(X座標,Y座標) hx,hy:バンド幅(X座標方向,Y座標方向)

 この式を見ると、X座標の値とY座標の値は互いに独立しているとみなし、共分散は考慮していないようです。それでいいのかどうかはよく分からないのですが、とりあえずはこのままの式で進めます。また、関数kde2dでは引数として与えたバンド幅の値を計算時に1/4にしているのですが(2次元の平面だから22ということでしょうか?)、今回は引数で与えた値をそのまま計算式に組み込むことにします。

 以下、2次元カーネル密度推定の実装プログラムソースです。 

function GaussianKernelDensityEstimation2D(x, y, nx, ny, h, rangex, rangey) {
/* 引数
     x,y:イベント発生点の座標(X座標,y座標) ※両変数とも配列形式でx,yの要素数は同数
   nx,ny:密度推定点数(X座標方向、Y座標方向)※計算対象の点数はnx×nyとなります。  
   h:バンド幅
    ※要素数=2の配列形式 [X座標方向のバンド幅,Y座標方向のバンド幅] 
    ※配列でなく数値を渡した場合は、その値がX,Y両方向共通のバンド幅として適用される
    ※Nullを渡した場合は、シルバーマンの方法(イベント発生点の分散や四分位範囲などから計算)により設定する
     rangex,rangey:密度推定点を配置する範囲(X座標方向、Y座標方向)要素数=2の配列形式
     例: rangex=[0,50]・・・Min=0,Max=50の範囲で配置
       nx=6であれば、推定点のX座標は0,10,20,30,40,50となる     
*/
   var hx,hy;
   var rx0,rx1,ry0,ry1;

   if(!h || isNaN(h) || ((h instanceof Array) && (h.length != 2))) {
      hx = SilvermanBandWidth(x);
      hy = SilvermanBandWidth(y);
   } else if (!(h instanceof Array)) {
      hx = h;
      hy = h;
   } else {
      hx = h[0];
      hy = h[1];
   }

   if((typeof rangex === 'undefined') || !(rangex instanceof Array) || (rangex.length != 2)) {
      rx0 = d3.min(x) - hx*1.5;
      rx1 = d3.max(x) + hx*1.5;
   } else {
      rx0 = rangex[0];
      rx1 = rangex[1];
   }
   if((typeof rangey === 'undefined') || !(rangey instanceof Array) || (rangey.length != 2)) {
      ry0 = d3.min(y) - hy*1.5;
      ry1 = d3.max(y) + hy*1.5;
   } else {
      ry0 = rangey[0];
      ry1 = rangey[1];
   }

   var gx = d3.range(rx0,rx1+Math.pow(10,-10),(rx1-rx0)/(nx-1));
   var gy = d3.range(ry0,ry1+Math.pow(10,-10),(ry1-ry0)/(ny-1));

   var kde = [];
   var dd;
   var i,j,ii;

   for (i in gx) {
      for (j in gy) {
         dd = 0;
         for (ii in x) {
            dd += (Math.exp(-0.5*Math.pow((gx[i]-x[ii])/hx,2))*Math.exp(-0.5*Math.pow((gy[j]-y[ii])/hy,2)))/(2 * Math.PI);
         }
         kde.push([gx[i],gy[j],(dd / (x.length * hx * hy))])
      }
   }
   return kde;
}

  
 前述したバンド幅の調整の部分を除けばkde2dと同じ結果が得られる・・・と思います。

 この計算を実際の地図上に反映させるに当たり、前回ボロノイ図作成時に使用した東京都大気汚染常時監視測定局は、「観測点」であって実際に大気汚染物質を排出している場所ではないので、カーネル密度推定に使用する点としては適当ではありません。そのため、今回は代わりに東京都内の清掃工場を使用することにしました。実際には大気汚染物質というのは自動車からの排出が半分以上で、しかも最近の清掃工場は環境にも配慮しているでしょうから都内の排出総量から考えれば微々たるものなのですが、一応排出源ではありますので標本点としてふさわしいかと思います。
 密度推定点は地図上の平面に格子状に配置します(メッシュ)。そして前回のボロノイ図と同様に、この計算結果の数値に応じて各メッシュを着色・・・すれば良いのですが、試したところメッシュを細かくすると描画に異常に時間がかかり固まってしまいます。svgのpath生成処理に時間がかかるのかとおもったのですが、色をつけなければ速いので描画自体に時間がかかる模様。ブラウザ(IE,Chrome,FireFox,Opera)による差異も大きく、速い順にChrome>Opera>IE>FireFoxとなりました。特にFireFoxの遅さが突出しています。
 描画速度はブラウザのバージョンにより改善される可能性はありますが、現時点ではメッシュ数=20,000(200×100)位にすると最も速いChromeでも相当時間がかかるので、実用に耐えられません。となると他の方法を考えなければなりませんが、他といってもCanvasくらいしか思いつきません。メッシュのみをCanvasに描画しSVGと重ねる方法を試してみました。以下が、そのソースです。

・WasteIncinerationPlantTokyo.html

<!DOCTYPE html>
<html>
<head>
<title>東京都の清掃工場分布とカーネル密度推定</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="js/d3.js"></script>
<script type="text/javascript" src="js/queue.v1.js"></script>
<script type="text/javascript" src="js/WasteIncinerationPlantTokyo.js"></script>
</style>
</head>

<body style="margin:0px;" onload="setMap(document.getElementById('f_mesh').value,document.getElementById('f_bandwidth').value,true)">
  <div style="background-color:#666600;padding:5px;">
  <table style="padding-left:5px;">
  <tr>
    <td style="font-size: 16px;font-weight: bold;color: #FFFFFF;">東京都の清掃工場分布とカーネル密度推定 </td>
    <td style="font-size:12px;color:#FFFFFF;padding-left:30px;">メッシュ:
      <select id="f_mesh" name="f_mesh" style="width:80px;font-size:12px;">
         <option value="20,10">20×10</option>
         <option value="50,25">50×25</option>
         <option value="100,50">100×50</option>
         <option value="160,80">160×80</option>
         <option value="240,120">240×120</option>
      </select>
    </td>
    <td style="font-size:12px;color:#FFFFFF;padding-left:20px;">バンド幅:
      <select id="f_bandwidth" name="f_bandwidth" style="width:180px;font-size:12px;">
         <option value="10">10</option>
         <option value="20" selected>20</option>
         <option value="30">30</option>
         <option value="40">40</option>
         <option value="50">50</option>
         <option value="">自動(SilvermanBandWidth)</option>
      </select>
    </td>
    <td style="padding-left:10px;">
       <input type="button" style="width:50px;font-size:14px;" onclick="setMap(document.getElementById('f_mesh').value,document.getElementById('f_bandwidth').value)" value="更新">
    </td>
  </tr>
  </table>
  </div>
</body>
</html>

・WasteIncinerationPlantTokyo.js

// *************************************************************** 
//  D3.jsで空間分析にチャレンジ
// *************************************************************** 

  var svg;
  var mapScale = 50000;
  var mapWidth = 1000;
  var mapHeight = 500;
  var mapTop = 40;
  var mapPath, mapBounds;
  var meshCountX,meshCountY,bandWidth;
  var jsonCityFileName = "json/CityBorderTokyo.json?=" + new Date().getTime();
  var jsonOutlineFileName = "json/OutlineTokyo.json?=" + new Date().getTime();
  var pointFileName = "csv/T_WasteIncinerationPlant.csv?" + new Date().getTime();
  var pointX = new Array();
  var pointY = new Array();
  var border,outline,point;
  var color = d3.interpolateHsl("#66ff00", "#ff0000");
  var alpha = 0.7;

  function setMap(meshxy,bw,initFlag) {
     bandWidth = bw;
     meshCount = meshxy.split(",");
     meshCountX = meshCount[0];
     meshCountY = meshCount[1];
     if (initFlag) {
        queue()
           .defer(d3.json, jsonCityFileName)
           .defer(d3.json, jsonOutlineFileName)
           .defer(d3.csv, pointFileName)
           .await(ready);
     } else {
        drawCanvas(false);
     }
  }

  function ready(error, data1, data2, data3) {
     border = data1;
     outline = data2;
     point = data3;
     drawMap();
  }

  function drawMap() {

     d3.select("body").append("canvas")
        .attr("width", mapWidth)
        .attr("height", mapHeight)
        .attr("id", "canvas")
        .style("position", "absolute")
        .style("top", mapTop+"px")
        .style("left", "0px")

     svg = d3.select("body").append("svg")
        .attr("width", mapWidth)
        .attr("height", mapHeight)
        .attr("id", "svg")
        .style("position", "absolute")
        .style("top", mapTop+"px")
        .style("left", "0px")

     mapBounds = d3.geo.bounds(border);
     var mapcenter = [(mapBounds[0][0]+mapBounds[1][0])/2, (mapBounds[0][1]+mapBounds[1][1])/2];

     mapPath = d3.geo.path()
        .projection(d3.geo.mercator()
           .center(mapcenter)
           .translate([mapWidth/2, mapHeight/2])
           .scale(mapScale)
        );

     point.forEach(function(d) {
        pointX.push(mapPath.projection()([d.Lng,d.Lat])[0]);
        pointY.push(mapPath.projection()([d.Lng,d.Lat])[1]);
     });

     drawCanvas(true);

     svg.selectAll(".outline")
        .data(outline.features)
        .enter()
        .append("path")
        .attr("d", mapPath)
        .style("fill", "none")
        .attr("class", "outline")
        .style("stroke", "#C0C0C0")
        .style("stroke-width", "2")

     svg.selectAll(".cityborder")
        .data(border.features)
        .enter()
        .append("path")
        .attr("d", mapPath)
        .style("fill", "none")
        .attr("class", "cityborder")
        .style("stroke", "#C0C0C0")
        .style("stroke-width", "0.3")

     var WasteIncinerationPlant = svg.selectAll(".plant")
        .data(point)
        .enter()
        .append("circle")
        .attr("cx", function(d,i) {
           return mapPath.projection()([d.Lng,d.Lat])[0];
        })
        .attr("cy", function(d,i) {
           return mapPath.projection()([d.Lng,d.Lat])[1];
        })
        .attr("r", 3)
        .style("fill", "#0000ff")
        .on('mouseover', function(d) {
           d3.select(this).style("fill","#ff0000");
           curText = d.PlaceName;
           SetTooltip(mapPath.projection()([d.Lng,d.Lat]),curText);
        })
        .on('mouseout', function() {
           d3.select(this).style("fill","#0000ff");
           var tooltip = svg.select(".tooltip")
           if(!tooltip.empty()) {
              tooltip.style("visibility", "hidden")
           }
        })

     setMapLegend();

  }

  function drawCanvas(initFlag) {

     var xRange = d3.extent([mapPath.projection()(mapBounds[0])[0],mapPath.projection()(mapBounds[1])[0]]);
     var yRange = d3.extent([mapPath.projection()(mapBounds[0])[1],mapPath.projection()(mapBounds[1])[1]]);
     var meshWidth = (xRange[1]-xRange[0])/(meshCountX- 1);
     var meshHeight = (yRange[1]-yRange[0])/(meshCountY- 1);

     var kde = GaussianKernelDensityEstimation2D(pointX, pointY, meshCountX, meshCountY, bandWidth, xRange, yRange);
     var drange = d3.extent(kde, function (d) { return d[2]; });

     var currentcolor;
     var canvas = document.getElementById('canvas')
     var ctx = canvas.getContext('2d');

     if (initFlag) {
        ctx.beginPath();
        var coordinate; 
        outline.features.forEach(function(d,i) {
           for (var i = 0; i < d["geometry"]["coordinates"][0].length; i++) {
              coordinate = mapPath.projection()([parseFloat(d["geometry"]["coordinates"][0][i][0]),parseFloat(d["geometry"]["coordinates"][0][i][1])])
              if (i==0) {
                 ctx.moveTo(coordinate[0], coordinate[1]);
              } else {
                 ctx.lineTo(coordinate[0], coordinate[1]);
              }
           }
        });
        ctx.closePath();
        ctx.clip();
     }

     ctx.clearRect(0,0,mapWidth,mapHeight); 
     ctx.globalAlpha = alpha;
     kde.forEach(function(d) {
        currentcolor = color((d[2]-drange[0])/(drange[1]-drange[0]));
        ctx.fillStyle = currentcolor;
        ctx.fillRect(d[0]-0.5*meshWidth,d[1]-0.5*meshHeight,meshWidth,meshHeight);
     });

  }

  function GaussianKernelDensityEstimation2D(x, y, nx, ny, h, rangex, rangey) {

     var hx,hy;
     var rx0,rx1,ry0,ry1;

     if(!h || isNaN(h) || ((h instanceof Array) && (h.length != 2))) {
        hx = SilvermanBandWidth(x);
        hy = SilvermanBandWidth(y);
     } else if (!(h instanceof Array)) {
        hx = h;
        hy = h;
     } else {
        hx = h[0];
        hy = h[1];
     }

     if((typeof rangex === 'undefined') || !(rangex instanceof Array) || (rangex.length != 2)) {
        rx0 = d3.min(x) - hx*1.5;
        rx1 = d3.max(x) + hx*1.5;
     } else {
        rx0 = rangex[0];
        rx1 = rangex[1];
     }
     if((typeof rangey === 'undefined') || !(rangey instanceof Array) || (rangey.length != 2)) {
        ry0 = d3.min(y) - hy*1.5;
        ry1 = d3.max(y) + hy*1.5;
     } else {
        ry0 = rangey[0];
        ry1 = rangey[1];
     }

     var gx = d3.range(rx0,rx1+Math.pow(10,-10),(rx1-rx0)/(nx-1));
     var gy = d3.range(ry0,ry1+Math.pow(10,-10),(ry1-ry0)/(ny-1));

     var kde = [];
     var dd;
     var i,j,ii;

     for (i in gx) {
        for (j in gy) {
           dd = 0;
           for (ii in x) {
               dd += (Math.exp(-0.5*Math.pow((gx[i]-x[ii])/hx,2))*Math.exp(-0.5*Math.pow((gy[j]-y[ii])/hy,2)))/(2 * Math.PI);
           }
           kde.push([gx[i],gy[j],(dd / (x.length * hx * hy))])
        }
     }
     return kde;
  }

  function SilvermanBandWidth(data,mode) {

     var n = 0;
     var Sxx = 0;
     var Ex,Sd;
     var d = [].concat(data),i;
     var IQR = d3.quantile(d.sort(d3.ascending), 0.75) - d3.quantile(d.sort(d3.ascending), 0.25);

     Ex = d3.mean(d);

     for (i in d) {
        Sxx += Math.pow(Ex - d[i],2);
     }
     Sd = Math.sqrt(Sxx/(d.length-1));

     if (mode == 0) {
        return 0.9 * d3.min([IQR/1.34,Sd]) * Math.pow(d.length,-0.2);  //Silverman's rule
     } else {
        return 1.06 * d3.min([IQR/1.34,Sd]) * Math.pow(d.length,-0.2); //Scott's rule
     }
  }

  function SetTooltip(tipXY,tipText) {

     var tooltip = svg.select(".tooltip")
     var fontSize = 12;
     var rectWidth = tipText.length * fontSize;
     if (tipText.length < 15) {
        rectWidth += 15 - tipText.length;
     }
     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.5")

        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 > mapWidth) {
        tipleft = mapWidth - rectWidth;
     }
     var tiptop = tipXY[1] - rectHeight - 2;

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

     tooltip.select("rect")
        .attr("width",rectWidth)

     tooltip.select("text")
        .text(tipText)
        .attr("transform", "translate(5,"+(fontSize+(rectHeight-fontSize)/2)+")")

  }

  function setMapLegend() { 

    var fontsize = 14;
     var legendLeft = 100;
     var legendTop = mapPath.projection()(mapBounds[0])[1] - 70;
     var legendWidth = 240;
     var legendHeight = 20;
     var rangeWidth = 4;
     var valuerange = d3.range(0,1.00001,1/(legendWidth/rangeWidth-1));

     var MapLegend = d3.select("svg")
        .append("g")
        .attr("id","maplegend")
        .attr("transform", "translate("+legendLeft+","+legendTop+")")

     MapLegend
        .append("circle")
        .attr("r", 3)
        .style("fill", "#0000ff")
        .on('mouseover', function(d) {
           d3.select(this).style("fill","#ff0000");
        })
        .on('mouseout', function() {
           d3.select(this).style("fill","#0000ff");
        })

     MapLegend
        .append("text")
        .attr("id","text1")
        .style("font-size",fontsize + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")
        .attr("transform", "translate("+10+","+ (fontsize * 0.5 - 2) +")")
      .text("清掃工場")

     MapLegend
        .append("text")
        .attr("id","text2")
        .style("font-size",(fontsize - 2) + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")
        .attr("transform", "translate("+20+","+ (fontsize * 1.5) +")")
      .text("※マウスオンすると名称が表示されます。")

     MapLegend.selectAll(".legend")
        .data(valuerange)
        .enter()
        .append("rect")
        .attr("class", "legend")
        .attr("transform", function(d,i) {
           return "translate("+(i*rangeWidth)+",32)";
        })
        .attr("width",rangeWidth)
        .attr("height",legendHeight)
        .style("fill", function(d){
           return color(d);
        })
        .style("opacity",alpha)

     MapLegend
        .append("rect")
        .attr("id","legendframe")
        .attr("transform", "translate(0,32)")
        .attr("width",legendWidth)
        .attr("height",legendHeight)
        .style("fill", "none")
        .style("stroke","#303030")
        .style("stroke-width",0.5);

     var MapLegendAxis = MapLegend
        .append("g")
        .attr("transform", "translate(0,"+ (32 + legendHeight + 5) +")")
        .attr("class","axis")
        .style("font-size",fontsize + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")

     MapLegendAxis
        .append("path")
        .attr("d", "M10,10L"+(legendWidth/2-(fontsize*3+10)/2)+",10M10,10L20,5M10,10L20,15"
                   +"M"+(legendWidth-10)+",10L"+(legendWidth/2+(fontsize*3+10)/2)+",10"
                   +"M"+(legendWidth-10)+",10L"+(legendWidth-20)+",5M"+(legendWidth-10)+",10L"+(legendWidth-20)+",15")
        .style("stroke","#808080")
        .style("stroke-width","0.5")

     MapLegendAxis
        .append("text")
        .attr("transform", "translate("+legendWidth/2+","+(10+fontsize*0.5)+")")
        .attr("text-anchor","middle")
        .attr("class","axistext")
      .text("密 度")

     MapLegendAxis
        .append("text")
        .attr("transform", "translate(0,"+(10+fontsize*0.5)+")")
        .attr("text-anchor","middle")
        .attr("class","axistext")
      .text("低")

     MapLegendAxis
        .append("text")
        .attr("transform", "translate("+legendWidth+","+(10+fontsize*0.5)+")")
        .attr("text-anchor","middle")
        .attr("class","axistext")
      .text("高")

  }

 Canvasへの描画はSVGよりもかなり速いです。これでメッシュを細かくしても(240×120)、さほどストレスなく表示されるようになりました。出力例を下図に示します。

図 東京都の清掃工場分布とカーネル密度推定(メッシュ数:240×120)

東京都の清掃工場分布とカーネル密度推定(バンド幅=10)

バンド幅=10

東京都の清掃工場分布とカーネル密度推定(バンド幅=20)

バンド幅=20

東京都の清掃工場分布とカーネル密度推定(バンド幅=30)

バンド幅=30

 実際には工場の煙の拡散には風向・風速等の気象条件が大きく影響するのですが、今回は考慮していません。前回作成したボロノイ図と比較しても、より「空間分析」っぽくなってきた気がします。
 清掃工場から排出される汚染物質がそれほど広い範囲に影響を及ぼすとは思えないので、上図の中では一番上のバンド幅=10が実態に合っているのではないかと思いますが、バンド幅=20もしくは30の方が見栄えがいいですね。見栄えで判断すべきではないのでしょうが、バンド幅=10だと単なる清掃工場分布図と大して変わらないですね。

コメント   

 2014年11月24日(月)
 府中市美術館の特別展「生誕100年 小山田二郎」を観覧してきました。

府中市美術館


 小山田二郎は多摩霊園の近くに住んでいた府中市ゆかりの画家だそうです。他の美術展で開催されていたら行かなかっただろうと思いますが、地元開催なので散歩がてら・・・。

 常設展等で何度か見たことがあるはずなのですが、ほとんど記憶に残っていませんでした。あらためて観覧してみると結構強烈です。でも正直にいうと、グッと引き込まれるような作品には出会えませんでした。まあ、無理に感動する必要はありませんからね。

 それよりも府中の森公園の紅葉がちょうど見頃で、とても綺麗でした。

 本展は1部が12/28(日)までで、2部は来年の1/10(土)~2/22(日)。1部と2部で大幅な展示替えがあるようですが、私は2部は行かなくていいかなあ。休日の午後なのにガラガラでした。

府中の森公園の紅葉(1)


府中の森公園の紅葉(2)

コメント   

 2014年11月23日(日・祝)
 大田原マラソンを走ってきました。2010年、2012年に続いて3回目、1年おきに参加しています。
 この大会は、参加者がさほど多くないせいもあり、着替えや荷物置きに体育館を利用出来ますし、トイレ待ちやスタート時の大渋滞もない・・・と何かと便利なんですが、やはり制限時間が4時間というのは厳しいですね。
 いつものペースで走れれば問題ないのですが、私の走力では楽勝という程ではないので少しでも体調不良を起こせばたちまちピンチとなってしまいます。それに最近の練習は、ゆったりペースのジョギングばかりで速いペースでは全然走れていません。体力の衰えも隠せないし・・・と不安を抱いてのスタートとなりました。
 最初の関門(12.7km)は1時間15分以内。2年前は1時間11分だったので少し緩くなったようですが、それでもやはりプレッシャー。まあ、それでも走り出してみたら何とかなるもので、周りのランナーに合わせて走っているうちに自分のペースをつかめてきて関門にかかることはありませんでした。
 このコースは100m弱の高低差がありますが、10kmかけて登るといった緩いものなので個人的には特に苦にはなりませんでしたが、それより今回は風が気になりました。コース設定は、大まかにいって前半に南東に向かい後半が北西に向かうというものなのですが、この時期は北西の風が吹くことが多いですから、きつくなる後半に向かい風と戦うことになります。アメダスの記録を見ると、午後1時時点で北北西の風5.3m・・・強風という程ではないですが、軽量級木の葉ランナーには負担となりました。
 あまり良いタイムでは走れませんでしたが、制限時間に引っかかることなく完走出来たので良かったです。

 いい大会だと思いますが、体力的に年々きつくなってきているので、今後は制限時間が長くて精神的に楽な大会の方がいいかなあ・・・。
 
 ちなみに今年の参加賞のTシャツはピンクでした。「こんな色じゃ着れないよ!」と言う男性ランナーもいそうですが、ピンクといってもマジェンタに近い濃い色なので個人的には全然問題ありません。
 書かれているフレーズが何とも硬派でいいですね。

参加賞のTシャツ

参加賞のTシャツ
今年はピンクです


参加賞(その2)唐辛子

参加賞(その2)唐辛子
大田原は唐辛子料理で町おこしをしているそうです。

コメント   

 空間分析といっても手法は様々であり、取っ掛かりがなかなかつかめません。
 「何から始めたらいいものか・・・」と思っていた所、d3.jsにはボロノイ図を作成するための関数が存在することを知りました。

 ボロノイ図とは「複数あるポイントのうち、最寄りとなるポイントは何処か?」という視点で、平面空間を領域に分けたものです(下図参照)。

ボロノイ図

ボロノイ図


 この例ですと、緑色の領域内ではFが最寄りのポイントとなります。
 新聞、雑誌、Web等の媒体でボロノイ図を見かけることはほとんどないので実際にどのような用途で利用されているのかは分かりませんが、イメージ的にはスーパーやコンビニの出店や携帯のアンテナ設置等の計画を立てる際にツールとして利用出来そうです。

 上図のようなシンプルなボロノイ図を出力するソースを以下に記載します。

<!DOCTYPE HTML>
<html>
<head>
<title>ボロノイ図サンプル</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="js/d3.js"></script>
</head>

<body style="margin:0px;" onload="draw_voronoi()">
  <div style="padding:10px;">ボロノイ図サンプル</div>
</body>
</html>

<script type="text/javascript">

  var svg;
  var svgWidth =  400;
  var svgHeight = 300;
  var svgPadding = 5;

  function draw_voronoi() {

     svg = d3.select("body").append("svg")
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .attr("id", "svg")
        .style("margin-left","25px")

     var point = [[60,210],[120,90],[180,120],[240,240],[210,60],[300,150],[360,210]];

     var voronoiData = d3.geom.voronoi()
                           .clipExtent([[svgPadding, svgPadding], [svgWidth - svgPadding, svgHeight - svgPadding]]);

     var voronoiPath = svg.selectAll(".voronoi")
        .data(voronoiData(point))
        .enter()
        .append("path")
        .attr("class", "voronoi")
        .attr("d", function(d, i) { 
             return "M" + d.join("L") + "Z";
        })
        .style("stroke","#000099")
        .style("stroke-width","1")
        .style("fill", "none")

     var pointElm = svg.selectAll(".point")
        .data(point)
        .enter()
        .append("circle")
        .attr("cx", function(d,i) {
           return d[0];
        })
        .attr("cy", function(d,i) {
           return d[1];
        })
        .attr("r", 3)
        .style("fill", "#000000")
  }
</script>

 d3.geom.voronoi関数を使用すれば、ポイント情報をボロノイ図用に加工してくれます。

var voronoiData = d3.geom.voronoi()
                      .clipExtent([[svgPadding, svgPadding], [svgWidth - svgPadding, svgHeight - svgPadding]]);

 clipExtentは、ボロノイの作成範囲を指定するメソッドで必須ではありませんが、本家d3.jsのサイトでは使用を推奨しています。
 そして、

.data(voronoiData(point))

と、データを設定します。すると、各ポイントについてボロノイ領域を構成するポリゴンの座標が配列に収納されますので

.attr("d", function(d, i) {
   return "M" + d.join("L") + "Z";
})

 といった表記で、配列の各要素(座標)をLでつなげることにより、
  M140,5L165,75L245,115L355,5Z
 このような形式の、SVGのpath要素にポリゴン描画するための文字列を生成します。

 これらを応用して前回作成した東京都大気汚染常時監視測定局の地図をボロノイ図にしてみました。

東京都大気汚染常時監視測定局 ボロノイ図(1)

東京都大気汚染常時監視測定局 ボロノイ図(1)
このMAPを表示

 一応出来たのですが、見栄えが良くないです。ボロノイの線は東京都の輪郭内に限定出来ないものか・・・。
 いろいろ探して見つけたのが以下のサイト
  Areas of Clipped Voronoi Regions
 「Areas of Clipped~」とのタイトルの通り、ボロノイ図の描画エリアは地図の輪郭内に収まっています。当ページに公開されているソースを解読して、
  d3.geom.polygon().clip
 このメソッドで、クリップ(トリミングと言った方が分かりやすいでしょうか?)していることが分かりました。
 早速これを試してみたのですが・・・どうも当メソッドでクリップ出来る図形は1つだけのようでマルチポリゴンには対応していないようで(実際には対応しているのかもしれませんが実装方法が分かりません)、次のようなボロノイ図となりました。

東京都大気汚染常時監視測定局 ボロノイ図(2)

東京都大気汚染常時監視測定局 ボロノイ図(2)
このMAPを表示


 東京都のメインの輪郭は着色されていますが、中央防波堤付近の島は白のままとなっています(赤い丸の箇所)。この程度であれば無視してしまっても良いのですが、これでは佐渡ヶ島のある新潟県や、島だらけの長崎県などには適用出来そうにありません。また、上の図を良く見ると着色に関しては「クリップ」されているのですが、ボロノイの線に関しては所々でクリップエリアをはみ出してしまっています(青い丸の箇所)。

 他の方法を探った所、d3.js固有ではないのですがSVGにはクリップパス(clipPath)という切り抜き用の要素があることを知りました。
 まずはdefsタグ(表示対象ではない定義用の要素)を用意し、中にclipPathタグを入れてそこに切り抜きの「型」を記述します。そして、対象の図形にclip-path=”url(#クリップパスのID)”と付加することで切り抜きが実行されます。
 クリップパス実装のシンプルなソースを以下に記載します。

<!DOCTYPE HTML>
<html>
<head>
<title>SVG ClipPath Sample</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="js/d3.js"></script>
</head>

<body onload="draw_clip()">
   <div style="padding:10px;">ClipPath Sample</div>
</body>
</html>

<script type="text/javascript">

  var svg;
  var svgWidth =  500;
  var svgHeight = 300;

  function draw_clip() {

     svg = d3.select("body").append("svg")
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .attr("id", "svg")
        .style("margin-left","10px")
        .style("background-color","#808080")

     var defs = svg.append("defs");
     var clip = defs.append("clipPath")
        .attr("id", "clip-two-circle")

     clip.append("circle")
        .attr("cx", 125)
        .attr("cy", 150)
        .attr("r", 100)
     clip.append("circle")
        .attr("cx", 375)
        .attr("cy", 150)
        .attr("r", 100)

     svg.append("rect")
        .attr("clip-path", "url(#clip-two-circle)")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .style("fill", "#0000ff")
     svg.append("rect")
        .attr("clip-path", "url(#clip-two-circle)")
           .attr("x", 100)
           .attr("y", 120)
           .attr("width", 300)
           .attr("height", 60)
           .style("fill", "#ff0000")
  }
</script>

 まずは、

     var defs = svg.append("defs");
     var clip = defs.append("clipPath")
        .attr("id", "clip-two-circle")

 とsvgにdefs要素、さらにその中にclipPath要素を生成し、clipPathのIDをclip-two-circleとしました。
 このclipPath要素に

     clip.append("circle")
        .attr("cx", 125)
        .attr("cy", 150)
        .attr("r", 100)
     clip.append("circle")
        .attr("cx", 375)
        .attr("cy", 150)
        .attr("r", 100)

 と円を2つ横に並べていますが、これらは全てdegs要素内で定義しているにすぎないので、これだけでは何も表示されません。
 実際に表示させているのは、

     svg.append("rect")
        .attr("clip-path", "url(#clip-two-circle)")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .style("fill", "#0000ff")
     svg.append("rect")
        .attr("clip-path", "url(#clip-two-circle)")
           .attr("x", 100)
           .attr("y", 120)
           .attr("width", 300)
           .attr("height", 60)
           .style("fill", "#ff0000")

 この部分。SVG全体と同サイズ(幅・高さとも)の大きな四角形(青)と300×60の小さな四角形(赤)を描画し、両図形とも
.attr(“clip-path”, “url(#clip-two-circle)”)
 とすることで、clipPathで定義した形に切り抜いています。結果として出力される図形は下画像の通りです。

SVG ClipPath利用の図形例

SVG ClipPath利用の図形例
この図を表示

 このclipPathを先の東京都大気汚染常時監視測定局のボロノイ図に実装してみました。

・ObservationPointsTokyoVoronoi.html

<!DOCTYPE html>
<html>
<head>
<title>東京都大気汚染常時監視測定局 ボロノイ図</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="js/d3.js"></script>
<script type="text/javascript" src="js/queue.v1.js"></script>
<script type="text/javascript" src="js/ObservationPointsTokyoVoronoi.js"></script>
</style>
</head>

<body style="margin:0px;" onload="initialize()">
  <div  style="background-color:#006666;padding: 10px;font-size: 16px;font-weight: bold;color: #FFFFFF;">東京都大気汚染常時監視測定局 ボロノイ図</div>
</body>
</html>

・ObservationPointsTokyoVoronoi.js

// *************************************************************** 
//  D3.jsで空間分析にチャレンジ
// *************************************************************** 

  var svg;
  var mapScale = 50000;  //マップスケール
  var mapWidth = 1000;  //マップ幅
  var mapHeight = 500; //マップ高さ
  var mapPath;
  var vcolor = d3.interpolateHsl("#66ff00", "#ff0000");
  var pMin = 0.045;  //濃度 Min
  var pMax = 0.080;  //濃度 Max
  var jsonCityFileName = "json/CityBorderTokyo.json?=" + new Date().getTime();
  var jsonOutlineFileName = "json/OutlineTokyo.json?=" + new Date().getTime();
  var pointFileName = "csv/T_Place_AirPollution.csv?" + new Date().getTime();
  var prefBorder = new Array();

  function initialize() {
     queue()
        .defer(d3.json, jsonCityFileName)
        .defer(d3.json, jsonOutlineFileName)
        .defer(d3.csv, pointFileName)
        .await(ready);
  }

  function ready(error, border, outline, observepoint) {

     svg = d3.select("body").append("svg")
        .attr("width", mapWidth)
        .attr("height", mapHeight)
        .attr("id", "svg")

     var mapbounds = d3.geo.bounds(border);
     var mapcenter = [(mapbounds[0][0]+mapbounds[1][0])/2, (mapbounds[0][1]+mapbounds[1][1])/2];
     
     mapPath = d3.geo.path()
        .projection(d3.geo.mercator()
        .center(mapcenter)
        .translate([mapWidth/2, mapHeight/2])
        .scale(mapScale)
     );

     outline.features.forEach(function(d,i) {	
        prefBorder[i] = new Array();
        d["geometry"]["coordinates"][0].forEach(function(dd,j) {
           prefBorder[i].push(mapPath.projection()([parseFloat(dd[0]),parseFloat(dd[1])]));
        });
     });

     setVoronoi(observepoint);

     svg.selectAll(".outline")
        .data(outline.features)
        .enter()
        .append("path")
        .attr("d", mapPath)
        .style("fill", "none")
        .attr("class", "outline")
        .style("stroke", "#808080")
        .style("stroke-width", "1")

     svg.selectAll(".cityborder")
        .data(border.features)
        .enter()
        .append("path")
        .attr("d", mapPath)
        .style("fill", "none")
        .attr("class", "cityborder")
        .style("stroke", "#808080")
        .style("stroke-width", "0.3")

     setMapLegend(mapbounds);

  }

  function setVoronoi(point) {

     var ObservePoint = svg.selectAll(".observepoint")
        .data(point)
        .enter()
        .append("g")
        .attr("class","observepoint")
   
     var strClip = "";
     for (var p = 0; p < prefBorder.length; p++) {
        strClip += mkPolygonPath(prefBorder[p]);
     }

     var defs = svg.append("defs");
     var clipBox = defs.append("clipPath")
        .attr("id", "clip")
        .append("path")
        .attr("d",function() { return strClip;})

     var voronoiData = d3.geom.voronoi(pickupCoordinates(point));

     var voronoiPath = ObservePoint.append("path")
        .attr("class", "voronoi")
        .attr("d", function(d, i) { 
           return(mkPolygonPath(voronoiData[i])); 
        })
        .attr("clip-path", "url(#clip)")
        .style("stroke","#000099")
        .style("stroke-width","0.5")
        .style("fill", function(d, i) { 
             var fcolor = vcolor((parseFloat(point[i].SPM2p) - pMin) / (pMax - pMin));
             return(fcolor)
        })

     var pointElm = ObservePoint.append("circle")
        .attr("cx", function(d,i) {
           return mapPath.projection()([d.Lng,d.Lat])[0];
        })
        .attr("cy", function(d,i) {
           return mapPath.projection()([d.Lng,d.Lat])[1];
        })
        .attr("r", 3)
        .style("fill", "#000099")
        .on('mouseover', function(d) {
           d3.select(this).style("fill","#ff0000");
           curText = d.PointName + " " + parseFloat(d.SPM2p).toFixed(3);
           SetTooltip(mapPath.projection()([d.Lng,d.Lat]),curText,"3");
        })
        .on('mouseout', function() {
           d3.select(this).style("fill","#000099");
           var tooltip = svg.select(".tooltip")
           if(!tooltip.empty()) {
              tooltip.style("visibility", "hidden")
           }
        })
  }

  function pickupCoordinates(point) {
     var coordinatesArray = new Array;
     for (var i = 0; i < point.length; i++) {
        coordinatesArray.push(mapPath.projection()([point[i].Lng,point[i].Lat]));
     }
     return coordinatesArray;
  }

  function mkPolygonPath(d) {
     return "M" + d.join("L") + "Z";
  }

  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] + 5;
     if (tipleft + rectWidth > mapWidth) {
        tipleft = mapWidth - rectWidth;
     }
     var tiptop = tipXY[1] - rectHeight -5;

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

     tooltip.select("text")
        .text(tipText + "mg/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)

  }

  function setMapLegend(bounds) { 

    var fontsize = 14;
     var legendWidth = 200;
     var legendHeight = 20;
     var legendLeft = 30;
     var legendTop = mapPath.projection()(bounds[0])[1] - 60;
     var valuerange = d3.range(pMin,pMax+0.0001,(pMax-pMin)/(legendWidth-1));

     var MapLegend = d3.select("svg")
        .append("g")
        .attr("id","maplegend")
        .attr("transform", "translate("+legendLeft+","+legendTop+")")

     MapLegend
        .append("circle")
        .attr("r", 3)
        .style("fill", "#0000ff")
        .on('mouseover', function(d) {
           d3.select(this).style("fill","#ff0000");
        })
        .on('mouseout', function() {
           d3.select(this).style("fill","#0000ff");
        })

     MapLegend
        .append("text")
        .attr("id","text1")
        .style("font-size",fontsize + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")
        .attr("transform", "translate("+10+","+ (fontsize * 0.5 - 2) +")")
      .text("大気汚染常時監視測定局(一般局)")

     MapLegend
        .append("text")
        .attr("id","text2")
        .style("font-size",(fontsize - 2) + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")
        .attr("transform", "translate("+10+","+ (fontsize * 1.5) +")")
      .text("※マウスオンすると測定局の名称とSPM濃度(2013年度2%除外値)が表示されます。")

     MapLegend.selectAll(".legend")
        .data(valuerange)
        .enter()
        .append("rect")
        .attr("class", "legend")
        .attr("transform", function(d, i) {
           return "translate("+((fontsize*10)+i)+",30)";
        })
        .attr("width",1)
        .attr("height",legendHeight)
        .style("fill", function(d, i){
           return vcolor((d - pMin) / (pMax - pMin));
        })

     MapLegend
        .append("rect")
        .attr("id","legendframe")
        .attr("transform", "translate("+(fontsize*10)+",30)")
        .attr("width",legendWidth)
        .attr("height",legendHeight)
        .style("fill", "none")
        .style("stroke","#303030")
        .style("stroke-width",0.5);

     MapLegend
        .append("text")
        .attr("id","text3")
        .style("font-size",fontsize + "px")
        .style("font-family","sans-serif")
        .style("fill","#000000")
        .attr("transform", "translate(0,"+ (30 + (fontsize + legendHeight) * 0.5) +")")
      .text("SPM濃度(2%除外値)")

     var MapLegendAxis = MapLegend
        .append("g")
        .attr("transform", "translate("+(fontsize*10)+","+ (30 + legendHeight) +")")
        .attr("class","axis")
        .style("stroke","none")
        .style("fill","#303030")

     MapLegendAxis
        .append("rect")
        .attr("transform", "translate(0,10)")
        .attr("width",legendWidth)
        .attr("height",1)

     var MapLegendAxisX = MapLegendAxis.selectAll(".axis")
        .data([d3.min(valuerange),d3.max(valuerange)])
        .enter()
        .append("g")

     MapLegendAxisX
        .append("rect")
        .attr("transform", function(d,i) {
           return "translate(" + (i * legendWidth) + ",5)";
        })
        .attr("width",1)
        .attr("height",10)

     var pFormat = d3.format(".3f");

     MapLegendAxisX
        .append("text")
        .attr("transform", function(d,i) {
           return "translate("+(i * legendWidth)+","+(15+(fontsize+2))+")";
        })
        .attr("text-anchor","middle")
        .attr("class","axistext")
      .attr("id",function(d,i) {
           return "axistext"+i;
        })
        .style("font-size",(fontsize-2) + "px")
        .style("font-family","sans-serif")
      .text(function(d,i) {
           return pFormat(d) +"mg/m";
        })

     MapLegendAxisX.selectAll("text")
        .append("tspan")
        .attr("class","super")
        .attr("dy","-2")
        .style("font-size",(fontsize-4)+"px")
        .text("3")

  }

 クリップパス以外では、上で紹介したサイト「Areas of Clipped Voronoi Regions」でボロノイ領域の面積に応じて色を変化させているのを参考に、各測定局の濃度によってボロノイ領域の色を緑(低濃度)→赤(高濃度)へと変化させてみました。
 また、上記ソースではD3.js以外にライブラリ「Queue.js」を利用しています。これはcsvやjson等の外部ファイルの読み込みが完了するまで処理待ちをしてくれるものです。
  Queue.jsダウンロードページ(GitHub):https://github.com/mbostock/queue 

 出来上がったのが次の図です。

東京都大気汚染常時監視測定局 ボロノイ図(3)

東京都大気汚染常時監視測定局 ボロノイ図(3)
このMAPを表示


 ようやく「らしい」図になりました。
 
 しかし、ここまではSVGやd3.jsのメソッドを利用しているだけで、統計処理の計算は全く行っていません。次回はもう少し本格的な計算をしてみたいと思います。

コメント   

 2014年11月1日(土)
 パナソニック汐留ミュージアムの「ジョルジョ・デ・キリコ -変遷と回帰-」を観覧してきました。
 

「ジョルジョ・デ・キリコ -変遷と回帰-」ちらし

「ジョルジョ・デ・キリコ -変遷と回帰-」ちらし

 20代の頃の初期作品から80代後半に描いた晩年作品まで、デ・キリコの作品ばかりが約100点。
 これだけ活動期間が長いと、最初と最後とでは画風が全く異なるケースも多いと思うのですが、デ・キリコの場合は初期からして一目でキリコ作品と分かる形而上絵画です。その後は一旦古典主義に回帰し、後期になって形而上絵画がさらに発展した形で戻ってくるという、少し風変わりな変遷をたどっています。

 顔のない甲冑風のマネキンは、無機質な存在なのに何故か感情を持っているかのよう。度々登場する古代ギリシャ風の建造物や彫刻、印象的な影・・・不思議な世界に迷い込んでしまったかのような感覚になります。

 デ・キリコはシュルレアリスムを代表する画家の1人かと思っていたのですが、本展では全体を通して「形而上絵画」として紹介されていて「シュルレアリスム」という言葉は解説にも全く登場しません。形而上絵画はシュルレアリスム誕生に大きな影響を与えたが、デ・キリコはあくまでも形而上絵画の画家でありシュルレアリスムとは一線を画する・・・本展には、そんなこだわりを感じました。

 ここ、パナソニック汐留ミュージアムはいつ訪問してもスタッフの方々の対応が素晴らしいです。
 12月26日(金)まで。デ・キリコ独特の世界を楽しめます。

コメント