Skip to content

Instantly share code, notes, and snippets.

@pinsterdev pinsterdev/README.md
Last active Mar 22, 2018

Embed
What would you like to do?
Houses: asking price per sq.ft by region
<!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>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<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>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
/* 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;";
}));
}
We can make this file beautiful and searchable if this error is corrected: It looks like row 2 should actually have 30 columns, instead of 22. in line 1.
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
@pinsterdev

This comment has been minimized.

Copy link
Owner Author

commented Feb 12, 2016

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

  1. Acquire map data: Mike Bostock has a fab article on how to make a map of anywhere in the world from open source map data and use it with D3. I was lazier than that, and just did a Google image search for "Ireland map SVG". Wikimedia Commons threw up some nice ones that could be easily cleaned up for the job at hand. See SVG images in the Gist above.
  2. Pah. Specs are boring. Bored smiley Why don't I just shut up and code. coder smiley
  3. Firefox has a nasty bug which causes it to miscalculate coordinate mappings for scaled SVG images. The upshot is that D3-tips tooltips appear in the wrong place. Trying to figure out a workaround.
  4. Finally figured out how to get data from Google Sheets using JSONP instead of an embedded CSV file in the Gist.
@Coles

This comment has been minimized.

Copy link

commented Feb 12, 2016

What's up with that grey bit on the auld Ireland map?

@pinsterdev

This comment has been minimized.

Copy link
Owner Author

commented Feb 14, 2016

That's the bad weather area of the country. Also not covered by myhome.ie.

@Coles

This comment has been minimized.

Copy link

commented Feb 14, 2016

Looking good so far. I mightn't be able to contribute anything of value but I'll enjoy watching as it comes together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.