block by bycoffe 5871252

Town/county map using d3 and TopoJSON

Full Screen

This is a demonstration of how to create a combination town/county map from a shapefile using TopoJSON and d3.js.

It includes a simplified version of the code used for the Massachusetts special Senate election results on The Huffington Post.

Get the data

Download a shapefile of Massachusetts towns from the state’s GIS site:

wget http://wsgw.mass.gov/data/gispub/shape/census2000/towns/census2000towns_poly.exe

Unzip the file:

unzip census2000towns_poly.exe

Project the shapefile

We then create a new shapefile with projected coordinates, using an appropriate projection for Massachusetts. Read more here about projected TopoJSON

ogr2ogr -f 'ESRI Shapefile' -t_srs 'EPSG:3585' towns-projected.shp census2000towns_poly.shp

Convert the shapefile to TopoJSON

We already know the dimensions we want for our map: 615x375. Since we’re using a projected shapefile, we can specify the width and height when we generate the TopoJSON, and then do not have to reproject and resize the map in the browser.

We also need to know which county each town is in, so we can show a map of counties as well as towns. The shapefile doesn’t have county information, though, so we need to add it. We’ll do that by using an external properties file that contains a list of town IDs (which do appear in the shapefile) and the FIPS code for the town’s county.

We’re also going to simplify the town shapes.

topojson --width=615 --height=373 -s 10 --id-property +TOWN_ID -e county_towns.csv -p town=town,fips=fips -o ma-towns.topojson towns=towns-projected.shp

That gives us a 73K file, ma-towns.topojson. You can reduce file size further by changing the simplification threshold.

Displaying the TopoJSON

See the code below for the basics of displaying the TopoJSON file and toggling between showing towns and counties.

index.html

<!doctype html>
<meta charset="utf-8">
<html>
  <head>

    <title>Massachusetts town/county map</title>

    <style type="text/css">
      #map {
        width: 615px;
        height: 375px;
      }
    </style>


  </head>

  <body>

    <button id="toggle">Toggle</button>

    <div id="map"></div>

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

  </body>
</html>

county_towns.csv

id,town,fips
1,ABINGTON,25023
2,ACTON,25017
3,ACUSHNET,25005
4,ADAMS,25003
13,ASHFIELD,25011
5,AGAWAM,25013
6,ALFORD,25003
7,AMESBURY,25009
8,AMHERST,25015
14,ASHLAND,25017
9,ANDOVER,25009
10,ARLINGTON,25017
11,ASHBURNHAM,25027
12,ASHBY,25017
45,BROOKFIELD,25027
15,ATHOL,25027
16,ATTLEBORO,25005
17,AUBURN,25027
18,AVON,25021
19,AYER,25017
20,BARNSTABLE,25001
21,BARRE,25027
22,BECKET,25003
23,BEDFORD,25017
24,BELCHERTOWN,25015
70,DALTON,25003
25,BELLINGHAM,25021
26,BELMONT,25017
30,BEVERLY,25009
34,BOLTON,25027
27,BERKLEY,25005
28,BERLIN,25027
29,BERNARDSTON,25011
35,BOSTON,25025
36,BOURNE,25001
200,NEW ASHFORD,25003
31,BILLERICA,25017
32,BLACKSTONE,25027
33,BLANDFORD,25013
37,BOXBOROUGH,25017
38,BOXFORD,25009
39,BOYLSTON,25027
40,BRAINTREE,25021
43,BRIMFIELD,25013
41,BREWSTER,25001
42,BRIDGEWATER,25023
44,BROCKTON,25023
46,BROOKLINE,25021
47,BUCKLAND,25011
48,BURLINGTON,25017
49,CAMBRIDGE,25017
50,CANTON,25021
51,CARLISLE,25017
52,CARVER,25023
53,CHARLEMONT,25011
54,CHARLTON,25027
55,CHATHAM,25001
56,CHELMSFORD,25017
57,CHELSEA,25025
58,CHESHIRE,25003
59,CHESTER,25013
60,CHESTERFIELD,25015
61,CHICOPEE,25013
112,GRANVILLE,25013
62,CHILMARK,25007
63,CLARKSBURG,25003
64,CLINTON,25027
65,COHASSET,25021
66,COLRAIN,25011
67,CONCORD,25017
68,CONWAY,25011
69,CUMMINGTON,25015
71,DANVERS,25009
72,DARTMOUTH,25005
73,DEDHAM,25021
74,DEERFIELD,25011
120,HAMPDEN,25013
75,DENNIS,25001
76,DIGHTON,25005
77,DOUGLAS,25027
78,DOVER,25021
79,DRACUT,25017
80,DUDLEY,25027
81,DUNSTABLE,25017
82,DUXBURY,25023
83,EAST BRIDGEWATER,25023
84,EAST BROOKFIELD,25027
85,EAST LONGMEADOW,25013
86,EASTHAM,25001
87,EASTHAMPTON,25015
88,EASTON,25005
89,EDGARTOWN,25007
90,EGREMONT,25003
91,ERVING,25011
92,ESSEX,25009
93,EVERETT,25017
94,FAIRHAVEN,25005
121,HANCOCK,25003
95,FALL RIVER,25005
96,FALMOUTH,25001
97,FITCHBURG,25027
98,FLORIDA,25003
99,FOXBOROUGH,25021
100,FRAMINGHAM,25017
101,FRANKLIN,25021
102,FREETOWN,25005
113,GREAT BARRINGTON,25003
103,GARDNER,25027
104,AQUINNAH,25007
105,GEORGETOWN,25009
116,GROVELAND,25009
106,GILL,25011
107,GLOUCESTER,25009
108,GOSHEN,25015
109,GOSNOLD,25007
110,GRAFTON,25027
111,GRANBY,25015
114,GREENFIELD,25011
115,GROTON,25017
117,HADLEY,25015
118,HALIFAX,25023
119,HAMILTON,25009
122,HANOVER,25023
123,HANSON,25023
128,HAVERHILL,25009
129,HAWLEY,25011
124,HARDWICK,25027
125,HARVARD,25027
126,HARWICH,25001
127,HATFIELD,25015
130,HEATH,25011
131,HINGHAM,25023
132,HINSDALE,25003
133,HOLBROOK,25021
134,HOLDEN,25027
135,HOLLAND,25013
140,HUBBARDSTON,25027
136,HOLLISTON,25017
137,HOLYOKE,25013
138,HOPEDALE,25027
139,HOPKINTON,25017
141,HUDSON,25017
142,HULL,25023
143,HUNTINGTON,25015
144,IPSWICH,25009
145,KINGSTON,25023
148,LANESBOROUGH,25003
146,LAKEVILLE,25023
147,LANCASTER,25027
149,LAWRENCE,25009
150,LEE,25003
151,LEICESTER,25027
152,LENOX,25003
153,LEOMINSTER,25027
154,LEVERETT,25011
155,LEXINGTON,25017
156,LEYDEN,25011
157,LINCOLN,25017
158,LITTLETON,25017
159,LONGMEADOW,25013
160,LOWELL,25017
161,LUDLOW,25013
162,LUNENBURG,25027
163,LYNN,25009
164,LYNNFIELD,25009
165,MALDEN,25017
166,MANCHESTER,25009
167,MANSFIELD,25005
168,MARBLEHEAD,25009
169,MARION,25023
170,MARLBOROUGH,25017
171,MARSHFIELD,25023
178,MELROSE,25017
172,MASHPEE,25001
173,MATTAPOISETT,25023
174,MAYNARD,25017
175,MEDFIELD,25021
176,MEDFORD,25017
177,MEDWAY,25021
179,MENDON,25027
180,MERRIMAC,25009
181,METHUEN,25009
183,MIDDLEFIELD,25015
182,MIDDLEBOROUGH,25023
184,MIDDLETON,25009
185,MILFORD,25027
186,MILLBURY,25027
190,MONROE,25011
187,MILLIS,25021
188,MILLVILLE,25027
189,MILTON,25021
198,NATICK,25017
191,MONSON,25013
192,MONTAGUE,25011
193,MONTEREY,25003
194,MONTGOMERY,25013
195,MOUNT WASHINGTON,25003
196,NAHANT,25009
197,NANTUCKET,25019
199,NEEDHAM,25021
201,NEW BEDFORD,25005
202,NEW BRAINTREE,25027
203,NEW MARLBOROUGH,25003
204,NEW SALEM,25011
205,NEWBURY,25009
206,NEWBURYPORT,25009
207,NEWTON,25017
208,NORFOLK,25021
209,NORTH ADAMS,25003
210,NORTH ANDOVER,25009
211,NORTH ATTLEBOROUGH,25005
212,NORTH BROOKFIELD,25027
213,NORTH READING,25017
214,NORTHAMPTON,25015
215,NORTHBOROUGH,25027
216,NORTHBRIDGE,25027
217,NORTHFIELD,25011
218,NORTON,25005
223,ORANGE,25011
224,ORLEANS,25001
219,NORWELL,25023
220,NORWOOD,25021
225,OTIS,25003
221,OAK BLUFFS,25007
222,OAKHAM,25027
226,OXFORD,25027
227,PALMER,25013
228,PAXTON,25027
229,PEABODY,25009
230,PELHAM,25015
231,PEMBROKE,25023
232,PEPPERELL,25017
233,PERU,25003
234,PETERSHAM,25027
235,PHILLIPSTON,25027
236,PITTSFIELD,25003
237,PLAINFIELD,25015
238,PLAINVILLE,25021
239,PLYMOUTH,25023
240,PLYMPTON,25023
241,PRINCETON,25027
242,PROVINCETOWN,25001
257,RUTLAND,25027
243,QUINCY,25021
244,RANDOLPH,25021
260,SANDISFIELD,25003
245,RAYNHAM,25005
246,READING,25017
247,REHOBOTH,25005
248,REVERE,25025
249,RICHMOND,25003
250,ROCHESTER,25023
251,ROCKLAND,25023
252,ROCKPORT,25009
253,ROWE,25011
254,ROWLEY,25009
255,ROYALSTON,25027
256,RUSSELL,25013
258,SALEM,25009
259,SALISBURY,25009
261,SANDWICH,25001
262,SAUGUS,25009
263,SAVOY,25003
264,SCITUATE,25023
265,SEEKONK,25005
266,SHARON,25021
285,STOUGHTON,25021
267,SHEFFIELD,25003
268,SHELBURNE,25011
269,SHERBORN,25017
270,SHIRLEY,25017
271,SHREWSBURY,25027
272,SHUTESBURY,25011
286,STOW,25017
273,SOMERSET,25005
274,SOMERVILLE,25017
275,SOUTH HADLEY,25015
276,SOUTHAMPTON,25015
277,SOUTHBOROUGH,25027
278,SOUTHBRIDGE,25027
279,SOUTHWICK,25013
280,SPENCER,25027
281,SPRINGFIELD,25013
282,STERLING,25027
283,STOCKBRIDGE,25003
284,STONEHAM,25017
287,STURBRIDGE,25027
288,SUDBURY,25017
289,SUNDERLAND,25011
317,WELLESLEY,25021
290,SUTTON,25027
291,SWAMPSCOTT,25009
292,SWANSEA,25005
331,WESTHAMPTON,25015
293,TAUNTON,25005
346,WINTHROP,25025
294,TEMPLETON,25027
295,TEWKSBURY,25017
301,TYNGSBOROUGH,25017
296,TISBURY,25007
297,TOLLAND,25013
302,TYRINGHAM,25003
298,TOPSFIELD,25009
299,TOWNSEND,25017
300,TRURO,25001
303,UPTON,25027
304,UXBRIDGE,25027
305,WAKEFIELD,25017
306,WALES,25013
307,WALPOLE,25021
308,WALTHAM,25017
312,WARWICK,25011
313,WASHINGTON,25003
309,WARE,25015
310,WAREHAM,25023
311,WARREN,25027
314,WATERTOWN,25017
315,WAYLAND,25017
316,WEBSTER,25027
318,WELLFLEET,25001
319,WENDELL,25011
320,WENHAM,25009
321,WEST BOYLSTON,25027
322,WEST BRIDGEWATER,25023
323,WEST BROOKFIELD,25027
324,WEST NEWBURY,25009
325,WEST SPRINGFIELD,25013
326,WEST STOCKBRIDGE,25003
327,WEST TISBURY,25007
328,WESTBOROUGH,25027
329,WESTFIELD,25013
330,WESTFORD,25017
332,WESTMINSTER,25027
333,WESTON,25017
334,WESTPORT,25005
335,WESTWOOD,25021
336,WEYMOUTH,25021
337,WHATELY,25011
338,WHITMAN,25023
339,WILBRAHAM,25013
340,WILLIAMSBURG,25015
341,WILLIAMSTOWN,25003
342,WILMINGTON,25017
343,WINCHENDON,25027
344,WINCHESTER,25017
345,WINDSOR,25003
347,WOBURN,25017
348,WORCESTER,25027
349,WORTHINGTON,25015
350,WRENTHAM,25021
351,YARMOUTH,25001

