How do Europeans rate the quality of life in their city? To answer this question, the EU regularly conducts an extensive survey of almost 80 cities of varying sizes (European Union, 2016). This interactive visualisation shows the level of satisfaction according to seven indicators.
You can click on the legend text to sort the cities by this subject.
Website with published interactive: Cities in Europe
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
font-family: Verdana, Arial, sans-serif;
font-variant: small-caps;
font-size: 12px;
font-weight: bold;
background-color: #9ccef3;
}
text .cityname.viz {
letter-spacing: 1.2px;
font-weight: bold;
fill: white;
}
path.i.unselected {
fill: #69B9EC;
}
li.unselected {
color: #009DE0;
}
#titles span {
cursor: pointer;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
not supported by any browser */
}
#titles span.indi:hover {
text-decoration: underline;
}
.Q1_1 {
fill: #006e9a;
color: #006e9a;
}
.Q1_11 {
fill: #8D9120;
color: #8D9120;
}
.Q2_4 {
fill: #9D0064;
color: #9D0064;
}
.Q2_5 {
fill: #E6AD1F;
color: #E6AD1F;
}
.Q2_6 {
fill: #459240;
color: #459240;
}
.Q2_7 {
fill: #C42117;
color: #C42117;
}
.Q2_2 {
fill: #D16A19;
color: #D16A19;
}
.tooltip {
text-decoration: none;
position: relative;
}
.tooltip span {
display: none;
}
.tooltip:hover span {
display: block;
position: fixed;
overflow: hidden;
}
</style>
<div id="titles"></div>
<div id="viz"></div>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<script>
var w = 960, h = 493;
var padding = 20;
var width = w, height = h - padding;
var cities, coln, rown, cheight, cwidth;
var svg = d3.select("#viz").append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "root")
.append("g")
.attr("transform", "translate(0," + padding / 2 + ")");
function getPathData(radius) {
// adjust the radius a little so our text's baseline isn't sitting directly
// on the circle
var r = radius;
var startX = 0 / 2 - r;
return 'm' + startX + ',' + (0 / 2) + ' ' + 'a' + r + ',' + r + ' 0 0 1 ' + (2 * r) + ',0';
}
queue()
.defer(d3.json, "data.json")
.defer(d3.tsv, "names.tsv")
.await(ready);
function ready(error, _data, _names) {
var titles = d3.select("#titles").selectAll("span")
.data(_data.indicators)
.enter().append("span")
.attr("class", function(d) {return "viz title " + d.indicator})
.text(function(d, i) {
sep = (i > 0) ? ' | ' : '';
return sep
})
.append("span").attr("class", function(d) {return "viz title indi " + d.indicator})
.text(function(d, i) {return d.desc})
.on("click", function(d, i) {
if (d3.selectAll(".viz.title." + d.indicator).classed("selected") == false) {
sortCharts(i);
d3.selectAll(".i").classed("unselected", true)
d3.selectAll(".i" + i).classed("unselected", false)
d3.selectAll(".i").classed("selected", false)
d3.selectAll(".i" + i).classed("selected", true)
d3.selectAll(".viz.title").classed("selected", false)
d3.selectAll(".viz.title." + d.indicator).classed("selected", true)
d3.selectAll(".viz.title").classed("unselected", true)
d3.selectAll(".viz.title." + d.indicator).classed("unselected", false)
d3.selectAll(".viz.title").classed("selection", true)
}
else {
sortCharts("alpha");
d3.selectAll(".i").classed("unselected", false)
d3.selectAll(".i").classed("selected", false)
d3.selectAll(".viz.title").classed("selected", false)
d3.selectAll(".viz.title").classed("unselected", false)
d3.selectAll(".viz.title").classed("selection", false)
}
})
.on("mouseover", function(d, i) {
if (d3.selectAll(".viz.title." + d.indicator).classed("selection") == true) {
d3.selectAll(".i" + i).classed("unselected", false)
d3.selectAll(".viz.title." + d.indicator + ".unselected").classed("unselected", false)
}
})
.on("mouseout", function(d, i) {
if (d3.selectAll(".viz.title." + d.indicator).classed("selected") == false && d3.selectAll(".viz.title." + d.indicator).classed("selection") == true) {
d3.selectAll(".viz.title." + d.indicator).classed("unselected", true)
d3.selectAll(".i" + i).classed("unselected", true)
}
});
var namesByName = {};
_names.forEach(function(d) {
namesByName[d.name1] = d;
});
var legendByIndicator = {};
_data.indicators.forEach(function(d) {
legendByIndicator[d.indicator] = d.desc;
});
_data.cities.forEach(function(d, i) {
_data.cities[i]["country"] = namesByName[_data.cities[i].name].country
_data.cities[i]["show"] = namesByName[_data.cities[i].name].show
});
var datalen = _data.cities.filter(function(d) {
return d.show == 1
}).length
cwidth = getSize(width, height, datalen);
cheight = cwidth;
var chartr = cwidth / 2 - 5;
var barwidth = cwidth / 10;
coln = (width - (width % cwidth)) / cwidth;
rown = Math.ceil(datalen / coln);
var charts = svg.append("g").attr("transform", "translate(" + (cwidth / 2) + "," + (cheight / 2) + ")");
var indicatorCount = _data.cities[0].values.length;
cities = charts.selectAll("cities")
.data(_data.cities
.sort(function(a, b) {return d3.ascending(a.name, b.name)})
.filter(function(d) {return d.show == 1}))
.enter().append("g")
.attr("class", "city")
.attr("id", function(d) {return d.name})
.attr("transform", function(d, i) {
xtrans = ((i / coln) - Math.floor(i / coln)) * coln * cwidth;
ytrans = Math.floor(i / coln) * cheight;
return "translate(" + xtrans + "," + ytrans + ")"
});
var grids = [0, 0.25, 0.5, 0.75, 1];
var circles = cities.selectAll("gridlines")
.data(grids)
.enter().append("circle")
.attr("r", function(d) {return d * (chartr - barwidth) + barwidth})
.attr("class", "gridline")
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", 0.5);
var position = {
0 : 1,
1 : 0,
2 : 6,
3 : 3,
4 : 2,
5 : 5,
6 : 4
};
var bars = cities.selectAll("bars")
.data(function(d) {return d.values})
.enter().append("path")
.attr("class", function(d, i) {return "i i" + i + " " + d.indicator})
.attr("d", function(d) {
t = d.nd + d.ns + d.rs + d.ru + d.vs
s = (d.rs + d.vs) / t
return describeRadialBar(0, 0, barwidth, 360 / indicatorCount, s * (chartr - barwidth))
})
.attr("transform", function(d, i) {return "rotate(" + (180 + position[i] * 360 / indicatorCount) + ")"});
var citynamepath = svg.append("defs").selectAll("path")
.data(_data.cities.filter(function(d) {return d.show == 1}))
.enter().append("path")
.attr({
d : getPathData((chartr - barwidth) + barwidth),
id : function(d) {
return "textp" + d.name.substring(0, 4)
}
});
var cityname = cities.append("text").append('textPath')
.attr({
'xlink:href' : function(d) {
return "#textp" + d.name.substring(0, 4)
},
startOffset : "50%"
})
.attr("text-anchor", "middle").attr("class", "cityname viz").text(function(d) {return namesByName[d.name].name2});
};
var sortCharts = function(parameter) {
if (parameter == "alpha") {
cities.sort(function(a, b) {
return d3.ascending(a.name, b.name)
})
}
else {
cities.sort(function(a, b) {
ta = a.values[parameter].nd + a.values[parameter].ns + a.values[parameter].rs + a.values[parameter].ru + a.values[parameter].vs
sa = (a.values[parameter].rs + a.values[parameter].vs) / ta
tb = b.values[parameter].nd + b.values[parameter].ns + b.values[parameter].rs + b.values[parameter].ru + b.values[parameter].vs
sb = (b.values[parameter].rs + b.values[parameter].vs) / tb
return sb - sa
});
}
cities.transition().duration(1000).attr("transform", function(d, i) {
xtrans = ((i / coln) - Math.floor(i / coln)) * coln * cwidth;
ytrans = Math.floor(i / coln) * cheight;
return "translate(" + xtrans + "," + ytrans + ")"
});
}
var getSize = function(x, y, n) {
console.log(n)
var px = Math.ceil(Math.sqrt(n * x / y));
var sx, sy;
if (Math.floor(px * y / x) * px < n) {
sx = y / Math.ceil(px * y / x);
}
else {
sx = x / px;
}
var py = Math.ceil(Math.sqrt(n * y / x));
if (Math.floor(py * x / y) * py < n) {
sy = x / Math.ceil(x * py / y);
}
else {
sy = y / py;
}
return sx > sy ? sx : sy;
}
function describeRadialBar(x, y, radius, angle, value) {
var start = polarToCartesian(x, y, radius, -angle / 2);
var end = polarToCartesian(x, y, radius, angle / 2);
d = ["M", start.x, start.y, "A", radius, radius, 0, 0, 1, end.x, end.y, "v", -value, "a", radius + value, radius + value, 0, 0, 0, -2 * end.x, 0, "v", value, "z"].join(" ");
return d;
}
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
return {
x : centerX + (radius * Math.cos(angleInRadians)),
y : centerY + (radius * Math.sin(angleInRadians))
};
}
</script>
Modified http://d3js.org/queue.v1.min.js to a secure url
https://d3js.org/d3.v3.min.js
https://d3js.org/queue.v1.min.js