Last active
April 8, 2024 12:19
-
-
Save hrbrmstr/7700364 to your computer and use it in GitHub Desktop.
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 t…
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
<!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> |
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
#!/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) | |
} | |
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
#!/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 |
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
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; | |
} |
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
#!/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 | |
# ) |
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
#!/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() |
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
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks like it's possible to yank full JSON data from CMP's ESRI map server:
This seems to get county-level data:
http://ib-prd-wme-elb-225371354.us-west-1.elb.amazonaws.com/ArcGIS/rest/services/Maine/MapServer/8/query?f=json&pretty=true&where=1%3D1&returnGeometry=false&spatialRel=esriSpatialRelIntersects&maxAllowableOffset=2042&outFields=*
This seems to town-level data:
http://ib-prd-wme-elb-225371354.us-west-1.elb.amazonaws.com/ArcGIS/rest/services/Maine/MapServer/9/query?f=json&pretty=true&where=17%3D17&returnGeometry=false&spatialRel=esriSpatialRelIntersects&maxAllowableOffset=2042&outFields=*
And, then there are these interesting beasties: