From charlesreid1

Installing

First step was prerequisites.

Prerequisites

I had to install GDAL (software for converting different geographic formats) and NPM (node package manager):

brew install gdal
brew install npm

Next, I used npm to install TopoJSON:

npm -g topojson

Then I checked that the installations went ok with:

which ogr2ogr
which topojson

Converting

GeoJson to TopoJson

To convert a GeoJson to a TopoJson and preserve properties:

topojson in.geojson -o out.json -p

Once I converted my GeoJson into TopoJson I was ready to start creating a map from it in D3.

My First D3 Map

I started with a basic map tutorial from Mike Bostock, creator of D3: http://bost.ocks.org/mike/map/

Initially I wasn't able to get it working, but it was because I was using GeoJson. Switching to TopoJson helped me make sense of some of the Javascript - there's a lot that's happening implicitly even in the simplest D3 examples. Opening the TopoJson to understand its structure and keys, how properties were stored in it, made the D3 more clear.

Here is the final map created for the tutorial: http://bost.ocks.org/mike/map/step-7.html

The main point that gave me trouble was figuring out the projection. With Leaflet, I can pass latitude and longitude to center the map on my region of interest. But this wasn't possible (or, more likely, I couldn't figure out how to do it) with D3.

I ended up using a US projection, which led to this funny rotation of California, out there on the left coast:

D3 CA Rotation.png

When I tried rotating or transforming it, it would disappear for any setting more than very tiny numbers, and it is really inefficient to position a map like you draw with an etch-a-sketch. Need to figure out how to use latitude and longitude. And maybe how to add tiles?


Step 1: The Data

First, I was using a GeoJson file from the US Census. This encoded some data about method of commuting to work by poverty status. I wanted to test whether I could preserve properties encoded in the GeoJson when I converted it to TopoJson, since the whole point of using D3 is to take advantage of its data-crunching capabilities.

When I ran the topojson command to convert GeoJson to TopoJson, I used the -p flag.

Step 2: The D3 Javascript

I began with specifying information about the canvas, projection, and setup of D3:

var width = 800,
    height = 600;

var projection = d3.geo.albersUsa()
    .scale(1400)
    .translate([width*3/4, height/2]);

var path = d3.geo.path()
    .projection(projection)
    .pointRadius(2);

// This is inserted as <body> is being constructed,
// so <svg> occurs wherever this .js file is called.
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

Next, we iterate over each TopoJson feature and extract properties. We use the geoid property to create a unique style for each California county. If we had multiple states, we could give each state's counties unique CSS classes. This would ease changing styles on a state-by-state basis. I've also encoded the geoid, which is unique to that county, so that we can map colors to data, or pick out one particular county (as with San Bernadino County in the D3 example on a-shrubbery).

var prefix = "/";
// this Json file is TopoJson:
d3.json(prefix+"d3basicmap.json", function(error, ca) {

  //console.log(ca);
  var subunits = topojson.feature(ca, ca.objects.d3basicmap);
  console.log(subunits);

  svg.selectAll(".subunit")
      .data(subunits.features)
    .enter().append("path")
      .attr("class", function(d) { return "subunit subunit" + d.properties.geoid; })
      .attr("d", path);
});

This can all be found on Github: https://github.com/charlesreid1/a-shrubbery/blob/master/pelican/maps/d3basicmap.js

Step 3: D3 HTML

I added all of this to an HTML document as follows.

The script above appends an <svg> tag to the <body> tag wherever I place the <script>.

I add the following to an HTML document, wherever I want my D3 content to go:

<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>

<script src="{ { SITEURL } }/d3basicmap.js" type="text/javascript"></script>

where <code>{{ SITEURL }}</code> is Jinja template markup, part of using Pelican to manage all the maps on the page.

Pelican/Jinja template on Github here: https://github.com/charlesreid1/a-shrubbery/blob/master/pelican/maps/d3basicmap.html


Workflow: D3 for Topo Maps