main.js

;(function() {

  var map;

  function Map(topology) {

    // Convert the topojson to geojson
    var geojson = topojson.feature(topology, topology.objects.towns),

    // Since we're using projected TopoJSON, we use a null projection here.
        path = d3.geo.path().projection(null),

    // Use topojson.mesh to create a path representing county outlines.
        countyMesh = topojson.mesh(topology, topology.objects.towns, function(a, b) {
          return a.properties.fips !== b.properties.fips;
        }),

        svg = d3.select("#map").append("svg")
                    .attr({
                      width: 615,
                      height: 375
                    }),

        g = svg.append("g").attr({"class": "g-town"}),

        colorScale = d3.scale.category20b(),

        // Add town paths
        town = g.selectAll("path.town").data(geojson.features)
                  .enter().append("path")
                    .attr({
                      "class": "town",
                      d: path
                    })
                    .style({
                      fill: function(d, i) {
                        return colorScale(Math.floor(Math.random()*1000));
                      },
                      stroke: "white"
                    }),

        // Add county path and set its opacity to 0,
        // since we don't want to show it initially.
        county = g.append("path").datum(countyMesh)
                  .attr({
                    "class": "county",
                    d: path
                  })
                  .style({
                    opacity: 0,
                    stroke: "white",
                    "stroke-width": 2,
                    fill: "none"
                  }),

        currentState = "town";

        // To show the county outlines, we
        // transition the town shapes to have
        // a consistent stroke, and fill, in effect hiding
        // them. We also transition the county
        // path to be visible.
        this.showCounties = function() {
          var countyColors = {};

          // Since we're using a mesh for the county outlines,
          // it doesn't work well to add the fill to the county
          // path. Instead, we'll set a consistent fill on all 
          // the towns in each county, to give the effect of 
          // filling in the county.
          town.transition()
            .duration(600)
            .style({
              fill: function(d, i) {
                if (!countyColors[d.properties.fips]) {
                  countyColors[d.properties.fips] = colorScale(Math.floor(Math.random()*1000));
                }
                return countyColors[d.properties.fips];
              },
              stroke: function(d, i) {
                return countyColors[d.properties.fips];
              }
            });

          county.transition()
            .duration(600)
            .style({
              opacity: 1
            })
        };

        // To show the town outlines, we transition
        // back to our original state.
        this.showTowns = function() {
          town.transition()
            .duration(600)
            .style({
              stroke: "white",
              fill: function(d, i) {
                return colorScale(Math.floor(Math.random()*1000));
              }
            });

          county.transition()
            .duration(600)
            .style({
              opacity: 0
            })
        };

        this.toggle = function() {
          if (currentState === "town") {
            this.showCounties();
            currentState = "county";
          } else {
            this.showTowns();
            currentState = "town";
          }
        };
  }

  d3.json("ma-towns.topojson", function(err, topology) {
    map = new Map(topology);
  });

  d3.select("#toggle").on("click", function() {
    map.toggle();
  });

}());