Created
April 2, 2014 19:52
-
-
Save psaia/9941815 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
// This will need to change depending on the enviornment. | |
// Necessary data files should be included here. Don't | |
// forget the trailing slash. | |
var DATA_DIR = "/widgets/railroads_and_states/dist/data/"; | |
// This key is kind of weird. It represents the United | |
// States in total within the data. Saving as a constant | |
// incase it's changed to something like "Nation", which | |
// I like much better. | |
var US_TOTAL_KEY = "United States"; | |
var US_TOTAL_KEY_ABBR = "U.S."; // And its abbreviation.. | |
// Global event dispatcher object to be shared amongst views. | |
// All events will be listening and dispatched to this object | |
// rather any the internal listeners within views and models. | |
var vent = _.extend({}, Backbone.Events); | |
// Specifiy variables for backbone classes. | |
var appView, appRouter; | |
// U.S. hash. For fast lookups. This is set when the data is loaded. | |
// => { abbr : name } | |
var stateHash = null; | |
// The map view controls the svg map along with the autocomplete | |
// box UI, as well as the map images and pdf link below. Basicall, | |
// the entire left column. | |
var MapView = Backbone.View.extend({ | |
events: { | |
"change select#state-selectbox": "stateSelect" | |
}, | |
/** | |
* On initialization the map will be drawn. This will | |
* only happen once. | |
*/ | |
initialize: function(options) { | |
this.data = options.data; | |
this.us = options.us; | |
// Specifiy all the map variables; path, projection, mesh, ect... | |
this.svg = d3.select(this.el).append("svg"); | |
this.width = $(this.svg[0][0]).outerWidth(); | |
this.height = $(this.svg[0][0]).outerHeight(); | |
this.projection = d3.geo.albersUsa().scale(400).translate([this.width/2, this.height/2]); | |
this.path = d3.geo.path().projection(this.projection); | |
// Not ideal, but grab divs where map images. | |
this.$railsDiv = this.$el.parent().find(".rail-map div"); | |
// Returns GeoJSON MultiLineString geometry object. Basically the | |
// border in high detail. | |
// https://github.com/mbostock/topojson/wiki/API-Reference#mesh | |
this.mesh = topojson.mesh(this.us, this.us.objects.state, function(a, b) { | |
return a !== b; | |
}); | |
this.listenTo(vent, "router:stateChange", this.stateChange); | |
}, | |
/** | |
* Changes the view based on the state change. | |
*/ | |
stateChange: function(currentData) { | |
// Update the map path to have class 'selected'. | |
this.svg.selectAll(".land").classed("selected", function(e, i) { | |
return e.properties.STUSPS10 === currentData.general.post; | |
}); | |
// Update the state dropdown. | |
d3.select("#state-selectbox") | |
.selectAll("option") | |
.property("selected", function(d) { | |
return currentData.general.post === d[0]; | |
}); | |
// Add in rail map if not national overview.If it is the national | |
// overview then this entire div would be hidden by the helper class | |
// specified in the AppView. | |
if (US_TOTAL_KEY_ABBR !== currentData.general.post) { | |
var imgName = stateHash[currentData.general.post].toLowerCase().replace(/\s+/g, "-"); | |
this.$railsDiv.html("") | |
.append( | |
$("<img />") | |
.attr("src", DATA_DIR + "img/rails/" + imgName + ".jpg") | |
.attr("height", 315) | |
.attr("width", 350) | |
); | |
} | |
}, | |
/** | |
* Called when a state is clicked or dropdown changed. | |
* It will call the event which will trigger a route change. | |
*/ | |
stateSelect: function(d) { | |
// Get value from either a map selection of dropdown. | |
var abbr = d.currentTarget ? d.currentTarget.value : d.properties.STUSPS10; | |
// If national view make root. | |
appRouter.navigate(abbr === US_TOTAL_KEY_ABBR ? "/" : "state/"+abbr, { | |
trigger: true | |
}); | |
}, | |
/** | |
* Draw the map with d3. | |
*/ | |
render: function() { | |
if (this._rendered) { | |
return false; | |
} | |
this._rendered = true; | |
// Insert paths for each state. | |
this.svg | |
.selectAll("path") | |
.data(topojson.feature(this.us, this.us.objects.state).features) | |
.enter() | |
.append("path") | |
.attr("class", function(d) { | |
return "land feature_"+d.id; | |
}) | |
.attr("d", this.path); | |
// Insert the state boundaries. | |
this.svg | |
.insert("path", ".graticule") | |
.datum(this.mesh) | |
.attr("class", "state-boundary") | |
.attr("d", this.path); | |
// Generate select dropdown. | |
d3.select("#state-selectbox") | |
.selectAll("option") | |
.data(_.pairs(stateHash)) | |
.enter() | |
.append("option") | |
.text(function(d, i) { | |
return d[1]; | |
}) | |
.property("value", function(d) { | |
return d[0]; | |
}); | |
// Event listeners. | |
this.svg.selectAll(".land").on("click", _.bind(this.stateSelect, this)); | |
return this; | |
} | |
}); | |
// The header overview for the table. | |
var OverviewDataView = Backbone.View.extend({ | |
initialize: function() { | |
this.$headers = this.$el.find("[class^=hr-]"); | |
this.$gridlock = this.$el.find(".gridlock"); | |
this.listenTo(vent, "router:stateChange", this.render); | |
}, | |
render: function(currentData) { | |
this.$gridlock.text(currentData.general.gridlock); | |
$(this.$headers[0]).text(currentData.general.railroads); | |
$(this.$headers[1]).text(currentData.general.railmiles); | |
$(this.$headers[2]).text(currentData.general.employees); | |
$(this.$headers[3]).text(currentData.general.earnings); | |
$(this.$headers[4]).text(currentData.general.retirees); | |
} | |
}); | |
// View for the chart containing graph. | |
var ChartView = Backbone.View.extend({ | |
initialize: function() { | |
this.barHistory = []; | |
this.barIndex = 0; | |
this.$originatingRow = this.$el.find(".originating-data-row"); | |
this.$terminatingRow = this.$el.find(".terminating-data-row"); | |
this.$originatingCols = this.$originatingRow.find("[class^=col-]"); | |
this.$terminatingCols = this.$terminatingRow.find("[class^=col-]"); | |
this.listenTo(vent, "router:stateChange", this.render); | |
}, | |
render: function(currentData) { | |
this.barIndex = 0; | |
this.$originatingCols.add(this.$terminatingCols).html(""); // Clear all cols. | |
this.drawChart(currentData.originating.commodity, this.$originatingCols); | |
this.drawChart(currentData.terminating.commodity, this.$terminatingCols); | |
}, | |
roundDecimal: function(num) { | |
return num % 1 !== 0 ? num.toFixed(1) : num; | |
}, | |
drawChart: function(commodityData, $cols) { | |
var tons = _.map(commodityData, function(d) { | |
return d.tons; | |
}); | |
var min = d3.min(tons); | |
var max = d3.max(tons); | |
var scale = d3.scale.linear().domain([min, max]).range([0, 100]); | |
var bar = null; | |
var pct = null; | |
_.each(commodityData, function(details, commodity) { | |
pct = scale(details.tons); | |
bar = $("<span "+this.barAttrs(scale, pct, this.barIndex)+">"); | |
$($cols[0]).append("<span>"+details.name+"</span>"); | |
$($cols[1]).append("<span>"+details.tons+"</span>"); | |
$($cols[2]).append("<span>"+this.roundDecimal(pct)+"%</span>"); | |
$($cols[3]).append(bar); | |
this.barIndex++; | |
}, this); | |
// Apply basic animation to allow bars to grow from their previous size. | |
// Why am I using async's each instead of jQuery? Just experimenting. | |
var $el; | |
async.each(this.$el.find(".bar"), function(el) { | |
$el = $(el); | |
$el.stop(true, false).animate({ | |
width: $el.data("towidth") | |
}, 300); | |
}); | |
}, | |
barAttrs: function(scale, pct, index) { | |
var w1 = 0; | |
var w2 = pct; | |
if (this.barHistory[index]) { | |
w1 = this.barHistory[index]; | |
} | |
this.barHistory[index] = w2; | |
return "class='bar' style='width:"+w1+"%;' data-towidth='"+w2+"%'"; | |
} | |
}); | |
// The mother application view. | |
var AppView = Backbone.View.extend({ | |
/** | |
* Parent level element. | |
*/ | |
el: $("#railroads_and_states")[0], | |
/** | |
* Basic application constructor. Called after DOM ready so elements can | |
* be specified here. | |
*/ | |
initialize: function(data, us) { | |
this.stateData = data; | |
// Create, render. | |
this.mapView = new MapView({ | |
el: this.$el.find(".interactive-map")[0], | |
data: data, | |
us: us | |
}).render(); | |
// Doesn't get rendered because there isn't an internal listener | |
// on route change to render itself. | |
this.overviewDataView = new OverviewDataView({ | |
el: this.$el.find(".overview-data")[0] | |
}); | |
// Doesn't get rendered because there isn't an internal listener | |
// on route change to render itself. | |
this.chartView = new ChartView({ | |
el: this.$el.find(".chart-data")[0] | |
}); | |
// Various global helpers for populating elements on state changes. | |
this.$hideOnNationHelper = this.$el.find(".hide-nation-helper"); | |
this.$stateNameHelper = this.$el.find(".state-name-helper"); | |
this.$stateReportUrlHelper = this.$el.find(".state-download-link-helper"); | |
vent.on("router:stateChange", _.bind(function(currentData) { | |
this.$stateNameHelper.text(stateHash[currentData.general.post] || "United States"); | |
this.$stateReportUrlHelper.attr("href", currentData.general.pdf); | |
if (US_TOTAL_KEY_ABBR === currentData.general.post) { | |
this.$hideOnNationHelper.hide(); | |
} else { | |
this.$hideOnNationHelper.show(); | |
} | |
}, this)); | |
}, | |
render: function() { } | |
}); | |
// Application router. The router is responsible for looking up the data | |
// the other views need and passing it in an event. | |
var AppRouter = Backbone.Router.extend({ | |
initialize: function(data) { | |
this.data = data; | |
// Set routes. | |
this.route(/^$/, "index"); | |
this.route(/^state\/([a-z]+)$/i, "state"); | |
}, | |
index: function() { | |
vent.trigger("router:stateChange", { | |
general: this.data.general[US_TOTAL_KEY], | |
originating: this.data.originating[US_TOTAL_KEY], | |
terminating: this.data.terminating[US_TOTAL_KEY] | |
}); | |
}, | |
state: function(abbrev) { | |
vent.trigger("router:stateChange", { | |
general: this.data.general[stateHash[abbrev.toUpperCase()]], | |
originating: this.data.originating[stateHash[abbrev.toUpperCase()]], | |
terminating: this.data.terminating[stateHash[abbrev.toUpperCase()]] | |
}); | |
} | |
}); | |
// Perform async loading of necessary files and then kick things off. | |
// This will bootstrap the entire app. | |
async.parallel([ | |
function(callback) { | |
$.getJSON(DATA_DIR + "data.json", function(json) { | |
callback(null, json); | |
}) | |
.fail(function() { | |
callback(new Error("Error loading data file.")); // Rare chance. | |
}); | |
}, | |
function(callback) { | |
$.getJSON(DATA_DIR + "us-state.topo.json", function(json) { | |
callback(null, json); | |
}) | |
.fail(function() { | |
callback(new Error("Error loading topojson file.")); // Rare chance. | |
}); | |
} | |
], function(err, result) { | |
if (err) { | |
return console.error(err.message); | |
} | |
var data = result[0]; // Chart data. | |
var topo = result[1]; // Topojson. | |
// Below all the data is parsed and formatted for the application. | |
// All of the manipulation only happens once and on load. Essentially, | |
// only the calculations for the U.S. in total and the state hash | |
// need to be created. | |
// | |
// UPDATE: Turns out the national data is static in the data already, | |
// commenting out most of below. | |
// | |
// Create state hash from data. | |
stateHash = _.chain(data.general) | |
.map(function(v, k) { | |
return [v.post, k]; | |
}) | |
.object() | |
.value(); | |
// // Aggregate all date in the U.S. to determine totals. | |
// var originatingTotal = data.originating[US_TOTAL_KEY] = { commodity: {}, abbr: null }; | |
// var terminatingTotal = data.terminating[US_TOTAL_KEY] = { commodity: {}, abbr: null }; | |
// async.parallel([ | |
// aggregate(data.originating[US_TOTAL_KEY], 'originating'), | |
// aggregate(data.terminating[US_TOTAL_KEY], 'terminating') | |
// ]); | |
// function aggregate(obj, key) { | |
// return function() { | |
// _.each(data[key], function(stateVal, stateName) { | |
// _.each(stateVal.commodity, function(commodityProps, commodityName) { | |
// if (!obj.commodity[commodityName]) { | |
// obj.commodity[commodityName] = {}; | |
// } | |
// _.each(commodityProps, function(propVal, propName) { | |
// if (!obj.commodity[commodityName][propName]) { | |
// obj.commodity[commodityName][propName] = 0; | |
// } | |
// if (_.isNumber(propVal)) { | |
// obj.commodity[commodityName][propName] = obj.commodity[commodityName][propName] + propVal; | |
// } else { | |
// obj.commodity[commodityName][propName] = propVal; | |
// } | |
// }); | |
// }); | |
// }); | |
// }; | |
// } | |
// Init the application. | |
appView = new AppView(data, topo); | |
appRouter = new AppRouter(data); | |
// Start routing with html5 pushState off. | |
Backbone.history.start({ | |
pushState: false | |
}); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment