Skip to content

Instantly share code, notes, and snippets.

@bseddon
Last active January 26, 2023 00:43
Show Gist options
  • Save bseddon/83275f9b3b7c9fd496cc70032f96d3f5 to your computer and use it in GitHub Desktop.
Save bseddon/83275f9b3b7c9fd496cc70032f96d3f5 to your computer and use it in GitHub Desktop.
D3 mapping example

This is not a review of all the ways D3 can be used to create maps and all the formats that can be used. It's a review of my use of D3 and how to use information within shape files published by, for example, US Census Bureau.

Let's suppose you want to create a map of all the zip codes in New Jersey and show something about each one. Here's a way to do it.

Create a folder. I'll call it 'mapping'.

Begin by downloading a source of zip codes for NJ. Copy the page of zip code and paste into Excel (drop the formatting). Delete all the columns except the one containing the zip code and so there are only zip codes then save as a CSV into the mapping folder.

Next download the Zip code tabulation areas (zctas) file and unzip again into the 'mapping' folder. The two key files are the shape file (.shp) and the database format (.dbf). To do anything with these file some help is needed. My choice has been to use the Node package manager (NPM) package called 'shapefile'.

Install NPM into the mapping. At a command prompt, cd to the 'mapping' folder then run the command:

npm install

When it's finished, there will be a sub-folder called 'node_modules'. Then run:

npm install shapefile

After this there will be useful commands in the folder .\node_modules.bin. These can be used to convert the information in the shape file into the GeoJSON format which is easier to use:

node_modules\.bin\shp2json -n -o us.zipcodes.json cb_2018_us_zcta510_500k.shp

This will take just a few seconds to run and will produce a JSON file of about 150MB. From this what's needed are those records for the NJ zip codes. To do this I use the following Powershell commands:

# Load the CSV file of Zip codes and verify
$csv = Get-Content -Path '.\Mapping data\NJ\nj-zipcodes.csv' | Sort-Object
$csv

# Load the GeoJSON records (each line represents a Zip code area)
# This command will report the number of records that found.  This number should be the same as the number of rows in the CSV file:
($zips | where { $csv -contains $_.substring( 45, 5 ) }).count

# If it looks good the GeoJSON records for NJ can be generated.  It should be correct JSON 
# but D3 does not like newline characters so one long line is created.
"[" + ( ($zips | where { $csv -contains $_.substring( 45, 5 ) }) -join "," ) + "]" | Set-Content -Path us.geo.json

With the GeoJSON file prepared, its time to let D3 use it. First create some HTML:

<html>
	<head>
		<script src="https://cdn.jsdelivr.net/npm/d3-array@3"></script>
		<script src="https://cdn.jsdelivr.net/npm/d3@3"></script>
		<script>
			document.addEventListener('DOMContentLoaded', function()
			{
          var container = document.getElementsByClassName("svg-container")
				  var width = container[0].clientWidth,
					height = container[0].clientHeight;

          // Create some variables in an object so they are in one place
				  var state = 
					{
						// -75.5600, 38.6500, -73.8800, 41.3600
						center: [(-75+-73)/2, (41.3+38.65)/2],
						file: "./Mapping data/NJ/nj-zip2.json",
						parallels: [38, 42],
            translate: [width / 4 - 30, height / 2],
						scale: 16000,
            height: '800px',
            width: '500px',
						code: 'nj',
						type: 'geo',
						feature_class: 'ZCTA5CE10',
						feature_name: 'ZCTA5CE10',
						get_features: iter => iter
					};

				// Create the main <svg> element
				var svg = d3.select(".svg-container").append("svg")
					.attr("width", state.width )
					.attr("height", state.height )
                    .attr("viewport", '0 0 100 100')
                    .attr("preserveAspectRatio", 'xMidYMid meet')

				// Create a group element so the whole map can be rotated
				var g = svg.append("g")
					.attr("class","subunits");

				// Use the projection to centre the generated map in the center of the display
        // Rotate ensure there is no rotation introduce by the projection
        // Center is used to ensure the projection has NJ in the middle of the display
        // Translate allows for some fine adjustment
        // Scale needs to be large because the shape file is zoomed out a long way
				var projection = d3.geo.albers()
					.rotate([0, 0])
					.center(state.center)
					.parallels(state.parallels)
					.scale(state.scale)
					.translate(state.translate);

				// Each new path element will assigned geometry data moderated by this projection
				var path = d3.geo.path()
				    .projection(projection);

				// For geojson or topojson formats, open the file and 
				// use a supplied function to retrieve the features
				d3.json(state.file, function(error, map) 
				{
						if (error) return console.error(error);
						console.log(map);

						state.get_features(map).forEach(d => 
						{
							const randomColor = Math.floor(Math.random() * 16777215).toString(16);

							g.datum(d)
								.append("path")
									.attr("class", d.properties[[state.feature_class]] )
									.attr("data-zip", d.properties[state.feature_class] )
									.attr("fill", "#" + randomColor )
									.attr("d", path)
								.append("title")
									.text( d.properties[state.feature_name] )
						} );
					} );
			} );
		</script>
		<style>
      svg {
        background-color: white;
      }

			svg g {
				transform-box: fill-box;
				transform-origin: center;
				transform: rotate(-45deg);
			}

      .svg-container {
          position: absolute;
          top: 0;
          left: 0;
          bottom: 0;
          right: 0;
          padding: 30px;
          display: grid;
          justify-content: center;
          align-content: center;
      }
		</style>
	</head>
	<body>
        <div class="svg-container"></div>
	</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment