|
|
|
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;"; |
|
})); |
|
} |
|
|
|
|
Looking good so far. I mightn't be able to contribute anything of value but I'll enjoy watching as it comes together.