Select Table or Map and drag slider for Year/Quarter. Press Play for animation. Float over map to see regional figures. Raw data is here.
v0.6. Maps modified from Wikimedia Commons under CC BY-SA 3.0 license
Select Table or Map and drag slider for Year/Quarter. Press Play for animation. Float over map to see regional figures. Raw data is here.
v0.6. Maps modified from Wikimedia Commons under CC BY-SA 3.0 license
<!DOCTYPE html> | |
<html><head> | |
<meta content="text/html; charset=UTF-8" http-equiv="content-type"/> | |
<link rel="stylesheet" type="text/css" href="main.css"> | |
</head> | |
<body> | |
<div id="chart"></div> | |
<div id="ctrls"> | |
<div id="readme"/></div> | |
<strong>Display</strong> | |
<form name="buttons" action=""> | |
<input type="radio" id="radio1" name="charttype" onclick="chartType(0)" checked>Table<br> | |
<input type="radio" id="radio2" name="charttype" onclick="chartType(1)">Map<br> | |
</form> | |
<br><br> | |
<input id="slider" type="range" min="0" max="10" oninput="sliderInput()"> | |
<br> | |
<label id="period">period</label> | |
<br><br> | |
<button id="play" type="button" onclick="play()">Play</button> | |
</div> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="//labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script> | |
<script src="//cdn.rawgit.com/showdownjs/showdown/1.3.0/dist/showdown.min.js"></script> | |
<script src="/pinsterdev/raw/a2fcc47fae884342e190/util.js"></script> | |
<script src="https://api.github.com/gists/0f82dc8380acfd77299724369184a01d?callback=gistDataLoaded"></script> | |
<script src="main.js"></script> | |
</body> | |
</html> |
<div id="tabular"> | |
<div id="tableft"> | |
<table> | |
<thead> | |
<tr> | |
<th class="col1" width="90">County/City</th> | |
<th width="55">€/sq.ft.</th> | |
<th width="55">YoY chg</th> | |
</tr> | |
</thead> | |
<tbody/> | |
</table> | |
</div> | |
<div id="tabright"> | |
<table> | |
<thead> | |
<tr> | |
<th class="col1" width="90">Post code</th> | |
<th width="55">€/sq.ft.</th> | |
<th width="55">YoY chg</th> | |
</tr> | |
</thead> | |
<tbody/> | |
</table> | |
<br><br><br> | |
<table> | |
<thead> | |
<tr> | |
<th class="col1" width="90">Region</th> | |
<th width="55">€/sq.ft.</th> | |
<th width="55">YoY chg</th> | |
</tr> | |
</thead> | |
<tbody/> | |
</table> | |
</div> | |
</div> |
/* main layouts */ | |
body { | |
font: 12px sans-serif; | |
width: 1000; | |
} | |
#chart { | |
width: 60%; | |
float: left; | |
} | |
#ctrls { | |
width: 40%; | |
float: left; | |
} | |
#period { | |
font-size: 36px; | |
} | |
#tableft, #tabright { | |
width: 260px; | |
float:left;" | |
} | |
/* data table */ | |
#tabular { | |
width: 540; | |
margin:20px auto; | |
font-size: 12px; | |
} | |
#tabular table { | |
border-collapse: collapse; | |
table-layout:fixed; | |
} | |
#tabular td { | |
border-bottom: 1px solid lightgray; | |
} | |
#tabular th { | |
border-bottom: 2px solid lightgray; | |
} | |
#tabular td, th { | |
text-align: right; | |
} | |
#tabular td.col1, th.col1 { | |
text-align: left; | |
} | |
#tabular td.neg { | |
color: red; | |
} | |
#tabular td.pos { | |
color: green; | |
} | |
#tabular table tr:nth-child(odd) td{ | |
background-color: #f0f0f0; | |
} | |
/* SVG transforms */ | |
svg { | |
width: 640px; | |
height: 640px; | |
} | |
#layer1 { | |
display: inline; | |
transform: scale(1) translate(0px,1200px); | |
} | |
/* irl map defaults */ | |
.region { | |
fill: #008000; | |
stroke: #000000; | |
stroke-width: 2; | |
} | |
.region:hover { | |
fill: green !important; | |
} | |
.ni { | |
fill: #a0a0a0; | |
} | |
/* tooltip */ | |
.d3-tip { | |
line-height: 1; | |
padding: 12px; | |
background: rgba(0, 0, 0, 0.8); | |
color: #fff; | |
border-radius: 2px; | |
} | |
.d3-tip .h { | |
float: left; | |
white-space: nowrap; | |
} | |
.d3-tip .v { | |
float: left; | |
font-weight: bold; | |
} | |
.d3-tip .vm { | |
font-weight: bold; | |
color: #ff8080; | |
} | |
.d3-tip .vp { | |
font-weight: bold; | |
color: lightgreen; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.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; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n:after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
} |
var | |
isFirefox = (typeof InstallTrigger !== 'undefined'), | |
tip = d3.tip().attr('class','d3-tip').offset(tipOffset).html(tipHtml), | |
tipData = null, savedChoice = -1, playing = false, mapSelector = "path.region", | |
elemMap, elemTable, xdata, ydata, mapData, tblData, colNames, rowName, qDub, qOth, | |
selectedPeriod, timer; | |
loadData(); | |
/** | |
* Load a number of resources in parallel. | |
*/ | |
function loadData() { | |
readme(document.getElementById("readme")); // async load readme | |
var loaded = new Object(), remaining = 3; | |
var load = function(d3Function, urlToLoad, resultName) { | |
d3Function(urlToLoad, function(error,d) { | |
if (error) throw error; | |
loaded[resultName] = d; | |
if (!--remaining) { | |
loadChart(loaded); | |
} | |
}); | |
}; | |
load(d3.text, "irlmap.svg", "map"); | |
load(d3.text, "ireland_table.txt", "table"); | |
load(d3.text, "sortedregion.csv", "regions"); | |
} | |
/** | |
* Munge data and views after loading. | |
* @param loaded async loaded data | |
*/ | |
function loadChart(loaded) { | |
// Munge loaded data into usable form | |
mungeData(); | |
// Create SVG map elements and data table | |
(elemMap = document.createElement("div")).innerHTML = loaded.map; | |
(elemTable = document.createElement("div")).innerHTML = loaded.table; | |
// Create empty rows in the table view. | |
var regionRows = d3.csv.parseRows(loaded.regions) | |
var rows = d3.select(elemTable).selectAll("tbody").data(regionRows) | |
.selectAll("tr").data(function(d) { return d;}) | |
.enter().append("tr").attr("id", function(d,i) {return d;}); | |
// 3 columns per row | |
rows.insert("td").classed("col1", true); | |
rows.insert("td"); | |
rows.insert("td"); | |
// munge data for map and set up tooltip | |
mapData = d3.select(elemMap).selectAll(mapSelector)[0].map(function(d,j) { | |
var id = d.getAttribute("id"); | |
return (id in xdata)? xdata[id] : {id: id, missing : true}; | |
}); | |
d3.select(elemMap).select("svg g").call(tip); | |
d3.select(elemMap).selectAll(mapSelector).data(mapData) | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide); | |
// set up slider based on number of periods | |
var last = (typeof parseQueryString().p == 'undefined')? | |
colNames.length - 1 : parseQueryString().p; | |
d3.select("#slider").attr("max", colNames.length - 1).property("value", last); | |
selectedPeriod = colNames[last]; | |
d3.select("#period").text(selectedPeriod); | |
// default to table | |
var ct = parseParamWithDefault('d',['table','map']); | |
document.buttons.charttype[ct].checked = true; | |
chartType(ct); | |
} | |
function sliderInput() { | |
selectedPeriod = colNames[d3.select("#slider").property("value")]; | |
d3.select("#period").text(selectedPeriod); | |
displayChart(); | |
} | |
function play() { | |
if (playing) { | |
playing = false; | |
d3.select("#play").text("Play"); | |
clearInterval(timer); | |
} else { | |
playing = true; | |
d3.select("#play").text("Stop"); | |
timer = setInterval(playStep, 750); | |
} | |
} | |
function playStep() { | |
selectedPeriod = colNames[d3.select("#slider").property("value")]; | |
var slider = d3.select("#slider"); | |
var period = +slider.property("value"); | |
period = (period < (colNames.length - 1))? period + 1 : 0; | |
slider.property("value", period); | |
sliderInput(); | |
} | |
function chartType(choice) { | |
if (choice == savedChoice) { | |
return; | |
} | |
savedChoice = choice; | |
d3.selectAll("#chart>*").remove(); | |
var choices = [elemTable, elemMap] | |
d3.select("#chart").append(function(d) {return choices[choice];}); | |
displayChart(); | |
} | |
function displayChart() { | |
switch (savedChoice) { | |
case 0: displayTable(); break; | |
case 1: displayMap(); break; | |
} | |
} | |
function displayMap() { | |
d3.select(elemMap).selectAll(mapSelector).data(mapData) | |
.attr("style", function(d) { | |
return d.missing? "fill:#a0a0a0;" : d.quantize(d[selectedPeriod]); | |
}); | |
document.querySelector(".d3-tip").innerHTML = tipHtml(tipData); | |
displayLegend(); | |
} | |
function displayLegend() { | |
var numLeg = 4; | |
var svg = d3.select(elemMap).select("svg"); | |
var qclass = ".legendo", legx = 2000, legt = "Ireland"; | |
[qOth, qDub].forEach(function(quant) { | |
var dom = quant.domain(), step = ((dom[1]-dom[0])/numLeg)-0.1; | |
var legData = d3.range(dom[0],dom[1], step); | |
legData.push(legt); | |
legData.reverse(); | |
var legend = svg.selectAll(".legend").data(legData) | |
.enter().append("g").attr("class", qclass).attr("transform", | |
function(d, i) { | |
return "translate(5," + (i * 40 + 400) + ")"; | |
}); | |
var sq = 36; | |
legend.append("rect").attr("x", legx).attr("width", sq).attr( | |
"height", sq).attr("style", function(d,i) {return i>0? quant(d) : "fill:#ffffff;"}); | |
legend.append("text").attr("x", legx - 6).attr("y", 9).attr("dy", | |
"0.5em").style({"text-anchor": "end", "font-size": "36px"}).text(function(d,i) { | |
return i>0?'€'+d3.round(d,0) : d; | |
}); | |
qclass = ".legendd"; | |
legx = 2200; | |
legt = "Dublin"; | |
}); | |
} | |
function displayTable() { | |
d3.select(elemTable).selectAll("tr[id]").each(function(d, i){ | |
var yoy = ydata[d][selectedPeriod]; | |
d3.selectAll(this.childNodes) | |
.text(function(d, i) { | |
switch (i) { | |
case 0: return d; | |
case 1: return xdata[d][selectedPeriod]; | |
case 2: return yoy; | |
} | |
}) | |
.classed("neg",function(d, i) {return i == 2 && yoy.startsWith('-');}) | |
.classed("pos",function(d, i) {return i == 2 && !yoy.startsWith('-');}); | |
}); | |
} | |
/** | |
* Html content for tooltip | |
* @param d data associated with moused-over svg element | |
* @returns html string | |
*/ | |
function tipHtml(d) { | |
if (typeof(d) == 'undefined' || d === null) return; | |
tipData = d; // cache | |
if (d.missing) return d.id; // name only | |
var val = d[selectedPeriod]; | |
var yoy = ydata[d.id][selectedPeriod]; | |
var cls = yoy.charAt(0) != '-' ? "vp" : "vm"; // css for +/- colour | |
var s = "<div><div class='h'>#per#: <br>€/sq.ft: <br>YoY: </div><div " + | |
"class='v'>#id#<br>#val#<br><span class='#cls#'>#yoy#</span></div>"; | |
return s.replace("#per#", selectedPeriod).replace("#id#", d.id) | |
.replace("#val#", val).replace("#cls#", cls).replace("#yoy#", yoy); | |
} | |
/** | |
* Workaround for a bug in Firefox whereby it ignores SVG scaling. | |
* Scaling adjustment is calculated and returned as an offset for | |
* displaying the d3 tooltip. | |
* @returns {Array} of vertical and horizontal offset | |
*/ | |
function tipOffset() { | |
if (!isFirefox) return [0, 0]; | |
var tbbox = this.getBBox(); | |
var matrix = this.getScreenCTM(); | |
var scale = 1.0; // TODO: hardcoded, must match CSS | |
var pt = { | |
// only works for default 'north' tooltip direction | |
x: tbbox.x + (tbbox.width / 2) + matrix.e, | |
y: tbbox.y + matrix.f | |
}; | |
return [pt.y * (scale - 1), pt.x * (scale - 1)]; | |
} | |
function mungeData() { | |
var sheet = extractGistData(gistData["MyhomeData.csv"].content); | |
colNames = sheet.colNames.filter(function (d) { | |
return d.indexOf(" Q") > -1; | |
}); | |
rowName = sheet.rowName; | |
xdata = new Object(); | |
ydata = new Object(); | |
var yoy = d3.format("+.0f"); // formatter for yoy | |
var allValuesDub = [], allValuesOth = []; | |
sheet.data.forEach(function(d) { | |
var arr = d[rowName].startsWith("dublin-")? allValuesDub : allValuesOth; | |
for (i = 0; i < colNames.length; i++) { | |
arr.push(+d[colNames[i]]); | |
} | |
}); | |
qDub = getQuantizeFunc(d3.min(allValuesDub),d3.max(allValuesDub), false); | |
qOth = getQuantizeFunc(d3.min(allValuesOth),d3.max(allValuesOth), true); | |
sheet.data.forEach(function(d) { | |
var id = d[rowName]; | |
xdata[id]= {id : id, quantize : id.startsWith("dublin-")? qDub : qOth, missing: false}; | |
ydata[id]= {id : id}; | |
for (i = 0; i < colNames.length; i++) { | |
var c = colNames[i]; | |
xdata[id][c] = d[c]; | |
// calculate YoY for years after first | |
ydata[id][c] = (i < 4) ? "" : yoy(d[c] / d[colNames[i - 4]] * 100 - 100) + "%"; | |
} | |
}); | |
} | |
/** | |
* Quantize given range into white-red or white-blue color gradient | |
* @param min range minimum | |
* @param max range maximum | |
* @param redblue true/false for blue or red gradient | |
* @returns color gradient quantize function | |
*/ | |
function getQuantizeFunc(min, max, redblue) { | |
var hex1 = d3.format("0x"); | |
var hex = function(n) { | |
var s = hex1(255-n); | |
return s.length == 1? "0" + s : s; | |
}; | |
return d3.scale.quantize() | |
.domain([min, max]) | |
.range(d3.range(255).map(function(i) { | |
return redblue? "fill:#ff" + hex(i) + hex(i)+";" : | |
"fill:#" + hex(i) + hex(i)+"ff;"; | |
})); | |
} | |
carlow,cavan,clare,cork,cork-city,cork-west,donegal,dublin,galway,galway-city,kerry,kildare,kilkenny,laois,leitrim,limerick,limerick-city,longford,louth,mayo,meath,monaghan,offaly,roscommon,sligo,tipperary,waterford,westmeath,wexford,wicklow | |
dublin-1,dublin-2,dublin-3,dublin-4,dublin-5,dublin-6,dublin-6W,dublin-7,dublin-8,dublin-9,dublin-10,dublin-11,dublin-12,dublin-13,dublin-14,dublin-15,dublin-16,dublin-17,dublin-18,dublin-20,dublin-22,dublin-24 | |
dublin-county,dublin-north,dublin-south,dublin-west |
What's up with that grey bit on the auld Ireland map?
That's the bad weather area of the country. Also not covered by myhome.ie.
Looking good so far. I mightn't be able to contribute anything of value but I'll enjoy watching as it comes together.
Project Diary
Requirement
Planning to do an Ireland chloropleth with historical asking prices. Slider controller allows scrolling through historical data by quarter, and Play button does it automatically.
Milestones