Cartograma de área continua (artículo, código) que muestra la sobrerrepresentación del voto a partidos nacionales en las 52 circuscripciones electorales españolas (años 2015, 2016 y 2019).
El área de una provincia es inversamente proporcional al coste en votos de un escaño en esa provincia. La intensidad del color viene determinada por la cantidad de escaños obtenidos por el partido seleccionado sobre el número total de escaños obtenidos por partidos nacionales en esa provincia. Los escaños obtenidos por partidos minoritarios no fueron tenidos en cuenta en este análisis.
Continuous area cartogram showing overrepresentation in the Spanish elections 2015-2019. Areas are inversely proportional to the cost in votes of each representative in the 52 circunscriptions. Showing only representatives obtained by national parties (nationalist and regional parties where excluded).
GeoJSON de España gracias a Martín González: es-atlas[https://github.com/martgnz/es-atlas]. Composite projection gracias a Roger Veciana: d3-composite-projections[https://github.com/rveciana/d3-composite-projections].
Built with blockbuilder.org
Made by Alex B. Sígueme en Twitter: ale0xb
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/topogram"></script>
<script src="https://unpkg.com/topojson"></script>
<script src="https://unpkg.com/d3-composite-projections"></script>
<script src="https://unpkg.com/d3-tip@0.9.1/dist/index.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<style>
body {
margin:0;
position:fixed;
top:0;
right:0;
bottom:0;
left:0;
width:960;
height:500;}
.provinces {
fill: #ccc;
stroke: #fff;
}
div.tooltip {
position: absolute;
text-align: center;
width: 70px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
.d3-tip {
line-height: 1;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
pointer-events: none;
}
.d3-tip span {
font-weight: bold;
}
/* 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);
position: absolute;
pointer-events: none;
}
/* Northward tooltips */
.d3-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}
/* Eastward tooltips */
.d3-tip.e:after {
content: "\25C0";
margin: -4px 0 0 0;
top: 50%;
left: -8px;
}
/* Southward tooltips */
.d3-tip.s:after {
content: "\25B2";
margin: 0 0 1px 0;
top: -8px;
left: 0;
text-align: center;
}
/* Westward tooltips */
.d3-tip.w:after {
content: "\25B6";
margin: -4px 0 0 -1px;
top: 50%;
left: 100%;
}
#selectors {
position: absolute;
top: 10px;
left: 10px;
}
.selector {
padding: 20px;
margin: 10px;
}
#info {
position: absolute;
top: 70px;
left: 20px;
width: 200px;
}
#info span {
font-weight: bold;
}
</style>
</head>
<body>
<div id=selectors>
<select class=selector id=year-selector>
</select>
<select class=selector id=party-selector>
</select>
</div>
<div id=info>
</div>
<script>
// Feel free to change or delete any of the code you see in this editor!
const width = 960,
height = 500
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
const proj = d3.geoConicConformalSpain()
const path = d3.geoPath().projection(proj)
const party_colors = {
'PSOE': '#b00003',
'PP': '#064294',
'CIUDADANOS': '#f53706',
'UP/PODEMOS': '#6a195c',
'VOX': '#4db729'
}
d3.queue()
.defer(d3.json, "provinces.json")
.defer(d3.json, "elecciones-3.json")
.await(function(error, provinces, elecciones) {
const data = elecciones['elecciones']
const norm_data = []
for (const [year, yearData] of Object.entries(data)) {
for (const [party, provincesData] of Object.entries(yearData)) {
for (const [province, provinceData] of Object.entries(provincesData)) {
provinceData.s = Math.round(provinceData.s * Math.pow(10,2)) / Math.pow(10,2)
norm_data.push({...provinceData,
'year': year,
'party': party,
'province': province})
}
}
}
const final_esc_year = {}
d3.nest()
.key(d => d.year)
.key(d =>d.province)
.rollup(d => d.map(c=>c.s).reduce((acc, current) => acc + current))
.entries(norm_data)
.forEach(d => {
const subObj = {}
let s_sum = 0
d.values.forEach(c => {
subObj[c.key] = c.value
s_sum += c.value
})
subObj['todos'] = s_sum
final_esc_year[d.key] = subObj
})
const nested_years_data = d3.nest().key(d => d.year).key(d => d.party).entries(norm_data)
let sel_year = null,
sel_party = null,
sel_data = null
const carto = topogram
.cartogram()
.projection(proj)
.properties(function(d) {
return d.properties
})
var land = topojson.feature(provinces, provinces.objects.provinces)
const tip = d3.tip()
.attr('class', 'd3-tip')
.html(function(d) {
return `<span>${d.province} (${sel_party})</span><br>
<span>escaños</span>: ${d['esc']}/${final_esc_year[sel_year][d.province]}<br>
<span>coste medio</span>: ${d['votesc']} votos`
})
svg.call(tip)
svg.selectAll("path").data(land.features)
.enter()
.append("path")
.attr("d", path)
// .attr("transform", 'translate() scale(30)')
.attr("fill", "black")
.attr("stroke", "black")
.style("stroke-opacity", 0.2)
.on('mouseover', function(d){
// d3.select(this).style("opacity", 1.0)
d3.select(this).style("stroke-opacity", 0.8)
const cross = sel_data.filter(c => c.province == d.properties.name)[0]
let tipData = {'province': d.properties.name}
if(typeof cross !== 'undefined') {
tipData = Object.assign(tipData, {'esc': cross.s, 'votesc': cross.c})
} else tipData = Object.assign(tipData, {'esc': 0, 'votesc': 0})
const direction = d3.mouse(this)[1] > height *0.25 ? "n" : "s"
tip.direction(direction)
tip.show(tipData, this)
})
.on('mouseout', function(d){
d3.select(this).style("stroke-opacity", 0.2)
tip.hide()
})
svg.append("path")
.attr("class","border")
.style("fill","none")
.style("stroke","#a0a0a0")
.attr("d", proj.getCompositionBorders())
setTimeout(function() {
d3.select('#party-selector')
.on('change', partyChange)
d3.select('#year-selector')
.on('change', yearChange)
const all_years = nested_years_data.map(d => d.key).reverse()
d3.select('#year-selector')
.selectAll('option')
.data(all_years)
.enter()
.append('option')
.text(d => d)
document.getElementById("year-selector").options[0].selected = true
yearChange()
}, 1000)
const opacityScale = d3.scaleLinear().domain([0,1]).range([0.1, 0.9])
function update() {
sel_data = nested_years_data.filter(d => d.key == sel_year)[0].values.filter(d => d.key == sel_party)[0].values
const sizeScale = d3.scaleLinear()
.domain([d3.max(sel_data, d => d.c), 0])
.range([100, 1000])
carto.value(function(d) {
const cross = sel_data.filter(c => c.province == d.properties.name)[0]
if (typeof cross != 'undefined'){
return sizeScale(cross.c)
} else {
return 25
}
})
const this_features = carto(provinces, provinces.objects.provinces.geometries).features;
svg.selectAll("path")
.data(this_features)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr('fill', d => {
const cross = sel_data.filter(c => c.province == d.properties.name)[0]
let percent = 0.05
if (typeof cross !== 'undefined') {
percent = opacityScale(cross.s / final_esc_year[sel_year][d.properties.name])
}
return hexToRGB(party_colors[sel_party], percent)
})
.style("stroke-opacity", 0.2)
.attr('d', carto.path)
}
function yearChange() {
sel_year = d3.select('#year-selector').property('value')
d3.select('#party-selector')
.selectAll("*")
.remove();
d3.select('#party-selector')
.selectAll('option')
.data(nested_years_data
.filter(d => d.key == sel_year)[0].values.map(d => d.key))
.enter()
.append('option').text(d => d);
const partySelector = document.getElementById("party-selector")
if (!sel_party) {
partySelector.options[0].selected = true
} else {
let sel_idx = -1
for (var i = 0; i < partySelector.options.length; i++) {
if (partySelector.options[i].label == sel_party) {
sel_idx = i
break
}
}
if (sel_idx >= 0)
partySelector.options[sel_idx].selected = true
else
partySelector.options[0].selected = true
}
partyChange()
}
function partyChange() {
sel_party = d3.select('#party-selector').property('value')
const partyPerf = nested_years_data
.filter(d => d.key == sel_year)[0].values.filter(d=>d.key == sel_party)[0]
let costAvg = partyPerf.values.map(d => d.c).reduce((acc, current) => {
return acc + current
}) / partyPerf.values.length
costAvg = Math.round(costAvg * Math.pow(10,2)) / Math.pow(10,2)
const totalEsc = partyPerf.values.map(d => d.s).reduce((acc, current) => {
return acc + current
})
const allEsc = final_esc_year[sel_year]['todos']
const percent = 100* Math.round(totalEsc/allEsc * Math.pow(10,2)) / Math.pow(10,2)
d3.select('#info').selectAll("*").remove()
d3.select('#info').html(`En <span>${sel_year}</span>, <span>${sel_party}</span> obtuvo <span>${totalEsc} escaños</span> de los <span>${allEsc} (${percent}%)</span> asignados a partidos nacionales, con un <span>coste medio</span> por escaño de <span>${costAvg} votos</span>.`)
update()
}
});
function hexToRGB(hex, alpha) {
var r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
if (alpha) {
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
} else {
return "rgb(" + r + ", " + g + ", " + b + ")";
}
}
</script>
</body>
https://unpkg.com/topogram
https://unpkg.com/topojson
https://unpkg.com/d3-composite-projections
https://unpkg.com/d3-tip@0.9.1/dist/index.js
https://d3js.org/d3.v4.js