Skip to content

Instantly share code, notes, and snippets.

@chriscanipe
Last active January 28, 2017 13:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chriscanipe/071984bcf482971a94900a01fdb988fa to your computer and use it in GitHub Desktop.
Save chriscanipe/071984bcf482971a94900a01fdb988fa to your computer and use it in GitHub Desktop.
US Election Base Map
license: mit
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background {
fill: #FFF;
}
.map-target {
width: 100%;
position: relative;
}
.home-btn {
position: absolute;
top: 15px;
right: 15px;
display: none;
}
.tooltip {
position: absolute;
padding: 4px 8px;
background-color: #fff;
z-index: 2;
text-align: center;
border: 1px solid #CCC;
display: none;
}
.map-g .feature {
stroke: #FFF;
cursor: zoom-in;
}
.map-g .feature.active {
stroke: #000;
}
.map-g.zoomed .background {
cursor: zoom-out;
}
.map-g.zoomed .feature.centered {
cursor: zoom-out;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<form>
<input type="radio" name="level" val="states" checked="checked">States </input>
<input type="radio" name="level" val="counties">Counties </input>
<input type="radio" name="level" val="districts">Districts </input>
</form>
<div class="map-target"></div>
<script>
var electionMap = function(opts) {
// load in arguments from config object
this.geo = opts.geo;
this.element = opts.element;
this.view = opts.view;
this.colors = {
blank: "#CCC" //Would add additional colors to our lookup here
}
// create the Map
this.draw();
this.setView();
this.update();
}
electionMap.prototype.draw = function() {
//Set width/height/margins
this.setDimensions();
// set up parent element and SVG
this.element.innerHTML = "";
this.svg = d3.select(this.element).append('svg');
this.element.style.width = this.width;
this.svg.attr('width', this.width);
this.svg.attr('height', this.height);
this.centered = null; //Store path data if map is zoomed to path
this.isZoomed = false; //Store path data if map is zoomed to path
this.maxZoom = 5; //Level to zoom into when area or region is clicked.
this.lineStroke = .5; //Stroke width to maintain at various zoom levels.
this.homeButton(); //Add a "reset map" button to the target element
// we'll actually be appending to a <g> element
this.plot = this.svg.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')')
.attr("class", "map-g");
//Append an invisible background element so we have something to click on in negative space
this.plot.append("rect")
.attr("class", "background")
.attr("x", 0)
.attr("y", 0)
.attr("width", this.width)
.attr("height", this.height)
.on("click", function(d) {
var el = this;
if (_this.isZoomed) {
_this.clicked(d, el);
}
});
//Append the tooltip div to the map target
this.tooltipDiv = d3.select(this.element)
.append("div")
.attr("class", "tooltip");
//Set the projection according to width/height
this.resetProjection();
/* DRAW THE MAP FEATURES */
var _this = this; //Store value of this for use inside selection-nested functions
/* ------------------- */
/* STATES */
/* ------------------- */
var states = this.plot.append("g")
.attr("class", "states-g");
states.selectAll("path")
.data(topojson.feature(this.geo, this.geo.objects.states).features)
.enter().append("path")
.attr("d", _this.path)
.attr("class", "state feature");
/* END STATES */
/* ------------------- */
/* ------------------- */
/* COUNTIES */
/* ------------------- */
var counties = this.plot.append("g")
.attr("class", "counties-g");
counties.selectAll("path")
.data(topojson.feature(this.geo, this.geo.objects.counties).features)
.enter().append("path")
.attr("d", _this.path)
.attr("class", "county feature");
/* END COUNTIES */
/* ------------------- */
/* ------------------- */
/* DISTRICTS */
/* ------------------- */
var districts = this.plot.append("g")
.attr("class", "districts-g");
districts.selectAll("path")
.data(topojson.feature(this.geo, this.geo.objects.districts).features)
.enter().append("path")
.attr("d", _this.path)
.attr("class", "district feature");
/* END DISTRICTS */
/* ------------------- */
//Assign mouse events to all geographies
var features = this.plot.selectAll(".feature")
.on("mouseover", function(d) {
d3.select(this).classed("active", true).moveToFront();
_this.tooltip(d);
_this.tooltipDiv.style('display', 'inherit');
})
.on("mouseout", function(d) {
d3.select(this).classed("active", false);
_this.tooltipDiv.style('display', 'none');
})
.on("mousemove", function() {
//Get page offset position of map container
//This tooltip positioning method should work across browsers
var bodyRect = document.body.getBoundingClientRect(),
elemRect = _this.element.getBoundingClientRect(),
offsetTop = elemRect.top - bodyRect.top,
offsetLeft = elemRect.left - bodyRect.left;
//Mouse positions
var xPos = d3.event.pageX - offsetLeft;
var yPos = d3.event.pageY - offsetTop;
//Tooltip dimensions
var ttWidth = parseInt(_this.tooltipDiv.style("width").replace("px", ""), 10);
var ttHeight = parseInt(_this.tooltipDiv.style("height").replace("px", ""), 10);
//Tooltip positions
var ttLeft = xPos - (ttWidth / 2);
var ttTop = yPos - ttHeight - 30;
//Some spacing logic to ensure tooltip doesn't get cut off by parent container
var maxRight = _this.width - (ttWidth / 2);
//If too far to the right
if (ttLeft + (ttWidth / 2) >= maxRight) {
ttLeft = maxRight - (ttWidth / 2);
}
//If too close to the top
if (ttTop < 0) {
ttTop = yPos + 30;
}
//If too far to the left
if (ttLeft < 0) {
ttLeft = 0;
}
_this.tooltipDiv.style({
"top": ttTop + "px",
"left": ttLeft + "px"
});
})
.on("click", function(d) {
var el = this;
_this.clicked(d, el);
});
}
electionMap.prototype.setColor = function(val) {
if (!val) {
return this.colors.blank; //Would add logic to here to color feature by result
}
}
electionMap.prototype.resetProjection = function() {
//Multiplier to determine how map fits in container.
projectionRatio = 1;
this.path = d3.geo.path();
this.projection = d3.geo.albersUsa()
.scale(this.width * projectionRatio)
.translate([this.width / 2, this.height / 2]);
this.path.projection(this.projection);
}
electionMap.prototype.setDimensions = function() {
// define width, height and margin
this.width = this.element.offsetWidth;
this.height = this.element.offsetWidth * .5; //Determine desired height here
this.margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
};
}
//Set view as "states", "counties" or "districts"
electionMap.prototype.setView = function() {
var states = this.plot.select(".states-g");
var counties = this.plot.select(".counties-g");
var districts = this.plot.select(".districts-g");
if (this.view === "states") {
states.style("display", "inherit");
counties.style("display", "none");
districts.style("display", "none");
} else if (this.view === "counties") {
states.style("display", "none");
counties.style("display", "inherit");
districts.style("display", "none");
} else if (this.view === "districts") {
states.style("display", "none");
counties.style("display", "none");
districts.style("display", "inherit");
}
}
/* ---------------------------------------- */
/* FIRE THIS FUNCTION TO UPDATE MAP DATA */
/* ---------------------------------------- */
electionMap.prototype.update = function(liveData) {
/* ------------- */
/* This model assumes data would be available as a dictionary object */
/* in which results are looked up by geographic ID or FIPS values */
/* Each feature in the selections below has a unique state, county, or district ID */
/* So you'd use it to look up the corresponding result in each iteration */
/* ------------- */
//Set width/height/margins
this.setDimensions();
//Set the projection according to width/height
//this.resetProjection();
//Update svg dimensions
this.svg.attr('width', this.width);
this.svg.attr('height', this.height);
var _this = this;
this.plot.selectAll("path")
.attr("d", _this.path);
var states = this.plot.select(".states-g");
var counties = this.plot.select(".counties-g");
var districts = this.plot.select(".districts-g");
states.selectAll("path")
.attr("fill", function(d) {
var winner = null;
return _this.setColor(winner);
});
counties.selectAll("path")
.attr("fill", function(d) {
var winner = null;
return _this.setColor(winner);
});
districts.selectAll("path")
.attr("fill", function(d) {
var winner = null;
return _this.setColor(winner);
});
}
electionMap.prototype.homeButton = function() {
var _this = this;
d3.select(this.element).append("button")
.attr("class", "home-btn")
.html("Reset Map")
.on("click", function() {
//Recenter map
_this.zoomScale(1, (_this.width) / 2, (_this.height / 2));
d3.select(this).style("display", "none");
})
}
//Zoom to center of selected feature when clicked
electionMap.prototype.clicked = function(d, el) {
var x, y, k; //left, top, zoom
var _this = this;
if (d && _this.centered !== d) {
var centroid = _this.path.centroid(d);
x = centroid[0];
y = centroid[1];
k = _this.maxZoom;
_this.centered = d;
d3.select(el).classed("centered", true);
_this.isZoomed = true;
d3.select(".home-btn").style("display", "inherit");
} else {
x = _this.width / 2;
y = _this.height / 2;
k = 1;
_this.centered = null;
d3.select(el).classed("centered", false);
_this.isZoomed = false;
d3.select(".home-btn").style("display", "none");
}
_this.plot.classed("zoomed", _this.isZoomed);
_this.zoomScale(k, x, y);
}
//Set new scale and translate position and size strokes according to scale.
electionMap.prototype.zoomScale = function(k, x, y) {
var _this = this;
_this.plot.transition()
.duration(750)
.attr("transform", "translate(" + (_this.width / 2) + "," + (_this.height / 2) + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
_this.plot.selectAll(".feature")
.style("stroke-width", (_this.lineStroke / k));
}
electionMap.prototype.tooltip = function(d) {
var txt;
if (this.view === "states") {
txt = "State FIPS: " + d.id;
} else if (this.view === "counties") {
txt = "County FIPS: " + d.id;
} else if (this.view === "districts") {
txt = "Congressional Dist. ID: " + d.id;
}
d3.select(".tooltip").html(txt);
}
function init() {
d3.json("http://chriscanipe.com/data/us2016.topo.json", function(error, us) {
if (error) throw error;
// create new Map using Map constructor
var theMap = new electionMap({
element: document.querySelector('.map-target'),
geo: us,
view: "states"
});
//Set button toggle for view state
d3.selectAll("input")
.on("click", function() {
theMap.view = d3.select(this).attr("val");
theMap.setView();
});
// redraw Map on each resize
d3.select(window).on('resize', function() {
theMap.update();
});
});
}
init();
/* HELPER FUNCTIONS */
//Move to front and back controls z-index of features on mouseover and mouseout.
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
</script>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment