2016-05-09

Rendering TopoJSON with D3.js demo (Vietnam - Population density in 2011 by province)

Hôm nay có chút việc phải render cái map VN, thể hiện 1 số thông số như doanh thu theo vùng chẳng hạn. Nếu đơn giản thì có thể dùng Google Maps như kiểu Fusion Tables Layer hay Data Layer cũng được. Nhưng nghĩ đi nghĩ lại bê Google Maps vào thì cũng kỳ, hơn nữa mục đích cũng ko đúng. Nên mình dùng D3.js cho cái này. Kết quả cuối cùng thì ở đây, cái map nhìn thấy cũng tạm tạm.


Trước có đọc cuốn Interactive Data Visualization for the Web, rồi dùng thử D3. Nhưng từ đó giờ chỉ xài cái Sankey của D3 còn cũng không dùng nhiều. Cuốn sách này giờ có thể đọc online. Ví dụ cũng trực quan dễ hiểu.


Note lại 1 số thứ khi thực hiện

Ví dụ phần mapping thì coi chapter 12, github nó ở đây https://github.com/alignedleft/d3-book/tree/master/chapter_12.

Đầu tiên là 1 số examples cần xem:
Let’s Make a Map
Chapter 12. GeomappingInteractive Data Visualization for the Web

Thường trong sách dùng dữ liệu của Mỹ, mình sẽ dùng data VN.

Data

Đầu tiên lấy 1 data về dân số mật độ dân số tại https://www.gso.gov.vn

Mình có edit lại thêm field ISO 3166 alpha 2 (ISO 3166-2 state codes for identifying the principal subdivisions e.g., provinces or states). Có thể dùng HASC (Hierarchical administrative subdivision codes) cũng được ko có vấn  đề gì: vn-population-2011.csv.

Dữ liệu như sau:

name population area density iso region
An Giang 2151 3536.7 608 VN-44 Mekong River Delta
Bà Rịa - Vũng Tàu 1027.2 1989.5 516 VN-43 South East
Bắc Giang 1574.3 3844 410 VN-54 North East

Sau đó là dự liệu 1 số thành phố (city, township) bao gồm lat, lng, level: vn-cities.csv. Ví dụ vài dòng:

name state code type alt_type lat lng level
An Khê VN-30 VN.GL.AK Thị xã Township 14.023333 108.691389 4
Bà Rịa VN-43 VN.BV.BR Thành phố City 10.509167 107.179722 2
Bắc Giang VN-54 VN.BG.BG Thành phố City 21.293333 106.188333 2
Bắc Kạn VN-53 VN.BK.BK Thành phố City 22.133333 105.852778 3
Bạc Liêu VN-55 VN.BL.BL Thành phố City 9.268056 105.752222 2
Bắc Ninh VN-56 VN.BN.BN Thành phố City 21.18 106.066111 2
Bảo Lộc VN-35 VN.LD.BC Thành phố City 11.546667 107.793611 3
Bến Tre VN-50 VN.BR.BE Thành phố City 10.238056 106.373889 3
Biên Hòa VN-39 VN.DN.BH Thành phố City 10.941944 106.847222 1

Cuối cùng là dữ liệu map. Đầu tiên download shapefile chẳng hạn tại GADM.
Sau khi download shapefile từ GADM convert nó sang GeoJSON và TopoJSON. Mình dùng TopoJSON nên cũng ko cần convert sang GeoJSON làm gì. Mở page http://mapshaper.org/, kéo 2 file VNM_adm1.dbf với VNM_adm1.shp vào page này. OK nó sẽ render ra cái bản đồ VN. Xong mình sẽ simplify cái data này, chọn Simplify > Lấy options giả sử Visvalingam / weighted area, kéo xuống khoảng 15-20% là ổn. Để ý nếu có line intersection thì repair.

http://www.gadm.org/
GADM is a spatial database of the location of the world's administrative areas (or adminstrative boundaries) for use in GIS and similar software.

http://mapshaper.org/
A tool for topologically aware shape simplification. Reads and writes Shapefile, GeoJSON and TopoJSON formats.


Xuất ra file TopoJSON, khi đó khoảng chừng 200KB. Mình có thể mở bằng Notepad++ chẳng hạn, dùng JSON Viewer plugin format lại cho dễ nhìn. Find "objects" để đổi lại tên đang là tên của file mình kéo vào: "VNM_adm1" thành "states" chẳng hạn. Edit lại 1 số thông tin properties ví dụ:

{"name":"Trà Vinh","iso":"VN-51","hasc":"VN.TV","types":"Tỉnh","alt_types":"Province","capital":"Trà Vinh","region":8}

Dùng RegEx replace cũng khá nhanh. Trong data VNM_adm1 lưu ý có 2 entry cho tỉnh Ninh Bình và Kiên Giang (tách riêng đảo), nên chuyển qua TopoJSON mình gộp lại thành MultiPolygon luôn, còn 1 entry cho mỗi tỉnh thôi. File đã chuẩn bị xong: vn-states.json.

Mình phát hiện ra 1 điều là Github nhận ra file GeoJSON và TopoJSON nên thực hiện viewer được luôn. Công nhận sp người ta làm chiều sâu dễ sợ.

Display data

Đầu tiên để code có thể dùng JSFiddle không thì nên tạo 1 local server để dev (xài node) vì không cho load file:// không cho request resource do CORS. OK tạo 1 folder, tạo file server.js để có 1 cái localhost xài (nếu không xài JSFiddle). Mình hay xài localhost server: server.js

var http = require("http"),
    url = require("url"),
    path = require("path"),
    fs = require("fs")
    port = process.argv[2] || 8888;
 
var contentTypes = {
    '.html': 'text/html; charset=utf-8',
    '.js':   'text/javascript; charset=utf-8',
    '.json':   'application/json; charset=utf-8',
    '.css':   'text/css; charset=utf-8',
 '.png': 'image/png',
 '.gif': 'image/gif',
 '.jpeg': 'image/jpeg', 
 '.jpg': 'image/jpeg',
 '.xml': 'application/xml; charset=utf-8',
 '.kml': 'application/vnd.google-earth.kml+xml; charset=utf-8',
 '.kmz': 'application/vnd.google-earth.kmz',
};

http.createServer(function(request, response) {

  var uri = url.parse(request.url).pathname
    , filename = path.join(process.cwd(), uri);
  
  fs.exists(filename, function(exists) {
    if(!exists) {
      response.writeHead(404, {"Content-Type": "text/plain"});
      response.write("404 Not Found\n");
      response.end();
      return;
    }

    if (fs.statSync(filename).isDirectory()) { 
  filename += '/index.html';
 }

    fs.readFile(filename, "binary", function(err, file) {
      if(err) {        
        response.writeHead(500, {"Content-Type": "text/plain"});
        response.write(err + "\n");
        response.end();
        return;
      }

      response.writeHead(200, {"Content-Type": contentTypes[path.extname(filename)]});
      response.write(file, "binary");
      response.end();
    });
  });
}).listen(parseInt(port, 10));

console.log("Static file server running at\n  => http://localhost:" + port + "/\nCTRL + C to shutdown");

Nếu xài JSFiddle thì file data để trên Github xong load raw là ổn.


OK chuẩn bị xong.

Các ví dụ ban đầu thì xài GeoJSON, còn load TopoJSON thì cũng như vậy. Sau khi load json file thì chuyển ngược lại thành GeoJSON để display

// While our data can be stored more efficiently in TopoJSON,
// we must convert back to GeoJSON for display.
var features = topojson.feature(json, json.objects.states).features;

Quantile scale

Để color cho tỉnh theo density thì mình dùng scale quantile (colorful hơn là quantize). Tức là chia 63 thành 9 nhóm. Sau đó push hết density vào domain. Nếu xài quantize() thì map domain là min và max của các density. Không thì xài linear cho đơn giản.

Demostration cho scale Quantile, Quantize, Threshold Scales

Xem thêm bài d3: scales, and color. của Jerome Cukier.

// We create a quantile scale to categorize the values in 9 groups.
// The domain is static and has a minimum/maximum of population/density.
// The scale returns text values which can be used for the color CSS
// classes (q0-9, q1-9 ... q8-9)
var quantiles = d3.scale.quantile()
  .range(d3.range(9).map(function(i) {
    return 'q' + i + '-9';
  }));

...

// Set the domain of the values
quantiles.domain(features.map(function(d) {
      return d.properties.density;
    }));

g.
...
.attr('class', function(d) {
      // Use the quantiled value for the class
      return 'feature ' + quantiles(d.properties.density);
    }) // add attribute class and fill with result from quantiles

Projection

Projection function chuyển từ tọa độ geo lat, lng sang Cartesian. Mặc định projection của D3 là Albers USA (albersUsa). Projection này thực hiện chuyển các vùng như Alaska và Hawaii về hiển thị phía góc dưới bên phải của bản đồ nước Mỹ, đồng thời center map vào bản đồ nước Mỹ. Scale mặc định là 1000, nếu muốn tăng hay giảm scale thì thực hiện điều chỉnh. Mình chọn center là Sài Gòn.

var width = 960,
  height = 600;
var center = [106.34899620666437, 16.553160650957434];
var scale = 4000;
var offset = [width / 2, height / 2 - 300];

// The projection function takes a location [longitude, latitude]
// and returns a Cartesian coordinates [x,y] (in pixels).
//
// D3 has several built-in projections. Albers USA is a composite projection
// that nicely tucks Alaska and Hawaii beneath the Southwest.
//
// Albers USA (albersUsa) is actually the default projection for d3.path.geo()
// The default scale value is 1,000. Anything smaller will shrink the map;
// anything larger will expand it.
//
// Add a scale() method with 800 to our projection in order to shrink things down a bit
// var projection = d3.geo.albersUsa()
//                      .translate([w/2, h/2]).scale([800]);
projection = d3.geo.mercator()
 .translate(offset)
 .scale([scale])
 .center(center);

// We define our first path generator for translating that
// mess of GeoJSON coordinates into even messier messes of SVG path codes.
// Tell the path generator explicitly that it should reference our customized
// projection when generating all those paths
path = d3.geo.path()
 .projection(projection);

Boundary và mess

Thực hiện merge tất cả các geometries của states loại bỏ interior borders thành country boundary. Sau đó dùng topojson.mesh để có mesh giữa các tỉnh. Format bằng CSS sẽ có kết quả OK.

var boundary = g.append('g')
    .attr('class', 'boundary');

// Country boundary from merge all geometries
boundary.append('path')
    .attr('class', 'country-boundary')
    .datum(topojson.merge(json, json.objects.states.geometries))
    .attr('d', path);

...

// State mesh
boundary.append('path')
    .attr('class', 'state-boundary')
    .datum(topojson.mesh(json, json.objects.states, function(a, b) {
        return a !== b;
    })).attr('d', path);

CSS lưu ý fill là none. Trong bước chuẩn bị dữ liệu nếu line intersections quá nhiều thì mess sẽ rất tệ.


.country-boundary {
  fill: none;
  stroke: #37C3BC;
  stroke-linejoin: round;
}

.state-boundary {
  fill: none;
  stroke: #003568;
  stroke-dasharray: 5, 3;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

Data bound

Về data binding của D3, nếu parent sử dụng function data() với array sau đó enter().append() thì data của các node child là item trong array.

Nếu child node có append các node cháu thì data bound của các node cháu lúc đó cũng là item của child node. Khi đó sử dụng .text(function(d) {}) thì d là item đó.

Nếu muốn set data bound cho 1 node thì dùng function datum(). Nếu dùng datum() thì có thể forEach array và dùng datum() thay vì data binding với data().

Zooming, text ...

Phần cuối cùng zooming thì follow theo ví dụ của Mike Bostock. Text thì nên append cuối cùng vào SVG để ko bị che, đè. Điều chỉ zooming text về font-size, stroke-width + CSS.

Github: https://github.com/kierandg/d3tuts
JSFiddle: https://jsfiddle.net/kierandg/5gxacnj2/

No comments:

Post a Comment