|
<!DOCTYPE html> |
|
<html> |
|
<meta charset="utf-8"> |
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> |
|
<style> |
|
text { |
|
font-family: sans-serif; |
|
font-size: 12px; |
|
} |
|
.dot { |
|
stroke: black; |
|
} |
|
.voronoiWrapper { |
|
-webkit-cursor: crosshair; |
|
cursor: crosshair; |
|
} |
|
.popover { |
|
pointer-events: none; |
|
} |
|
.tooltip--value { |
|
font-family: "Menlo", monospace; |
|
font-size: 10px; |
|
text-transform: uppercase; |
|
letter-spacing: 1px; |
|
} |
|
</style> |
|
<body> |
|
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script> |
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
|
|
<script> |
|
var margin = { top: 20, right: 20, bottom: 20, left: 40 }, |
|
width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom; |
|
|
|
var x = d3.scaleLinear().range([0, width]); |
|
var y = d3.scaleLinear().range([height, 0]); |
|
|
|
var color = d3.scaleOrdinal(d3.schemeCategory10); |
|
|
|
var xAxis = d3.axisBottom(x).tickFormat(function(d) { |
|
return d + "€"; |
|
}); |
|
|
|
var yAxis = d3.axisLeft(y); |
|
|
|
var svg = d3 |
|
.select("body") |
|
.append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
d3.tsv("data.tsv", function(error, data) { |
|
if (error) throw error; |
|
|
|
data.forEach(function(d) { |
|
d.square_meter = +d.square_meter; |
|
d.avg_rent = +d.avg_rent; |
|
d.population = +d.population; |
|
}); |
|
|
|
x |
|
.domain( |
|
d3.extent(data, function(d) { |
|
return d.square_meter; |
|
}) |
|
) |
|
.nice(); |
|
y |
|
.domain( |
|
d3.extent(data, function(d) { |
|
return d.avg_rent; |
|
}) |
|
) |
|
.nice(); |
|
|
|
svg |
|
.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis) |
|
.append("text") |
|
.attr("class", "label") |
|
.attr("x", width) |
|
.attr("y", -6) |
|
.style("text-anchor", "end") |
|
.text("Average second hand m²"); |
|
|
|
svg |
|
.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis) |
|
.append("text") |
|
.attr("class", "label") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 6) |
|
.attr("dy", ".7em") |
|
.style("text-anchor", "end") |
|
.text("Average family rent"); |
|
|
|
// pack the circles on their own group |
|
var dotsGroup = svg.append("g").attr("class", "dotsWrapper"); |
|
|
|
dotsGroup |
|
.selectAll(".dot") |
|
.data(data) |
|
.enter() |
|
.append("circle") |
|
.attr("class", function(d, i) { |
|
return "dot neighbourhood-" + i; |
|
}) |
|
.attr("r", 5) |
|
.attr("cx", function(d) { |
|
return x(d.square_meter); |
|
}) |
|
.attr("cy", height) |
|
.style("opacity", 0) |
|
.style("fill", function(d) { |
|
return color(d.category); |
|
}); |
|
|
|
// setup the transition to make the circles appear |
|
dotsGroup |
|
.selectAll(".dot") |
|
.transition() |
|
.duration(1000) |
|
.delay(function(d, i) { |
|
return i * 20; |
|
}) |
|
.style("opacity", 1) |
|
.attr("cx", function(d) { |
|
return x(d.square_meter); |
|
}) |
|
.attr("cy", function(d) { |
|
return y(d.avg_rent); |
|
}); |
|
|
|
// start the voronoi |
|
var voronoi = d3 |
|
.voronoi() |
|
.x(function(d) { |
|
return x(d.square_meter); |
|
}) |
|
.y(function(d) { |
|
return y(d.avg_rent); |
|
}) |
|
.extent([[0, 0], [width, height]]); |
|
|
|
var voronoiGroup = svg.append("g").attr("class", "voronoiWrapper"); |
|
|
|
voronoiGroup |
|
.selectAll("path") |
|
.data(voronoi.polygons(data)) |
|
.enter() |
|
.append("path") |
|
.attr("d", function(d, i) { |
|
return "M" + d.join("L") + "Z"; |
|
}) |
|
.attr("class", function(d, i) { |
|
return "voronoi neighbourhood-" + i; |
|
}) |
|
.style("fill", "none") |
|
.style("pointer-events", "all") |
|
.on("mouseover", showTooltip) |
|
.on("mouseout", removeTooltip); |
|
|
|
function showTooltip(d, i) { |
|
var element = d3.selectAll(".neighbourhood-" + i); |
|
var elementNode = d3.selectAll(".neighbourhood-" + i).node(); |
|
|
|
$(elementNode).popover({ |
|
placement: "auto top", |
|
container: "body", |
|
trigger: "manual", |
|
html: true, |
|
content: function() { |
|
return ( |
|
"<strong>" + |
|
d.data.neighbourhood + |
|
"</strong>" + |
|
"<br /><span class='tooltip--value'>Avg. m²: " + |
|
d.data.square_meter + |
|
"€</span>" + |
|
"<br /><span class='tooltip--value'>Avg. rent: " + |
|
d.data.avg_rent + |
|
"</span>" |
|
); |
|
} |
|
}); |
|
|
|
$(elementNode).popover("show"); |
|
|
|
// vertical line |
|
svg |
|
.append("g") |
|
.attr("class", "guide") |
|
.append("line") |
|
.attr("x1", element.attr("cx")) |
|
.attr("x2", element.attr("cx")) |
|
.attr("y1", +element.attr("cy")) |
|
.attr("y2", height) |
|
.style("stroke", element.style("fill")) |
|
.style("stroke-dasharray", "5,5") |
|
.style("opacity", 0) |
|
.style("pointer-events", "none") |
|
.transition() |
|
.duration(100) |
|
.style("opacity", 0.5); |
|
|
|
// horizontal line |
|
svg |
|
.append("g") |
|
.attr("class", "guide") |
|
.append("line") |
|
.attr("x1", +element.attr("cx")) |
|
.attr("x2", 0) |
|
.attr("y1", element.attr("cy")) |
|
.attr("y2", element.attr("cy")) |
|
.style("stroke", element.style("fill")) |
|
.style("stroke-dasharray", "5,5") |
|
.style("opacity", 0) |
|
.style("pointer-events", "none") |
|
.transition() |
|
.duration(200) |
|
.style("opacity", 0.5); |
|
} |
|
|
|
function removeTooltip() { |
|
$(".popover").each(function() { |
|
$(this).remove(); |
|
}); |
|
|
|
d3 |
|
.selectAll(".guide") |
|
.transition() |
|
.duration(100) |
|
.style("opacity", 0) |
|
.remove(); |
|
} |
|
}); |
|
</script> |