Skip to content

Instantly share code, notes, and snippets.

@psaia
Created April 2, 2014 19:52
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 psaia/9941815 to your computer and use it in GitHub Desktop.
Save psaia/9941815 to your computer and use it in GitHub Desktop.
(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