Skip to content

Instantly share code, notes, and snippets.

@sjengle
Last active January 26, 2016 23:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sjengle/eaba52747c044273d900 to your computer and use it in GitHub Desktop.
Save sjengle/eaba52747c044273d900 to your computer and use it in GitHub Desktop.
Proportional Symbol Map

Proportional Symbol Map

This example is loosely based on the Symbol Map block by M. Bostock. It uses the TopoJSON files for the United States from the TopoJSON Examples block by the same author.

Precipitation Data

The precipitation data is available in R. The usprecip.r script uses the ggmap package in R to geocode the city names and convert them into a JSON format using the jsonlite package.

Reusable Charts

This example demonstrates the Reusable Charts model, which is a nice way to encapsulate your JavaScript code in a reusable way without losing access to the underlying data and configuration.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Symbol Map Demo</title>
<!-- load D3 and TopoJSON //-->
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<!-- load custom CSS and JavaScript //-->
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<div id="block">
<svg id="map" width="940" height="480"></svg>
<p id="log">Loading map... please wait.</p>
</div>
<script>
/*
* For sample TopoJSON files, go to:
* https://gist.github.com/mbostock/4090846
*/
var base = "https://gist.githubusercontent.com/mbostock/4090846/raw/";
var url = {
country: base + "us.json",
states: base + "us-state-names.tsv",
precip: "usprecip.json" // relative URL
};
// Uses reusable chart model
// See http://bost.ocks.org/mike/chart/
var chart = symbolMap();
// Update how we access data (need the precip property)
chart = chart.value(function(d) { return d.precip; });
// Nested calls to trigger drawing in proper order
d3.json(url.country, function(mapError, mapJSON) {
if (processError(mapError)) return;
// update map data
chart = chart.map(mapJSON);
// Wait until the map is drawn before loading
// and drawing the data values
d3.json(url.precip, function(dataError, dataJSON) {
if (processError(dataError)) return;
chart = chart.values(dataJSON);
chart("map");
});
});
// Load state lookup information
// Possible some lookups will fail until this loads
d3.tsv(url.states, parseStateName, function(error, data) {
if (processError(error)) return;
chart = chart.lookup(data);
}
);
</script>
</body>
</html>
/*
* If there is an error, insert an error message in the HTML
* and log the error to the console.
*/
function processError(error) {
if (error) {
// Use the "statusText" of the error if possible
var errorText = error.hasOwnProperty("statusText") ?
error.statusText : error.toString();
// Insert the error message before all else
d3.select("body")
.insert("p", ":first-child")
.text("Error: " + errorText)
.style("color", "red");
// Log the error to the console
console.warn(error);
return true;
}
return false;
}
/*
* Parses us-state-names.tsv into components.
* Used by d3.tsv() function.
*/
function parseStateName(row) {
return {
id: +row.id,
name: row.name.trim(),
code: row.code.trim().toUpperCase()
};
}
function symbolMap() {
var lookup = {};
var projection = d3.geo.albersUsa();
var radius = d3.scale.sqrt().range([5, 15]);
var log = d3.select("#log");
var map = null; // map data
var values = null; // values for symbols
// gets the value property from the dataset
// for our example, we need to reset this!
var value = function(d) { return d.value; };
function chart(id) {
if (map === null || values === null) {
console.warn("Unable to draw symbol map: missing data.");
return;
}
updateLog("Drawing map... please wait.");
var svg = d3.select("svg#" + id);
var bbox = svg.node().getBoundingClientRect();
// update project scale
// (this may need to be customized for different projections)
projection = projection.scale(bbox.width);
// update projection translation
projection = projection.translate([
bbox.width / 2,
bbox.height / 2
]);
// set path generator based on projection
var path = d3.geo.path().projection(projection);
// update radius domain
// uses our value function to get the right property
radius = radius.domain(d3.extent(values, value));
// create groups for each of our components
// this just reduces our search time for specific states
var country = svg.append("g").attr("id", "country");
var states = svg.append("g").attr("id", "states");
var symbols = svg.append("g").attr("id", "dots");
// show that only 1 feature for land
console.log(topojson.feature(map, map.objects.land));
// show that we have an array of features for states
console.log(topojson.feature(map, map.objects.states));
// draw base map
country.append("path")
// use datum here because we only have 1 feature,
// not an array of features (needed for data() call)
.datum(topojson.feature(map, map.objects.land))
.attr("d", path)
.classed({"country": true});
// draw states (invisible for now)
// may need to adjust to draw countries instead?
states.selectAll("path")
.data(topojson.feature(map, map.objects.states).features)
.enter()
.append("path")
.attr("d", path)
// set the ID so we can select it later
.attr("id", function(d) { return "state" + d.id; })
.classed({"state": true});
// draw symbols
symbols.selectAll("circle")
.data(values)
.enter()
.append("circle")
.attr("r", function(d, i) {
return radius(value(d));
})
.attr("cx", function(d, i) {
// projection takes [longitude, latitude]
// and returns [x, y] as output
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d, i) {
return projection([d.lon, d.lat])[1];
})
.classed({"symbol": true})
.on("mouseover", showHighlight)
.on("mouseout", hideHighlight);
}
/*
* These are functions for getting and setting values.
* If no argument is provided, the function returns the
* current value. Otherwise, it sets the value.
*
* If setting the value, ALWAYS return the chart object.
* This will allow you to save the updated version of
* this environment.
*
* Personally, I do not like _ to indicate the argument
* that may or may not be provided, but its what the
* original model uses.
*/
// gets/sets the mapping from state abbreviation to topojson id
chart.lookup = function(_) {
// if no arguments, return current value
if (!arguments.length) {
return lookup;
}
// otherwise assume argument is our lookup data
_.forEach(function(element) {
lookup[element.id] = element.name;
// lets you lookup the ID of a state
// by its code (2-letter abbreviation)
if (element.hasOwnProperty("code")) {
lookup[element.code] = element.id;
}
});
// always return chart object here
console.log("Updated lookup information.")
return chart;
};
/*
* Note the semi-colon above. This was an assignment,
* even though we were defining a function. All assignments
* should end in a semi-colon.
*/
// allows for customization of projection used
chart.projection = function(_) {
if (!arguments.length) {
return projection;
}
projection = _;
return chart;
};
/*
* You can get/set functions just like variables.
* The basic format is always the same.
*/
// allows for customization of radius scale
chart.radius = function(_) {
if (!arguments.length) {
return radius;
}
radius = _;
return chart;
};
// updates the map data, must happen before drawing
chart.map = function(_) {
if (!arguments.length) {
return map;
}
map = _;
updateLog("Map data loaded.");
return chart;
};
// updates the symbols values, must happen before drawing
chart.values = function(_) {
if (!arguments.length) {
return values;
}
values = _;
updateLog("Symbol data loaded.");
return chart;
};
// updates how we access values from our dataset
chart.value = function(_) {
if (!arguments.length) {
return value;
}
value = _;
return chart;
};
/*
* These functions are not outwardly accessible. They
* are only used within this environment.
*/
// updates the log message
function updateLog(message) {
// if no arguments, use default message
if (!arguments.length) {
log.text("Hover over a circle for more details");
return;
}
// otherwise set log message
log.text(message);
}
// called on mouseover
function showHighlight(d) {
// highlight symbol
d3.select(this).classed({
"highlight": true,
"symbol": true
});
// highlight state associated with symbol
d3.select("g#states")
.select("path#state" + lookup[d.state])
.classed({
"highlight": true,
"state": true
});
updateLog(d.city + ", " + d.state +
" received an average of " + d.precip +
" inches of precipitation.");
}
// called on mouseout
function hideHighlight(d) {
// reset symbol
d3.select(this).classed({
"highlight": false,
"symbol": true
});
// reset state associated with symbol
d3.select("g#states")
.select("path#state" + lookup[d.state])
.classed({
"highlight": false,
"state": true
});
// reset log message
updateLog();
}
return chart;
}
body {
background-color: whitesmoke;
margin: 8px 10px;
padding: 0px;
}
svg {
background-color: white;
margin: 0px;
float: left;
}
#block {
max-width: 950px;
}
#log {
color: dimgray;
font-size: 10px;
font-style: italic;
text-align: center;
margin: 0px;
padding: 0px;
}
.country {
fill: gainsboro;
}
.state {
fill: none;
}
/*
* Setting "opacity" can affect both
* fill and stroke. We want to set
* both of those separately.
*/
.symbol {
fill: royalblue;
fill-opacity: 0.25;
stroke: royalblue;
stroke-width: 1px;
stroke-opacity: 1.0;
}
/*
* We can specify how to highlight based
* on the type of underlying object.
*/
circle.highlight {
stroke: orangered;
stroke-width: 2px;
}
path.highlight {
fill: whitesmoke;
stroke: tomato;
stroke-width: 1px;
}
[
{
"city": "Mobile",
"state": "AL",
"lat": 30.6954,
"lon": -88.0399,
"precip": 67
},
{
"city": "Juneau",
"state": "AK",
"lat": 58.3019,
"lon": -134.4197,
"precip": 54.7
},
{
"city": "Phoenix",
"state": "AZ",
"lat": 33.4484,
"lon": -112.074,
"precip": 7
},
{
"city": "Little Rock",
"state": "AR",
"lat": 34.7465,
"lon": -92.2896,
"precip": 48.5
},
{
"city": "Los Angeles",
"state": "CA",
"lat": 34.0522,
"lon": -118.2437,
"precip": 14
},
{
"city": "Sacramento",
"state": "CA",
"lat": 38.5816,
"lon": -121.4944,
"precip": 17.2
},
{
"city": "San Francisco",
"state": "CA",
"lat": 37.7749,
"lon": -122.4194,
"precip": 20.7
},
{
"city": "Denver",
"state": "CO",
"lat": 39.7392,
"lon": -104.9903,
"precip": 13
},
{
"city": "Hartford",
"state": "CT",
"lat": 41.7637,
"lon": -72.6851,
"precip": 43.4
},
{
"city": "Wilmington",
"state": "DE",
"lat": 39.7391,
"lon": -75.5398,
"precip": 40.2
},
{
"city": "Washington",
"state": "DC",
"lat": 38.9072,
"lon": -77.0369,
"precip": 38.9
},
{
"city": "Jacksonville",
"state": "FL",
"lat": 30.3322,
"lon": -81.6557,
"precip": 54.5
},
{
"city": "Miami",
"state": "FL",
"lat": 25.7617,
"lon": -80.1918,
"precip": 59.8
},
{
"city": "Atlanta",
"state": "GA",
"lat": 33.749,
"lon": -84.388,
"precip": 48.3
},
{
"city": "Honolulu",
"state": "HI",
"lat": 21.3069,
"lon": -157.8583,
"precip": 22.9
},
{
"city": "Boise",
"state": "ID",
"lat": 43.6187,
"lon": -116.2146,
"precip": 11.5
},
{
"city": "Chicago",
"state": "IL",
"lat": 41.8781,
"lon": -87.6298,
"precip": 34.4
},
{
"city": "Peoria",
"state": "IL",
"lat": 40.6936,
"lon": -89.589,
"precip": 35.1
},
{
"city": "Indianapolis",
"state": "IN",
"lat": 39.7684,
"lon": -86.1581,
"precip": 38.7
},
{
"city": "Des Moines",
"state": "IA",
"lat": 41.6005,
"lon": -93.6091,
"precip": 30.8
},
{
"city": "Wichita",
"state": "KS",
"lat": 37.6889,
"lon": -97.3361,
"precip": 30.6
},
{
"city": "Louisville",
"state": "KY",
"lat": 38.2527,
"lon": -85.7585,
"precip": 43.1
},
{
"city": "New Orleans",
"state": "LA",
"lat": 29.9511,
"lon": -90.0715,
"precip": 56.8
},
{
"city": "Portland",
"state": "OR",
"lat": 45.5231,
"lon": -122.6765,
"precip": 40.8
},
{
"city": "Baltimore",
"state": "MD",
"lat": 39.2904,
"lon": -76.6122,
"precip": 41.8
},
{
"city": "Boston",
"state": "MA",
"lat": 42.3601,
"lon": -71.0589,
"precip": 42.5
},
{
"city": "Detroit",
"state": "MI",
"lat": 42.3314,
"lon": -83.0458,
"precip": 31
},
{
"city": "Sault Ste. Marie",
"state": "MI",
"lat": 46.4953,
"lon": -84.3453,
"precip": 31.7
},
{
"city": "Duluth",
"state": "MN",
"lat": 46.7867,
"lon": -92.1005,
"precip": 30.2
},
{
"city": "Minneapolis",
"state": "MN",
"lat": 44.9778,
"lon": -93.265,
"precip": 25.9
},
{
"city": "Jackson",
"state": "MS",
"lat": 32.2988,
"lon": -90.1848,
"precip": 49.2
},
{
"city": "Kansas City",
"state": "MO",
"lat": 39.0997,
"lon": -94.5786,
"precip": 37
},
{
"city": "Saint Louis",
"state": "MO",
"lat": 38.627,
"lon": -90.1994,
"precip": 35.9
},
{
"city": "Great Falls",
"state": "MT",
"lat": 47.4942,
"lon": -111.2833,
"precip": 15
},
{
"city": "Omaha",
"state": "NE",
"lat": 41.2524,
"lon": -95.998,
"precip": 30.2
},
{
"city": "Reno",
"state": "NV",
"lat": 39.5296,
"lon": -119.8138,
"precip": 7.2
},
{
"city": "Concord",
"state": "CA",
"lat": 37.978,
"lon": -122.0311,
"precip": 36.2
},
{
"city": "Atlantic City",
"state": "NJ",
"lat": 39.3643,
"lon": -74.4229,
"precip": 45.5
},
{
"city": "Albuquerque",
"state": "NM",
"lat": 35.0853,
"lon": -106.6056,
"precip": 7.8
},
{
"city": "Albany",
"state": "NY",
"lat": 42.6526,
"lon": -73.7562,
"precip": 33.4
},
{
"city": "Buffalo",
"state": "NY",
"lat": 42.8864,
"lon": -78.8784,
"precip": 36.1
},
{
"city": "New York",
"state": "NY",
"lat": 40.7128,
"lon": -74.0059,
"precip": 40.2
},
{
"city": "Charlotte",
"state": "NC",
"lat": 35.2271,
"lon": -80.8431,
"precip": 42.7
},
{
"city": "Raleigh",
"state": "NC",
"lat": 35.7796,
"lon": -78.6382,
"precip": 42.5
},
{
"city": "Bismarck",
"state": "ND",
"lat": 46.8083,
"lon": -100.7837,
"precip": 16.2
},
{
"city": "Cincinnati",
"state": "OH",
"lat": 39.1031,
"lon": -84.512,
"precip": 39
},
{
"city": "Cleveland",
"state": "OH",
"lat": 41.4993,
"lon": -81.6944,
"precip": 35
},
{
"city": "Columbus",
"state": "OH",
"lat": 39.9612,
"lon": -82.9988,
"precip": 37
},
{
"city": "Oklahoma City",
"state": "OK",
"lat": 35.4676,
"lon": -97.5164,
"precip": 31.4
},
{
"city": "Portland",
"state": "OR",
"lat": 45.5231,
"lon": -122.6765,
"precip": 37.6
},
{
"city": "Philadelphia",
"state": "PA",
"lat": 39.9526,
"lon": -75.1652,
"precip": 39.9
},
{
"city": "Pittsburgh",
"state": "PA",
"lat": 40.4406,
"lon": -79.9959,
"precip": 36.2
},
{
"city": "Providence",
"state": "RI",
"lat": 41.824,
"lon": -71.4128,
"precip": 42.8
},
{
"city": "Columbia",
"state": "SC",
"lat": 34.0007,
"lon": -81.0348,
"precip": 46.4
},
{
"city": "Sioux Falls",
"state": "SD",
"lat": 43.5446,
"lon": -96.7311,
"precip": 24.7
},
{
"city": "Memphis",
"state": "TN",
"lat": 35.1495,
"lon": -90.049,
"precip": 49.1
},
{
"city": "Nashville",
"state": "TN",
"lat": 36.1627,
"lon": -86.7816,
"precip": 46
},
{
"city": "Dallas",
"state": "TX",
"lat": 32.7767,
"lon": -96.797,
"precip": 35.9
},
{
"city": "El Paso",
"state": "TX",
"lat": 31.7776,
"lon": -106.4425,
"precip": 7.8
},
{
"city": "Houston",
"state": "TX",
"lat": 29.7604,
"lon": -95.3698,
"precip": 48.2
},
{
"city": "Salt Lake City",
"state": "UT",
"lat": 40.7608,
"lon": -111.891,
"precip": 15.2
},
{
"city": "Burlington",
"state": "VT",
"lat": 44.4759,
"lon": -73.2121,
"precip": 32.5
},
{
"city": "Norfolk",
"state": "VA",
"lat": 36.8508,
"lon": -76.2859,
"precip": 44.7
},
{
"city": "Richmond",
"state": "VA",
"lat": 37.5407,
"lon": -77.436,
"precip": 42.6
},
{
"city": "Seattle",
"state": "WA",
"lat": 47.6062,
"lon": -122.3321,
"precip": 38.8
},
{
"city": "Spokane",
"state": "WA",
"lat": 47.6588,
"lon": -117.426,
"precip": 17.4
},
{
"city": "Charleston",
"state": "SC",
"lat": 32.7765,
"lon": -79.9311,
"precip": 40.8
},
{
"city": "Milwaukee",
"state": "WI",
"lat": 43.0389,
"lon": -87.9065,
"precip": 29.1
},
{
"city": "Cheyenne",
"state": "WY",
"lat": 41.14,
"lon": -104.8202,
"precip": 14.6
}
]
library(ggmap)
library(stringr)
library(jsonlite)
# Remove last non-US value from precip dataset
values <- head(precip, -1)
# Get the city names
cities <- names(values)
# Some need fixing to be geocoded correctly
cities[cities == "Columbia"] <- "Columbia, SC"
cities[cities == "Seattle Tacoma"] <- "Seattle, WA"
cities[cities == "Minneapolis/St Paul"] <- "Minneapolis, MN"
# Use ggmap to geocode the city names to GPS coordinates
# Beware you are rate limited how many times you can run this!
coords <- geocode(cities, output = "latlona")
# Capitalization function from ?toupper
capitalize <- function(s) {
cap <- function(s) {
paste(toupper(substring(s, 1, 1)),
substring(s, 2),
sep = "",
collapse = " ")
}
sapply(strsplit(s, split = " "), cap)
}
# Split address component into parts
address <- strsplit(as.character(coords$address), ", ", fixed = TRUE)
address <- t(data.frame(address, stringsAsFactors = FALSE))
# Extract state abbrevation from address
cities <- unname(address[, 1])
cities <- capitalize(cities)
states <- unname(address[, 2])
states <- toupper(states)
# Plop everything into a nice data frame
df <- data.frame(
city = cities,
state = states,
lat = coords$lat,
lon = coords$lon,
precip = values,
stringsAsFactors = FALSE,
row.names = NULL
)
# Convert to JSON format
json <- toJSON(
df,
dataframe = "rows",
factor = "string",
pretty = TRUE
)
# Output JSON to file
cat(json, file = "usprecip.json")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment