Add a map Legend and Header Identifier on Hover
This map is generated from data in this Google Docs Spreasheet:
https://docs.google.com/spreadsheet/pub?key=0AhLWjwzZgPNGdEFib2pYeVg1VEU5U0w0MXV5Y254NlE&output=html
<style type="text/css">
#map_test_1 {
height: 500px;
width: 880px;
margin: 3em 0;
}
.info {
padding: 6px 8px;
font: 14px/16px Arial, Helvetica, sans-serif;
background: white;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
}
.info h4 {
margin: 0 0 5px;
color: #777;
}
.uberMap-legend {
clear: both;
margin: 0px;
padding: 0px;
list-style: none;
}
.legend-item {
float: left;
width: 1.5em;
height: 1.5em;
text-align: center;
padding: .5em .25em 0em .25em;
font-weight: bold;
font-size: 1.25em;
opacity: 0.7;
margin: 0px;
color: black;
}
.zoomedIn .legend-item {
opacity: 1;
}
</style>
<p>
This map is generated from data in this Google Docs Spreasheet:<br />
<a href="https://docs.google.com/spreadsheet/pub?key=0AhLWjwzZgPNGdEFib2pYeVg1VEU5U0w0MXV5Y254NlE&output=html">https://docs.google.com/spreadsheet/pub?key=0AhLWjwzZgPNGdEFib2pYeVg1VEU5U0w0MXV5Y254NlE&output=html</a>
</p>
<!-- These are shims for IE6,7,8 and the like "lesser-browsers" -->
<script src="/choropleth/js/es5-shim.min.js"></script>
<script src="/choropleth/js/json2.js"></script>
<!-- used for javascript mustache templating -->
<script src="/choropleth/js/mustache.js"></script>
<!-- You must set the height of the div for the map, yes this should be in an external file -->
<div id="map_test_1"></div>
<div id="map_info_area"></div>
<script src="/choropleth/js/tabletop.js"></script>
<script type="text/javascript" src="/choropleth/js/us-states.js"></script>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.5/leaflet.css" />
<!--[if lte IE 8]>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.5/leaflet.ie.css" />
<![endif]-->
<script type="text/javascript" src="http://cdn.leafletjs.com/leaflet-0.5/leaflet.js"></script>
<script type="text/javascript">
// Once all tests are complete this version is complete.
// [Done] include all required .js files
// [Done] use tabletop.js to load data table from google spreadsheet and turn geoJSON into real objects
// [Done] make array to associate machine headers to human headers from spreadsheet
// [Done] forEach spreadsheet-data-row display geoJSON object to map
// [Done] forEach spreadsheet-data-row update the map control layer
// [TODO] Add feedback to the control layer to show loading of map.
// This should probably change to a jQuery on.doc.load thing...
window.onload = function() { init() };
// Initialize the different constants that this uses to get information from a Google-Drive spreadsheet
var mapKey = "0AhLWjwzZgPNGdEFib2pYeVg1VEU5U0w0MXV5Y254NlE";
var sheetName = 'Hunger Data'
var geoJSONHeaderName = "ubergeojson";
var headersSheetName = "uberMap_meta-data";
var choroplethRangeHeaderName = "foodinsecurityrate";
var choroplethRangeHeaderName_HumanReadable = "";
var choroplethRangeHigh = 0;
var choroplethRangeLow = 0;
var choroplethRangeStep = 0;
var choroplethRangeSteps = [];
// Initialize map specific variables
var defaultMapCenter = [41.05, -77.37]; // A good way to get this number is go to maps.google.com zoom to where you want to center the map - right click and "Drop LatLng marker" copy and paste that little set of numbers in here just the way it is.
var defaultZoomLevel = 7; // Self explanitory: Sets the zoom level from 0 to 18 usually of how close you want to be to the earth when the map loads. For Pennsylvania it's ~7
// Constants that are needed in multiple places in the app below.
var sheetHeaders = [];
var masterGeoJSON = {
"type": "FeatureCollection",
"features": []
};
var geojson;
var choroplethDataClasses = 9;
// Is in case you're using liquid layouts (in this case bundeled with Jekyll) you need to set a mustache delimiter.
var mustacheSetDelimiter = "{{={u{ }u}=}}";
var controlPannel_clickState = mustacheSetDelimiter;
var controlPannel_hoverState = mustacheSetDelimiter;
var controlPannel_initState = mustacheSetDelimiter;
var controlPannel_legend = "";
var zoomToJSONObject = L.geoJson(JSON.parse('{"type":"FeatureCollection","features":[{"type":"Feature","id":"USA-PA","properties":{"fips":"42","name":"Pennsylvania"},"geometry":{"type":"Polygon","coordinates":[[[-79.76278,42.252649],[-79.76278,42.000709],[-75.35932,42.000709],[-75.249781,41.863786],[-75.173104,41.869263],[-75.052611,41.754247],[-75.074519,41.60637],[-74.89378,41.436584],[-74.740426,41.431108],[-74.69661,41.359907],[-74.828057,41.288707],[-74.882826,41.179168],[-75.134765,40.971045],[-75.052611,40.866983],[-75.205966,40.691721],[-75.195012,40.576705],[-75.069042,40.543843],[-75.058088,40.417874],[-74.773287,40.215227],[-74.82258,40.127596],[-75.129289,39.963288],[-75.145719,39.88661],[-75.414089,39.804456],[-75.616736,39.831841],[-75.786521,39.722302],[-79.477979,39.722302],[-80.518598,39.722302],[-80.518598,40.636951],[-80.518598,41.978802],[-80.518598,41.978802],[-80.332382,42.033571],[-79.76278,42.269079],[-79.76278,42.252649]]]}}]}'), {style: {opacity: 0, fillOpacity: 0, clickable: false}});
// State Control
var polygonHasFocus = false;
var lastClickedLayer;
var geoJSONLayers = [];
//console.log(zoomToJSONObject.getBounds());
// init() is where we actually go to the Google-Drive spreadsheet and load the data in.
function init() {
Tabletop.init( { key: mapKey,
callback: processSpreadsheetData,
wanted: [sheetName, headersSheetName]
});
}
// This is where we actually process all of the data from the Google-Drive spreadsheet
// and populate the masterGeoJSON object to get it ready for the map display function below.
function processSpreadsheetData(data, tabletop) {
var choroplethRangeCandidate = [];
// I leave these here to toggle the ability to check out these variables in the browser
// - so I can see what we're actually pulling from the Google-Drive spreadsheet.
// --- Before data processing variables --- //
//console.log(data);
//console.log(tabletop.sheets(headersSheetName));
//console.log(tabletop.sheets(sheetName));
//console.log(tabletop.sheets(sheetName).elements);
// ---
// this creates a sheetHeaders[] object that holds two different
// - properties: humanReadable and machineReadable
// - sheetHeaders[] is an array that we can step through using forEach (thank you ES5)
// - to display all of the map properties for each map object when we get to updating the map control layer.
tabletop.sheets(headersSheetName).elements.forEach( function(element, index) {
sheetHeaders[index] = {
machineReadable: tabletop.sheets(sheetName).column_names[index],
humanReadable: element.uberheaderlabel
};
if (sheetHeaders[index].machineReadable === choroplethRangeHeaderName) {
choroplethRangeHeaderName_HumanReadable = sheetHeaders[index].humanReadable;
}
});
// browser-view toggle for debugging:
//console.log("Headers:");
//console.log(sheetHeaders);
tabletop.sheets(sheetName).elements.forEach(function(element, rowIndex) {
masterGeoJSON.features[rowIndex] = JSON.parse(element[geoJSONHeaderName]).features[0];
tabletop.sheets(sheetName).column_names.forEach(function(headerName, headerIndex) {
if (headerName !== geoJSONHeaderName) {
masterGeoJSON.features[rowIndex].properties[headerName] = element[headerName];
} else {
// Do nothing
}
});
//console.log("County Name Check: " + masterGeoJSON.features[rowIndex].properties['countyname'] + " - " + masterGeoJSON.features[rowIndex].properties['name']);
choroplethRangeCandidate[rowIndex] = parseFloat(masterGeoJSON.features[rowIndex].properties[choroplethRangeHeaderName]);
});
choroplethRangeHigh = Math.max.apply(null, choroplethRangeCandidate);
choroplethRangeLow = Math.min.apply(null, choroplethRangeCandidate);
choroplethRangeStep = (choroplethRangeHigh - choroplethRangeLow);
for (i=0;i<choroplethDataClasses;i++) {
choroplethRangeSteps[i] = parseFloat(choroplethRangeLow + ((choroplethRangeStep / choroplethDataClasses) * i));
}
//console.log("Max Choropleth Range: " + choroplethRangeHigh);
//console.log("Min Choropleth Range: " + choroplethRangeLow);
//console.log("Steps = " + choroplethRangeSteps);
// More browser-view toggles for debugging:
// --- After data processing variables --- //
//console.log(data);
//console.log("masterGeoJSON:");
//console.log(masterGeoJSON);
//console.log(statesData);
//Build the mustache templates
var legendInstructions = '<p class="legend-instructions">Choose a county below for hunger statistics.</p>'
controlPannel_initState += legendInstructions;
controlPannel_hoverState += '<p class="legend-header">{u{countyname}u} County</p><p class="legend-sub-header">(Click for more information)</p>';
controlPannel_clickState += "<ul>";
sheetHeaders.forEach( function(sheetHeader, sheetHeaderIndex){
if (sheetHeader.machineReadable !== geoJSONHeaderName) {
controlPannel_clickState += "<li>" +
'<span class="map-data-property label">' + sheetHeader.humanReadable + "</span>" +
": " +
'<span class="map-data-property value">' + "{u{" + sheetHeader.machineReadable + "}u}" + "</span>" +
"</li>";
}
});
controlPannel_clickState += "</ul>";
controlPannel_legend += '<p class="uberMap-legend-header">' + choroplethRangeHeaderName_HumanReadable + '</p>' +
'<ul class="uberMap-legend">';
choroplethRangeSteps.forEach( function(rangeStep, rangeIndex) {
controlPannel_legend += '<li class="legend-item" style="background-color: ' + getColor(rangeStep) + '">' + Math.floor(rangeStep) + '</li>';
});
controlPannel_legend += '</ul>';
controlPannel_clickState += '<div class="zoomedIn">' + controlPannel_legend + '</div>';
controlPannel_hoverState += controlPannel_legend;
controlPannel_initState += controlPannel_legend;
//console.log(controlPannel_hoverState);
// ---
// Now that we have actually loaded all of the data from the Google-Drive spreadsheet
// - go ahead and load masterGeoSJON up to the map.
loadMapData(masterGeoJSON);
}
// Setup the map to center where you would like it to. You can always to go maps.google.com and right click anywhere on the map and "Drop LatLng Marker".
var map = L.map('map_test_1', {
zoomControl: false,
dragging: false,
touchZoom: false,
scrollWheelZoom: false,
doubleClickZoom: false
});
// This is where you get your map tiles. You can get your own free API key from cloudmade.com
// - please replace my key with yours if you're using this code in your own project.
var cloudmade = L.tileLayer('http://{s}.tile.cloudmade.com/{key}/{styleId}/256/{z}/{x}/{y}.png', {
attribution: 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
key: 'c5007019bb4e4787afb0135c36690912',
styleId: 86036
}).addTo(map);
// This is where we decide which colors the polygons we draw on the map will be.
// - this needs to be re-written before version 1.0 to calculate the proper range
// - from the data provided automatically to provide a 5 to 10 color change range.
// [TODO] re-write getColor() to calculate the 5 to 10 color step automatically based on the data from the spreadsheet.
function getColor(d) {
return d >= choroplethRangeSteps[8] ? '#800026' :
d >= choroplethRangeSteps[7] ? '#BD0026' :
d >= choroplethRangeSteps[6] ? '#E31A1C' :
d >= choroplethRangeSteps[5] ? '#FC4E2A' :
d >= choroplethRangeSteps[4] ? '#FD8D3C' :
d >= choroplethRangeSteps[3] ? '#FEB24C' :
d >= choroplethRangeSteps[2] ? '#FED976' :
d >= choroplethRangeSteps[1] ? '#FFEDA0' :
'#FFFFCC';
}
// [TODO] re-write this to get the styles from a stylesheet instead of hard-coding them here.
// ------ Maybe I won't actually do this since some of these don't really match a CSS standard.
// ------ Maybe the we will css standard the colors? Some of these things can be set as
// ------ variables at the top of this document... perhaps we'll just set it there due to the
// ------ styleing constraints set forth from our leaflet.js buddies
function style(feature) {
return {
fillColor: getColor(feature.properties.foodinsecurityrate),
weight: 1,
opacity: 1,
color: 'white',
fillOpacity: 0.7
};
}
var info = L.control();
info.onAdd = function (map) {
this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
this.update();
return this._div;
};
// method that we will use to update the control based on feature properties passed
info.update = function (props, featureEventType) {
if (props) {
if (featureEventType === "click") {
this._div.innerHTML = Mustache.render(controlPannel_clickState, props);
} else if (featureEventType === "hover") {
this._div.innerHTML = Mustache.render(controlPannel_hoverState, props);
}
} else {
this._div.innerHTML = Mustache.render(controlPannel_initState);
}
};
// This is what happens when your mouse hovers over a map element.
function highlightFeature(e) {
var layer = e.target;
if (!polygonHasFocus) {
layer.setStyle({
fillColor: '#78A700',
fillOpacity: 0.8
});
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
info.update(layer.feature.properties, "hover");
}
}
function clickFeature(e) {
var layer = e.target;
if (polygonHasFocus) {
map.fitBounds(zoomToJSONObject.getBounds());
polygonHasFocus = false;
geojson.resetStyle(lastClickedLayer);
info.update();
geoJSONLayers.forEach(function(thisLayer, layerArrayIndex) {
geojson.resetStyle(thisLayer);
});
} else {
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
geoJSONLayers.forEach(function(thisLayer, layerArrayIndex) {
if (thisLayer === layer) {
geojson.resetStyle(thisLayer);
thisLayer.setStyle({
fillOpacity: 1,
weight: 0
});
} else {
thisLayer.setStyle({
fillColor: '#9EA4A1',
fillOpacity: 0.7,
weight: 0
});
}
});
map.fitBounds(layer.getBounds());
polygonHasFocus = true;
lastClickedLayer = layer;
info.update(layer.feature.properties, "click");
}
}
// This is what happens when your mouse goes away from an element.
function resetHighlight(e) {
if (!polygonHasFocus) {
geojson.resetStyle(e.target);
info.update();
}
}
// This is where we assign the behavior to each map element we draw.
// [TODO] Add on click event
// - zoom in when first clicked - then update map control layer
// - zoom out when clicked again to pan back to the overall map view
function onEachFeature(feature, layer) {
geoJSONLayers.push(layer);
layer.on({
mouseover: highlightFeature,
mouseout: resetHighlight,
click: clickFeature
});
}
// This is the bit where we load all the geoJSON information into the map.
function loadMapData(geoJSONData) {
zoomToJSONObject.addTo(map);
geojson = L.geoJson(geoJSONData, {
style: style,
onEachFeature: onEachFeature
}).addTo(map);
map.fitBounds(zoomToJSONObject.getBounds());
map.on({
click: function() {
map.fitBounds(zoomToJSONObject.getBounds());
polygonHasFocus = false;
geojson.resetStyle(lastClickedLayer);
info.update();
geoJSONLayers.forEach(function(thisLayer, layerArrayIndex) {
geojson.resetStyle(thisLayer);
});
},
zoomend: function() {
if (polygonHasFocus) {
map.panBy([150, 0]);
geoJSONLayers.forEach(function(thisLayer, layerArrayIndex) {
thisLayer.setStyle({
weight: 1
});
});
}
}
});
info.addTo(map);
}
</script>