nykood
7/28/2016 - 8:31 PM

Graphing Maine power outages with D3. The "meta refresh" is the sub-optimal way of updating every 5 minutes, but the fam was getting a bit i

Graphing Maine power outages with D3. The "meta refresh" is the sub-optimal way of updating every 5 minutes, but the fam was getting a bit irked that I was coding on Thanksgiving. See previous gists and http://rud.is/b entries for why I made this. UPDATE : 2013-12-23 : mouseover now shows historical graphs of outages; data table cleaned up and total added; scripts that maintain & generate historical data aded to gist. Live preview @ http://rud.is/outage ; UPDATE: 2013-12-24 : added towns choropleth after discovering the seekrit CMP JSON data feeds.UPDATE : 2013-12-26 : switched to a more useful "table" which now has hoverable and clickable sparklines vs plain text; also created an R script to make the time series much nicer and accounted for no power outages in the sparkline "table"

<!DOCTYPE html>
<!--
-- by @hrbrmstr (2013)
-- MIT License
-->
<html>
<head>
<title>Central Maine Power Live Outage Map</title>
<meta charset="utf-8"/>
<meta http-equiv="refresh" content="300"/>

<!--

Grabbed counties.zip from http://www.baruch.cuny.edu/geoportal/data/esri/esri_usa.htm

It has tons of good data. Check it out with:
  ogrinfo -al -so counties.shp

Extracted "Maine"
  ogr2ogr -f GeoJSON -where "STATE_NAME = 'MAINE'" maine.json counties.shp

Built topojson including county FIPS (just in case)
  topojson --id-property NAME -p name=county -p CNTY_FIPS -o county.json maine.json

NOTE: I renamed the top-level object from "maine" to "counties" by hand &
      for production usage sanity I renamed county.json back to maine.json 

CMP has an outage page - http://www.cmpco.com/outages/outageinformation.html - with
an embedded iframe that is generated by their SAP system (u haven't checked for the 
frequency). I have a python script that extracts the table every 5 mintues and puts it
into a CSV file for use by this web app. 

Now uses: https://github.com/caged/d3-tip for tooltips vs svg path title text

-->

<link rel="stylesheet" type="text/css" href="outage.css"  />
<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lato:300,400,700" />

<script src="d3.v3.min.js" type="text/javascript" charset="utf8"></script>
<script src="topojson.v1.min.js" type="text/javascript" charset="utf8"></script>
<script src="d3.tip.min.js" type="text/javascript" charset="utf8"></script>
<script src="jquery-1.10.2.min.js" type="text/javascript" charset="utf8"></script>
<script src="jquery-migrate-1.2.1.min.js" type="text/javascript" charset="utf8"></script>
<script src="jquery.tinysort.min.js" type="text/javascript" charset="utf8"></script>

<script>

String.prototype.toTitleCase = function() {
  return this.replace(/\w\S*/g, function(txt) {return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}

var outages ;
var outageTable ;
var maineMap ;
var tip ;
var width, height ;
var outColor;
var commasFormatter = d3.format(",")
var fmt = d3.time.format("%Y-%m-%d");

var summary = { } ;
var inverts = { } ;
var COUNTIES = [ 'ANDROSCOGGIN', 'CUMBERLAND', 'FRANKLIN', 'HANCOCK', 'KENNEBEC', 
							   'KNOX', 'LINCOLN', 'OXFORD', 'PENOBSCOT', 'PISCATAQUIS', 
								 'SAGADAHOC', 'SOMERSET', 'WALDO', 'YORK' ];
var data = [ ];
var dt = new Date();
var alphaSort = true ; // initial view is sorted by countay alpha name
			
toggleSort = function() {
		
	$("#h").css({left:"-1000px"});
	$("#h").hide();	
	
	if (alphaSort) {
		alphaSort = false ;
		$('div#charts>div').tsort({ sortFunction: function(a, b) {
			return(summary[b.e[0].id] - summary[a.e[0].id]) ;
			}});
	} else {
    alphaSort = true ;
		$('div#charts>div').tsort({ attr:'id' });
	}
		
}

bisectDate = d3.bisector(function(d) { return d.ts; }).left ;

function mousemove() {

	var inv = inverts[d3.select(this)[0][0].id]
	var x0 = inv.X.invert(d3.mouse(this)[0]) ;
	var i = bisectDate(inv.data,x0,1);
	var d0 = inv.data[i-1];
	var d1 = inv.data[i];
	var d = x0 - d0.ts > d1.ts - x0 ? d1 : d0 ;

  var pos = d3.mouse(d3.select("body").node()) ;

	$("#h").html(fmt(d.ts) + " : " + commasFormatter(d.withoutPower));
	$("#h").css({left:pos[0],top:pos[1]});
	$("#h").show();
	
}


addCharts = function() {

	var tsParser = d3.time.format("%Y-%m-%d %H:%M") ;

	var width = 250 ;
  var height = 20;
	var sparkWidth = 100 ;

	var X = d3.scale.linear().range([0, sparkWidth]);
	var Y = d3.scale.linear().range([height,0]);

	var line = d3.svg.line()
				   .x(function(d) { return X(d.ts); })
				   .y(function(d) { return Y(d.withoutPower); });
	
	
	$.each(COUNTIES, function(i, county) {

		var chart = d3.select("#"+county).append("svg")
						    .attr("width", width)
						    .attr("height", height);
		
		var g = chart.append("g") ;
		
		d3.csv("data/"+county+".csv?"+dt.getTime(), function(d) {

		  return {
		    ts: tsParser.parse(d.ts.substring(0,16)), // don't need precision below the minute
		    withoutPower: +d.withoutPower //  convert to number
		  };

		}, function(error, rows) {
							
		  summary[county] = rows[rows.length-1].withoutPower ;
			$("#"+county).click(toggleSort);
					
		  X.domain(d3.extent(rows, function(d) { return d.ts; }));
		  Y.domain(d3.extent(rows, function(d) { return d.withoutPower; }));	
			
			inverts["rect_"+county] = {X:X,Y:Y,data:rows} ;			

      var labelG = g.append("g");				
			labelG.append("text")
						.attr("x",90)
						.attr("y",15)
						.attr("id", "t_"+county)
						.text(county.toTitleCase())
						.attr("text-anchor","end")
						.attr("font-family","Lato")
						.attr("font-size", "10px")
						.attr("font-weight", "300")
						.attr("fill", "black") ;
						
			var sparkG = g.append("g")
										.attr("transform", "translate(100,0)");							

			var sparkGRect = sparkG.append("rect")
				.attr("x1",0)
				.attr("y1",0)
				.attr("id", "rect_" + county)
				.attr("width",sparkWidth)
				.attr("height",height)
	      .on("mouseover", function() { 
					$("#h").css({'display':null}); 
				})
	      .on("mouseout", function() { 
					$("#h").css({left:"-1000px"});
					$("#h").hide();
				})
				.on("mousemove", mousemove)
				.style({"stroke":"black"})
				.style({"stroke-width":"0.25"})
				.style("fill",outColor(rows[rows.length-1].withoutPower));
		  			
		  sparkG.append("path")
	      .datum(rows)
	      .attr("class", "line")
				.attr("pointer-events", "none")
				.style({"stroke":"white"})
	      .attr("d", line);

      var valueG = g.append("g")
										.attr("transform", "translate(205,0)");			
														
			valueG.append("text")
						.attr("x",0)
						.attr("y",15)
						.text(commasFormatter(rows[rows.length-1].withoutPower))
						.attr("text-anchor","start")
						.attr("font-family","Lato,Helvetica,sans-serif")
						.attr("font-size", "10px")
						.attr("fill", "black") ;
	
		});
				
	});
	
	$("#dethead").click(toggleSort);
	
}

$(document).ready(function(){

	width = 450 ;
  height = 600;

	maineMap = d3.select("#map").append("svg")
					    .attr("width", width)
					    .attr("height", height);

	var legendSVG = d3.select("#maplegend").append("svg") ;

	var counties ; 

	// set colors for the ranges

	var outageThresholds = [ 100, 1000, 10000, 100000, 1000000 ];
	var thresholdColors = ['rgb(253,208,162)','rgb(253,174,107)','rgb(253,141,60)','rgb(241,105,19)','rgb(217,72,1)','rgb(140,45,4)'];
	outColor = d3.scale.threshold()
                 .domain(outageThresholds)
                 .range(thresholdColors);

	tip = d3.tip()
	 .attr('class', 'd3-tip')
	 .offset([-10, 0])
	 .html(function(d) {
		 if (d.properties.withoutPower > 0) {
		   return "<center><span style='color:white'>" + d.id + " County</span><br/><span style='color:red'>" + commasFormatter(d.properties.withoutPower) + " w/o power</span><br/><span style='color:yellow'>Select for details</span></center>";
		 } else {
		   return "<center><span style='color:white'>" + d.id + " County</span><br/><span style='color:yellow'>Select to  report an outage</span></center>";
		 	
		 }
	 })

 	maineMap.call(tip);

	// build the map
	function redraw() { 
		
		d3.json("maine.json", function(error, maine) {
	
			// get the topojson features object
			counties = topojson.feature(maine, maine.objects.counties);
	
			// read in the CSV data
			d3.csv("current.csv?"+dt.getTime(), function(d) {

			  return {
			    id: d.county,
					population: +d.population,
			    withoutPower: +d.withoutpower
			  };
		
			}, function(error, rows) {
		
			  outages = rows;
		
				// add the outage data to the topojson features object
	
				for (var i=0; i<outages.length; i++) {
			
					var withoutPower = outages[i].withoutPower;
					var county = outages[i].id;
			
					for (var j=0; j<counties.features.length; j++) {
						var mCounty = counties.features[j].properties.county;
						if (county == mCounty) {
							counties.features[j].properties.withoutPower = withoutPower;
							break;
						}
					}
				}
		
				// setup the projection
	
				var projection = d3.geo.mercator()
				    .center([-69,45]) // rly close to the "center" of maine
					  .scale(5000) // this seems to work well as a scale for maine
					  .translate([240,350]); // move it over so there's room for real data
		
				var path = d3.geo.path()
				    .projection(projection);
				
				// create the map with outages
	
				maineMap.selectAll(".county")
					.data(counties.features)
					.enter().append("path")
					.style('stroke-width','0.50')
					.on("mouseover", function(d, i) {
						d3.select(this.parentNode.appendChild(this)).transition().duration(150)
						        .style({'stroke-opacity':1.0,'stroke':'#F00','stroke-width':'0.50'});
						tip.show(d) ;
						$("#tip_" + d.id.toUpperCase()).css("visibility","visible") ;
						$("#tip_" + d.id.toUpperCase()).css("top","450px") ;
						$("#tip_" + d.id.toUpperCase()).css("left","10px") ;
						$("#tip_" + d.id.toUpperCase()).show() ;
						d3.select("#t_"+d.id.toUpperCase()).attr("font-weight","700");
						
					})
					.on("mouseout", function(d, i) {
						d3.select(this.parentNode.appendChild(this)).transition().duration(150)
						        .style({'stroke-opacity':1.0,'stroke':'#7f7f7f','stroke-width':'0.50'});
						tip.hide(d);
						$("#tip_" + d.id.toUpperCase()).hide() ;
						$("#tip_" + d.id.toUpperCase()).css("left","-1000px") ;
						d3.select("#t_"+d.id.toUpperCase()).attr("font-weight","300")
						
					})
					.attr("id", function(d) { return d.id; })
					.attr("class", function(d) { return "county " + d.id; })
					.attr("fill", function(d) {
						if (d.properties.withoutPower > 0) {
							return(outColor(d.properties.withoutPower));
						} else {
							return("white");
						}
					} )
					.on("click", function(d, i) {
						window.open('http://www3.cmpco.com/OutageReports/CMP'+d.id.toUpperCase()+".html",'_blank');
					})
					.attr("d", path);

				// now build the legend

				legend = legendSVG.selectAll(".lentry")
										      .data(outColor.domain())
										      .enter()
										      .append("g")
										      .attr("class","leg")

				legend.append("rect")
							.attr("y", function(d,i) { return(i*40)})
							.attr("width","40px")
							.attr("height","40px")
							.attr("fill", function(d) { return outColor(d) ; })
							.attr("stroke","#7f7f7f")
							.attr("stroke-width","0.5");
			
				legend.append("text")
							.attr("class", "legText")
							.text(function(d, i) { return "≤ "+commasFormatter(outageThresholds[i]) ; })
							.attr("x", 45)
							.attr("y", function(d, i) { return (40 * i) + 20 + 4; })
			
			});	
			
		});	
		
	};
	
	redraw();
	addCharts();

	setTimeout(toggleSort,500);
		
});
</script>
</head>
<body>
<center><h2 style="padding-bottom:5px;margin-bottom:0px;">Central Maine Power Live Outage Map</h2>Switch to <a href="towns/">Town Detail View</a></center>
<center>
<div id="container" class="container">
<div id="maplegend" class="maplegend"></div>
<div id="map" class="map"></div>
<div id="details" class="details">
	<div id="dethead" style="margin-bottom:0;padding:0;cursor:pointer">
		<div style="font-weight:700;text-align:right;float:left;font-size:10px;width:90px">County</div>
		<div style="font-weight:700;font-size:10px;float:left;margin:0;padding:0;width:10px;height:12px">&nbsp;</div>
		<div style="font-weight:700;margin:0;padding:0;float:left;font-size:10px;width:100px">History</div>
		<div style="font-weight:700;font-size:10px;float:left;margin:0;padding:0;width:5px;height:12px">&nbsp;</div>
		<div style="font-weight:700;margin:0;padding:0;float:left;font-size:10px"># Out</div>
	</div><br style="height:1px;padding:0;margin:0"/>
	<div id="charts" style="clear:all">
		<div id="ANDROSCOGGIN" class="countychart"></div>
		<div id="CUMBERLAND" class="countychart"></div>
		<div id="FRANKLIN" class="countychart"></div>
		<div id="HANCOCK" class="countychart"></div>
		<div id="KENNEBEC" class="countychart"></div>
		<div id="KNOX" class="countychart"></div>
		<div id="LINCOLN" class="countychart"></div>
		<div id="PENOBSCOT" class="countychart"></div>		
		<div id="PISCATAQUIS" class="countychart"></div>
		<div id="SAGADAHOC" class="countychart"></div>
		<div id="SOMERSET" class="countychart"></div>
		<div id="OXFORD" class="countychart"></div>
		<div id="WALDO" class="countychart"></div>
		<div id="YORK" class="countychart"></div>
	</div>
</div>
</div>
</center>
<div id="h" style="text-align:center;padding:3px;font-size:10px;color:white;background:black;width:100px;height:12;position:absolute;left:-1000px;pointer-events:none;">sdfsdfsnmm</div>
<hr style="clear:both" noshade size="1">
<center>By <a href="http://twitter.com/hrbrmstr">@hrbrmstr</a> | Powered by <a href="http://d3js.org/">d3</a> | Source at <a href="https://gist.github.com/hrbrmstr/7700364">github</a> | <a href="http://twitter.com/cmpco">@CMPCO</a>'s <a href="http://outagemap.cmpco.com/maine/?style=maine#">Interactive ESRI Map</a></center>
</script>
</body>
</html>
#!/usr/bin/Rscript
# running in a cron job so no spurious text pls

options(warn=-1)
options(show.error.messages=FALSE)

suppressMessages(library(methods))
suppressMessages(library(zoo))
library(chron)
library(xts)
library(reshape2)
library(ggplot2)
library(scales)
library(DBI)
library(RMySQL)

m <- dbDriver("MySQL");
con <- dbConnect(m, user='DBUSER', password='DBPASSWORD', host='localhost', dbname='DBNAME');
res <- dbSendQuery(con, "SELECT * FROM outage")
outages <- fetch(res, n = -1)
outages$ts <- as.POSIXct(gsub("\\:[0-9]+\\..*$","", outages$ts), format="%Y-%m-%d %H:%M")

for (county in unique(outages$county)) {
  
  outage.raw <- outages[outages$county == county,c(1,4)]
  
  outage.zoo <- zoo(outage.raw$withoutpower, outage.raw$ts)
  
  complete.zoo <- merge(outage.zoo, zoo(, seq(start(outage.zoo), max(outages$ts), by="15 min")), all=TRUE)
  complete.zoo[is.na(complete.zoo)] <- 0
  
  hourly.zoo <- last(to.hourly(complete.zoo), "30 days")
    
  df <- data.frame(hourly.zoo)
  df <- data.frame(ts=rownames(df), withoutPower=df$complete.zoo.High)
  
  write.csv(df, sprintf("OUTPOUT_LOCATION/%s.csv",county), row.names=FALSE)
  
}

#!/bin/bash
# hack to dump each county outage timeline to a CSV file
# YOU SHOULD USE THE R VERSION INSTEAD!

# it doesn't look like CMP serves all Maine counties
COUNTIES=( CUMBERLAND HANCOCK KNOX LINCOLN SAGADAHOC OXFORD WALDO KENNEBEC ANDROSCOGGIN FRANKLIN PISCATAQUIS SOMERSET YORK PENOBSCOT )
OUTPUTDIR="/your/output/dir/outage/data"
TMPDIR="/your/temp/dir"
DBNAME="yourdbname"

for COUNTY in ${COUNTIES[@]}; do

	rm $OUTPUTDIR/$COUNTY.csv

	echo "SELECT ts, withoutpower
FROM outage WHERE county = '$COUNTY'
ORDER BY ts INTO OUTFILE '$OUTPUTDIR/$COUNTY.csv'
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '\"'
LINES TERMINATED BY '\n';" | mysql $DBNAME

	echo "ts,withoutPower" | cat - $OUTPUTDIR/$COUNTY.csv > $TMPDIR/out && mv $TMPDIR/out $OUTPUTDIR/$COUNTY.csv

done
html, body, div, p { font-family: 'Lato', Helvetica, sans-serif; font-weight: 300; }
h1, h2, h3 { font-family: 'Lato', Helvetica, sans-serif; font-weight: 400; }
table, tr, td { font-family: 'Lato', Helvetica, sans-serif; font-weight: 300; font-size:11px;}
thead td { font-family: 'Lato', Helvetica, sans-serif; font-weight: 400; font-size:12px;}

.county {
	stroke:#7f7f7f; 
	stroke-opacity:0.4;
	stroke-width:0.75;
}

.county.Aroostook {}
.county.Somerset {}
.county.Piscataquis {} 
.county.Penobscot {}
.county.Washington {}
.county.Franklin {} 
.county.Oxford {}
.county.Waldo {}
.county.Kennebec {}
.county.Androscoggin {}
.county.Hancock {}
.county.Knox {}
.county.Lincoln {}
.county.Cumberland {}
.county.Sagadahoc {}
.county.York {}

.container {
	padding-left: 30px;
	vertical-align:top;
	margin-left:20px;
	width:850px;
}
.maplegend {
  background-color: #fff;
	margin-top:40px;
  width: 120px;
  height: 300px;
  float:left;
}
.map {	
	width:450px;
	height:600px;
	float:left;
}
.details {
	margin-top:40px;
	margin-left:0px;
	padding-left:0px;
	width:250px;
	height:440px;
	float:left;
}

#charts {
	margin-left:0px;
	padding-left:0px;	
}
.legText {
	color:black;
	font-family: font-family: 'Lato', Helvetica, sans-serif; font-weight: 300;
	font-size:9pt;
}

.d3-tip {
	font-size:9pt;
  line-height: 1.1em;
  font-weight: bold;
  padding: 12px;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  border-radius: 2px;
}

.d3-tip:after {
  box-sizing: border-box;
  display: inline;
  font-size: 10px;
  width: 100%;
  line-height: 1;
  color: rgba(0, 0, 0, 0.8);
  content: "\25BC";
  position: absolute;
  text-align: center;
}

.d3-tip.n:after {
  margin: -1px 0 0 0;
  top: 100%;
  left: 0;
}

.countychart {
	height:20px;
	width:250px;
	margin:0px;
	padding:0px;
	padding-bottom:10px;
}

.line {
  fill: none;
  stroke-width: 1.0px;
}
#!/usr/bin/python
#
# parses CMP website like other table but shoves data into a database and 
# executes at a different frequency than the other one

from bs4 import BeautifulSoup
import requests
from datetime import datetime
import MySQLdb as mdb

r = requests.get('http://www3.cmpco.com/OutageReports/CMP.html')

soup = BeautifulSoup(r.text)
table = soup.find('table')

ts = datetime.now().isoformat(' ')

rows = []
i = 0

try:
    for row in table.find_all('tr'):
        i = i + 1
        if (i<4): continue
        rows.append([val.text.encode('utf8').replace(",", "") for val in row.find_all('td')])

    del rows[-1]

    con = mdb.connect('DBHOST', 'USER', 'PASS', 'DB')
    cur = con.cursor()

    for row in rows:
        cur.execute("INSERT INTO outage VALUES (%s,%s,%s,%s);", (ts, row[0], row[1], row[2]))
        con.commit()
    con.close()

except:
    pass


# Database schema:
#
# CREATE TABLE outage (
#   ts VARCHAR(30),
#   county VARCHAR(50),
#   population INT,
#   withoutpower INT
# )
#!/usr/bin/python
# generate CSV from outages

from bs4 import BeautifulSoup
import requests

r = requests.get('http://www3.cmpco.com/OutageReports/CMP.html')

soup = BeautifulSoup(r.text)
table = soup.find('table')

rows = []
i = 0

for row in table.find_all('tr'):
    i = i + 1
    if (i<4): continue
    rows.append([val.text.encode('utf8').replace(",", "") for val in row.find_all('td')])

if len(rows) > 0:
    del rows[-1]

f = open("/output/dir/current.csv","w")
f.write("county,population,withoutpower\n")
for row in rows:
    f.write("%s,%s,%s\n" % (row[0].title(), row[1], row[2]))
f.close()
<!DOCTYPE html>

<html>
<head>
<title>CMP Outages (%) by Town</title>

<link rel="stylesheet" type="text/css" href="../outage.css"  />
<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lato:300,400" />

<script src="../d3.v3.min.js" type="text/javascript" charset="utf8"></script>
<script src="../topojson.v1.min.js" type="text/javascript" charset="utf8"></script>
<script src="../jquery-1.10.2.min.js" type="text/javascript" charset="utf8"></script>
<script src="../jquery-migrate-1.2.1.min.js" type="text/javascript" charset="utf8"></script>


<script>

// http://www.maine.gov/megis/catalog/shps/state/metwp24s.zip
//
// Maine gov uses transverse mercator so we need to re-project the shapefile to something sane
// topojson --id-property TOWN -p name=TOWN -p COUNTY -o me_towns.json me_towns_geo.json
//
// topojson --simplify-proportion=0.25 --id-property TOWN -p name=TOWN -p COUNTY -o me_towns.json me_towns_geo.json

var maineMap ;
var width, height ;
var meTowns ; 
var me ;
var centered ;
								 
function isEven(n) {
  return isNumber(n) && (n % 2 == 0);
}

function isNumber(n) {
 return n == parseFloat(n);
}
								 
var centers = {} ;

var townOuts = {};
var q;
var quantize ;
var commasFormatter = d3.format(",.0f")
								 
$(document).ready(function(){

	width = 700 ;
  height = 600;

	maineMap = d3.select("#map").append("svg")
					    .attr("width", width)
					    .attr("height", height);
							
	g = maineMap.append("g")
	quantize = d3.scale.quantile()
	             .domain([0, 100]).
							 range(['rgb(252,197,192)','rgb(250,159,181)','rgb(247,104,161)',
							        'rgb(221,52,151)','rgb(174,1,126)','rgb(122,1,119)']);
	
	// build the map
	function redraw() { 

		d3.json("/cmp/towns.json", function(error,json) { 

		  q = json ; 

			for (var i=0; i<json.features.length; i++) { 
			  var a = json.features[i].attributes;
			  townOuts[a.COUNTYNAME + "-" + a.TOWNNAME] = { numServed:+a.NUMSERVED, numOut:+a.NUMOUT, percentOut:+a.PERCENTOUT };
		 
			};
	 
		}) ;
		
		d3.json("me_towns.json", function(error, maine) {
			
			me = maine ;
			
			meTowns = topojson.feature(maine, maine.objects.me_towns_geo);
	
			// get the topojson features object
	
				var projection = d3.geo.mercator()
											     .center([-69,45]) // rly close to the "center" of maine
												   .scale(5000) // this seems to work well as a scale for this maine shapefile
												   .translate([250,320]); // move it over so there's room for real data
		
				var path = d3.geo.path()
				    .projection(projection);	

				function clickme(d) {
										
				  var x, y, k;

				  if (d && centered !== d) {
				    var centroid = path.centroid(d);
				    x = centroid[0];
						y = centroid[1];
				    k = 2.5; // good zoom for this use case
				    centered = d;
				  } else {
				    x = width / 2;
				    y = height / 2;
				    k = 1;
				    centered = null;
				  }

				  g.selectAll("path")
				      .classed("active", centered && function(d) { return d === centered; });

				  g.transition()
				      .duration(500)
				      .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
				      .style("stroke-width", 0.75 / k + "px");
				}	
				
				g.append("g")
				  .selectAll(".town")
					.data(meTowns.features)
					.enter()
					.append("path")
					.attr("id", function(d) { return d.id; })
					.attr("class", function(d) { 
				    return "town " + d.properties.COUNTY  ; 	
					})
					.on("click", clickme)
					.style("stroke", function(d) {
						
						var k =d.properties.COUNTY.toUpperCase() + "-" +  d.properties.name.toUpperCase();

						if (k in townOuts) {
							return("black") ;
						} else {
							return("#7f7f7f");
						}

					})
					.style("stroke-width", "0.25")
					.attr("fill",function(d,i) { 
						
						var k =d.properties.COUNTY.toUpperCase() + "-" +  d.properties.name.toUpperCase();
						
						if (k in townOuts) {
							return(quantize(townOuts[k].percentOut)) ;
						} else {
							return("white");
						}

					})
					.on("mouseover", function(d, i) {
						var k =d.properties.COUNTY.toUpperCase() + "-" +  d.properties.name.toUpperCase();
						if (k in townOuts) {
						 $("#details").html("<center><b>" + d.properties.name + "<br/>" + d.properties.COUNTY + " County</b><br/>" + 
		             "Total customers: " + commasFormatter(townOuts[k].numServed) + "<br/><b><span style='color:" + quantize(townOuts[k].percentOut) + "'>" + 
								 commasFormatter(townOuts[k].numOut) + " (" + commasFormatter(townOuts[k].percentOut) + "%)</span></b> without power</center>") ;
					  } else {
 						 $("#details").html(""); 
					  }
					})
					.attr("d", path);
											
			});	
			
	};	
	
	redraw();
		
});

</script>
</head>
<body>
<center><h1>CMP Outages (%) by Town</h1></center>
<center><div id="container" class="container">
	<div id="maplegend" class="maplegend"></div>
	<div id="map" class="map"></div>
	<div id="details" class="details"></div></center>
</div>
<hr style="clear:both" noshade size="1">
<center>By <a href="http://twitter.com/hrbrmstr">@hrbrmstr</a> | Powered by <a href="http://d3js.org/">d3</a> | Source at <a href="https://gist.github.com/hrbrmstr/7700364">github</a> | <a href="http://twitter.com/cmpco">@CMPCO</a>'s <a href="http://outagemap.cmpco.com/maine/?style=maine#">Interactive ESRI Map</a></center>
</body>
</html>