If this works, it'll be great:

http://stackoverflow.com/questions/18300527/d3js-how-to-design-topographic-maps

I'm going to test it out on visualizing contours of the Barry Goldwater bombing range in southwest Arizona, using data obtained from the National Map Viewer.

Requirements

The page lists the following requirements:

  • make
  • curl
  • unzip
  • gdal
  • nodejs
  • topojson
  • ogr

I used homebrew to install most of these...

Creating make file

Next step was to dump all the shapefiles in a working directory, and create a makefile there:

$ ls barrygoldwater/
Elev_Contour.dbf
Elev_Contour.prj
Elev_Contour.shp
Elev_Contour.shx
topo.mk

My makefile was significantly simpler than the one given in the example, mainly because I was dealing with a single shapefile and not doing all of the tif contour rastering nonsense. Here's my topo.mk:

# topojsoning:
final.json:  Elev_Contour.json
        topojson --id-property none --simplify=0.5 -p -o barrygoldwater_new.json -- Elev_Contour.json
        # simplification approach to explore further. Feedbacks welcome.

# shp2jsoning:
Elev_Contour.json:  Elev_Contour.shp
        ogr2ogr -f GeoJSON Elev_Contour.json Elev_Contour.shp

clean:
        rm `ls | grep -v 'zip' | grep -v 'Makefile'`
# Makefile v4b (@Lopez_lz)

Don't forget, in Vim you can use Control + T in insert mode to insert hard tabs, just in case you've told vim to automatically use spaces everywhere it sees tabs. Makefiles MUST use tabs, or else they'll throw errors. No four-spaces allowed!

I ran into a problem with the ogr2ogr command:

$ make -f ./topo.mk
ogr2ogr -f GeoJSON Elev_Contour.json Elev_Contour.shp
dyld: Library not loaded: /usr/local/lib/libproj.0.dylib
  Referenced from: /usr/local/bin/ogr2ogr
  Reason: image not found
make: *** [Elev_Contour.json] Trace/BPT trap: 5

This ended up being a problem with the brew installed version of GDAL (see this issue on Github: [1]).

I fixed it with:

$ brew uninstall proj
$ brew install proj
Xcode can be updated from the App Store.
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/proj-4.8.0.mountain_lion.bottle.tar.gz
Already downloaded: /Library/Caches/Homebrew/proj-4.8.0.mountain_lion.bottle.tar.gz
==> Pouring proj-4.8.0.mountain_lion.bottle.tar.gz
Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /usr/local
You can try again using:
  brew link proj

$ brew link proj
Linking /usr/local/Cellar/proj/4.8.0...
Error: Could not symlink bin/proj
Target /usr/local/bin/proj
already exists. You may want to remove it:
  rm /usr/local/bin/proj

To force the link and overwrite all conflicting files:
  brew link --overwrite proj

To list all files that would be deleted:
  brew link --overwrite --dry-run proj

$ brew link --overwrite proj
Linking /usr/local/Cellar/proj/4.8.0... 42 symlinks created

Hooray! Now it will work:

$ make -f ./topo.mk
ogr2ogr -f GeoJSON Elev_Contour.json Elev_Contour.shp
topojson --id-property none --simplify=0.5 -p -o barrygoldwater_new.json -- Elev_Contour.json
bounds: -114.0000000001562 32 -112.99999999962512 32.99999999981907 (spherical)
pre-quantization: 0.111m (0.00000100°) 0.111m (0.00000100°)
topology: 34880 arcs, 4083014 points
post-quantization: 11.1m (0.000100°) 11.1m (0.000100°)
simplification: retained 69760 / 3702887 points (2%)
prune: retained 34880 / 34880 arcs (100%)

Init D3 HTML Page

Now time for making the D3 HTML page.

I copied and pasted most of what the example gave, with a few small modifications for my own data. Here's the final HTML file, barrygoldwater.html:

<style>
svg { border: 5px solid #333; background-color: #C6ECFF;}

/* TOPO */
path.Topo_1 { fill:#ACD0A5; stroke: #0978AB; stroke-width: 1px; }
path.Topo_50 {fill: #94BF8B; }
path.Topo_100 {fill: #BDCC96; }
path.Topo_200 {fill: #E1E4B5; }
path.Topo_500 {fill: #DED6A3; }
path.Topo_1000 {fill:#CAB982 ; }
path.Topo_2000 {fill: #AA8753; }
path.Topo_3000 {fill: #BAAE9A; }
path.Topo_4000 {fill: #E0DED8 ; }
path.Topo_5000 {fill: #FFFFFF ; }
.download { 
  background: #333; 
  color: #FFF; 
  font-weight: 900; 
  border: 2px solid #B10000; 
  padding: 4px; 
  margin:4px;
}
</style>
<body>
<script src="http://code.jquery.com/jquery-2.0.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>


// 1. -------------- SETTINGS ------------- //
// Geo-frame_borders in decimal degrees
var WNES = { "W": 32.0, "N":-113.0, "E": 33.0, "S": -114.0 };

// Geo values of interest :
var latCenter = (WNES.S + WNES.N)/2,
    lonCenter = (WNES.W + WNES.E)/2,
    geo_width = (WNES.E - WNES.W),
    geo_height= (WNES.N - WNES.S);
// HTML expected frame dimensions
var width  = 600,
    height = width * (geo_height / geo_width);

// Projection: projection, reset scale and translate
var projection = d3.geo.equirectangular()
      .scale(1)
      .translate([0, 0]);

// SVG injection:
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

// Path
var path = d3.geo.path()
    .projection(projection)
    .pointRadius(4);

// Data (getJSON: TopoJSON)
d3.json("barrygoldwater_new.json", showData);

// 2. ---------- FUNCTION ------------- //
function showData(error, contours) {

    var contour = topojson.feature(contours, contours.objects.Elev_Contour);

    var onecontour = contour.features.filter(function(d) {
        return d.properties.CONTOURELE === 1840;
    });
    
    // Focus area box compute for derive scale & translate.
    // [​[left, bottom], [right, top]​] // E   W    N   S
    var b = path.bounds(contour[0]),
        s = 1 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
        t = [(width  - s * (b[1][0] + b[0][0])) / 2, 
             (height - s * (b[1][1] + b[0][1])) / 2];

    console.log("bounds = "+b);
    
    // Projection update
    projection
        .scale(s)
        .translate(t);

    console.log("Scale factor = "+s);

    // black border around map
    svg.append("rect").attr('width', width).attr('height', height)
      .style('stroke', 'black').style('fill', 'none');

    svg.selectAll("path")
        .data(contour.features)
        .enter().append("path")
        .attr("d",path)
        .attr("class", function(d) { return "contour contour" + d.properties.CONTOURELE; })
        .style("stroke-width", "1")
        .style("stroke", "black")

}
</script>
<br />
<div>
    <a class="download ac-icon-download" href="javascript:javascript: (function () { var e = document.createElement('script'); if (window.location.protocol === 'https:') { e.setAttribute('src', 'https://raw.github.com/NYTimes/svg-crowbar/gh-pages/svg-crowbar.js'); } else { e.setAttribute('src', 'http://nytimes.github.com/svg-crowbar/svg-crowbar.js'); } e.setAttribute('class', 'svg-crowbar'); document.body.appendChild(e); })();"><!--⤋--><big></big> Download</a> -- Works on Chrome. Feedback me for others web browsers ?
</div>
<br />
</body>
</html>

This failed. I kept seeing errors about the path elements, as well as problems with the computed bounds and scales:

D3 Centering Fail1.png

D3 Centering Fail2.png


I finally ended up, once again, adjusting the whole map by hand to get the view centered on the interesting data. Only problem was, it wasn't that interesting...:

D3 Contour Init.png