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"> </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"> </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>