Last active
June 6, 2018 18:00
-
-
Save rubenmoya/a20f488f87335258080f82c4f266850e to your computer and use it in GitHub Desktop.
Hackarto.vl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Spanish Congress Elections · Hackarto.VL</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta charset="UTF-8"> | |
<!-- Include CARTO VL JS --> | |
<script src="https://libs.cartocdn.com/carto-vl/v0.5.0-beta/carto-vl.js"></script> | |
<!-- Include Mapbox GL JS --> | |
<script src="https://libs.cartocdn.com/mapbox-gl/v0.45.0-carto1/mapbox-gl.js"></script> | |
<!-- Include Mapbox GL CSS --> | |
<link href="https://libs.cartocdn.com/mapbox-gl/v0.45.0-carto1/mapbox-gl.css" rel="stylesheet" /> | |
<link href="https://carto.com/developers/carto-vl/examples/maps/style.css" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600" rel="stylesheet"> | |
<style> | |
* { | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
body { | |
font-family: 'Open Sans', sans-serif; | |
background: #d0d2d8; | |
color: #2C2C2C; | |
font-size: 16px; | |
text-rendering: optimizeLegibility; | |
} | |
ul { | |
margin: 0; | |
padding: 0; | |
list-style-type: none; | |
} | |
li { | |
display: inline-block; | |
padding-right: 10px; | |
} | |
.sidebar { | |
position: absolute; | |
max-width: 250px; | |
top: 16px; | |
left: 16px; | |
z-index: 10; | |
width: 100%; | |
} | |
.panel { | |
background: white; | |
max-width: 600px; | |
width: 100%; | |
padding: 2rem; | |
position: absolute; | |
bottom: 2rem; | |
left: 50%; | |
transform: translate3d(-50%, 0, 0); | |
z-index: 10; | |
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.1); | |
border-radius: 2px; | |
} | |
.card { | |
width: 100%; | |
background: white; | |
overflow: hidden; | |
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.1); | |
border-radius: 2px; | |
} | |
.card--header { | |
padding: 12px; | |
border-bottom: 1px solid rgba(0, 0, 0, 0.2); | |
font-size: 14px; | |
} | |
.card--hidden { | |
display: none; | |
} | |
.card:not(:last-child) { | |
margin-bottom: 20px; | |
} | |
.title { | |
margin: 0; | |
} | |
.party { | |
position: relative; | |
padding-left: 16px; | |
font-size: 14px; | |
} | |
.party--color { | |
border: 1px solid #000; | |
background-color: 1px solid #000; | |
border-radius: 50%; | |
position: absolute; | |
width: 10px; | |
height: 10px; | |
content: ' '; | |
transform: translate3d(0, -50%, 0); | |
top: 50%; | |
left: 0; | |
} | |
.party--name { | |
color: #000; | |
padding-right: 5px; | |
} | |
.party--votes { | |
color: #979EA1; | |
} | |
.results { | |
max-height: 250px; | |
overflow: scroll; | |
padding: 10px; | |
} | |
.selector { | |
display: flex; | |
justify-content: space-between; | |
} | |
.selector--year { | |
width: 16px; | |
height: 16px; | |
background-color: #FFF; | |
box-shadow: 0px 0 0 2px #6858A9; | |
display: inline-block; | |
border-radius: 50%; | |
font-weight: 600; | |
text-align: center; | |
line-height: 35px; | |
border: 0; | |
outline: none; | |
cursor: pointer; | |
margin-bottom: 20px; | |
} | |
.selector--year:nth-child(n+2) { | |
margin: 0 0 0 16px; | |
} | |
.selector--year:nth-child(n+2):before { | |
width: 19px; | |
height: 2px; | |
display: block; | |
background-color: #6858A9; | |
transform: translate(-30px, 6px); | |
content: ''; | |
position: absolute; | |
pointer-events: none; | |
} | |
.selector--year:after { | |
width: 32px; | |
display: block; | |
transform: translate(-16px, 14px); | |
color: #585858; | |
content: attr(data-year); | |
font-size: 12px; | |
} | |
.selector--year:after:first-child { | |
transform: translate(-16px, 142px); | |
} | |
.selector--year.active { | |
background: #6858A9; | |
} | |
#provinces { | |
max-height: 200px; | |
overflow-y: scroll; | |
} | |
#provinces div.selected i::after { | |
content: 'x'; | |
cursor: pointer; | |
} | |
#provinces span { | |
cursor: pointer; | |
} | |
#provinces div.selected span { | |
font-weight: bolder; | |
} | |
.province { | |
height: 30px; | |
line-height: 10px; | |
padding: 10px 10px; | |
border-bottom: 1px solid rgba(0, 0, 0, 0.05); | |
} | |
.province.selected { | |
background-color: rgba(0, 0, 0, 0.1); | |
} | |
.province:hover { | |
background-color: rgba(0, 0, 0, 0.05); | |
cursor: pointer; | |
} | |
.clear-filter, | |
.animation-button { | |
background: #6858A9; | |
color: #fff; | |
width: 100%; | |
transition: background, .3s; | |
position: relative; | |
padding: 8px 20px; | |
border: 1px solid transparent; | |
box-sizing: border-box; | |
cursor: pointer; | |
} | |
.animation-button { | |
margin-top: 1rem; | |
} | |
</style> | |
</head> | |
<body> | |
<section class="sidebar"> | |
<div class="card"> | |
<div class="card--header"> | |
<h1 class="title">Select provinces</h1> | |
</div> | |
<div class="card--content"> | |
<div id="provinces"></div> | |
</div> | |
<div class="card--footer"> | |
<button class="clear-filter js-clear-filter"> | |
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium">Clear Filter</span> | |
</button> | |
</div> | |
</div> | |
<div class="card card--results card--hidden"> | |
<div class="card--header"> | |
<h1 class="title">Results for | |
<span class="js-municipio"></span> | |
</h1> | |
</div> | |
<div class="card--content"> | |
<div class="js-results"></div> | |
</div> | |
</div> | |
</section> | |
<section class="panel"> | |
<section class="selector"> | |
<button class="selector--year active" data-year="1977"></button> | |
<button class="selector--year" data-year="1979"></button> | |
<button class="selector--year" data-year="1982"></button> | |
<button class="selector--year" data-year="1986"></button> | |
<button class="selector--year" data-year="1989"></button> | |
<button class="selector--year" data-year="1993"></button> | |
<button class="selector--year" data-year="1996"></button> | |
<button class="selector--year" data-year="2000"></button> | |
<button class="selector--year" data-year="2004"></button> | |
<button class="selector--year" data-year="2008"></button> | |
<button class="selector--year" data-year="2011"></button> | |
<button class="selector--year" data-year="2015"></button> | |
<button class="selector--year" data-year="2016"></button> | |
</section> | |
<button class="animation-button" onclick="toggleAnimation()"> | |
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium">Play</span> | |
</button> | |
</section> | |
<div id="map"></div> | |
<div id="loader"> | |
<div class="CDB-LoaderIcon CDB-LoaderIcon--big"> | |
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50"> | |
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle> | |
</svg> | |
</div> | |
</div> | |
<script> | |
let currentYear = 0; | |
let animation; | |
function getExtent() { | |
const query = encodeURI(`select st_extent(sub.the_geom) as extent from (${layer.getSource()._query}) as sub`); | |
return fetch(`https://roman-carto.carto.com/api/v2/sql?rows_per_page=1&sort_order=&page=0&order_by=&api_key=default_public&q=${query}`) | |
.then(response => response.json()) | |
.then(json => json.rows[0].extent); | |
} | |
function fitMapToBounds() { | |
getExtent().then(function (bounds) { | |
const parsed = /BOX\((.+) (.+),(.+) (.+)\)/.exec(bounds) | |
.splice(1, 4) | |
.map(e => parseFloat(e)); | |
map.fitBounds([ | |
[parsed[0], parsed[1]], [parsed[2], parsed[3]] | |
]); | |
}); | |
} | |
function getSQLSource() { | |
return new carto.source.SQL(` | |
SELECT | |
q.cartodb_id, | |
a.the_geom, | |
a.the_geom_webmercator, | |
q.cod_municipio, | |
q.cod_provincia, | |
q.nombre_municipio, | |
${years.map(year => `q.ganador_${year}`).join(',')} | |
FROM | |
"roman-carto".resultados_inline as q | |
LEFT JOIN | |
"roman-carto".ign_spanish_adm3_municipalities_displaced_canary as a | |
ON | |
q.cod_municipio = a.parsed_code | |
${ selectedProvinces.length ? `WHERE q.cod_provincia IN (${selectedProvinces.join(',')})` : ''} | |
`); | |
} | |
function renderProvinces() { | |
provincesEl.innerHTML = ''; | |
provinces.forEach((provPair) => { | |
const opt = document.createElement('div'); | |
opt.classList.add('province'); | |
opt.setAttribute('data-province-value', provPair[1]); | |
const selected = selectedProvinces.indexOf(provPair[1]) !== -1; | |
if (selected) opt.className = 'province selected'; | |
opt.innerHTML = ` | |
<span>${provPair[0]}</span> | |
<i class="toggle" /> | |
`; | |
opt.addEventListener('click', toggleProvince); | |
provincesEl.appendChild(opt); | |
}); | |
} | |
function toggleProvince(e) { | |
const intProv = parseInt(this.getAttribute('data-province-value')); | |
const where = selectedProvinces.indexOf(intProv); | |
if (where != -1) { | |
selectedProvinces.splice(where, 1); | |
} else { | |
selectedProvinces.push(intProv); | |
} | |
renderProvinces(); | |
layer.update(getSQLSource(), layer.getViz()).then(fitMapToBounds); | |
} | |
function clearProvincesFilter() { | |
selectedProvinces = []; | |
layer.update(getSQLSource(), layer.getViz()).then(fitMapToBounds); | |
} | |
document.querySelector('.js-clear-filter') | |
.addEventListener('click', function () { | |
clearProvincesFilter(); | |
renderProvinces(); | |
}); | |
const provincesEl = document.querySelector('#provinces'); | |
const provinces = [["Sevilla", 41], ["Asturias", 33], ["Palencia", 34], ["Huelva", 21], ["Valladolid", 47], ["Álava", 1], ["Lugo", 27], ["Ávila", 5], ["Alicante / Alacant", 3], ["Valencia / València", 46], ["Málaga", 29], ["Badajoz", 6], ["Illes Balears", 7], ["Castellón / Castelló", 12], ["Pontevedra", 36], ["Ceuta", 51], ["Zaragoza", 50], ["Burgos", 9], ["Vizcaya", 48], ["Tarragona", 43], ["Ciudad Real", 13], ["Ourense", 32], ["Almería", 4], ["Las Palmas", 35], ["Santa Cruz de Tenerife", 38], ["Navarra", 31], ["Segovia", 40], ["Girona", 17], ["A Coruña", 15], ["Barcelona", 8], ["Soria", 42], ["Cantabria", 39], ["Guadalajara", 19], ["Melilla", 52], ["Teruel", 44], ["Jaén", 23], ["Granada", 18], ["Albacete", 2], ["Lleida", 25], ["Salamanca", 37], ["La Rioja", 26], ["Zamora", 49], ["Cádiz", 11], ["Cáceres", 10], ["Huesca", 22], ["Murcia", 30], ["Córdoba", 14], ["Toledo", 45], ["Madrid", 28], ["Guipúzcoa", 20], ["León", 24], ["Cuenca", 16]].sort((a, b) => a[1] - b[1]); | |
let selectedProvinces = []; | |
renderProvinces(); | |
function fetchData(year, code) { | |
const sqlQuery = `select meta_${year} from "roman-carto".results_big WHERE cod_municipio = ${code}`; | |
const urlToFetch = `https://roman-carto.carto.com/api/v2/sql?rows_per_page=1&sort_order=&page=0&order_by=&api_key=default_public&q=${encodeURI(sqlQuery)}`; | |
return fetch(urlToFetch) | |
.then(response => response.json()) | |
.then(json => parseQueryRows(json.rows[0])); | |
} | |
function parseQueryRows(queryRow) { | |
return Object.keys(queryRow) | |
.map(function (key) { | |
const rawData = queryRow[key].replace(/'/g, '"'); | |
const data = JSON.parse(rawData); | |
const parties = data[0]; | |
const votes = data[1]; | |
const zippedArray = parties.map((party, index) => [party, votes[index]]); | |
return zippedArray; | |
})[0]; | |
} | |
const s = carto.expressions; | |
const years = [1977, 1979, 1982, 1986, 1989, 1993, 1996, 2000, 2004, 2008, 2011, 2015, 2016]; | |
const parties = { | |
'PP': '#03a1e2', | |
'PSOE': '#e02c1d', | |
'PODEMOS': '#6b205e', | |
'CS': '#ff6919', | |
'IU': '#e51636', | |
'EH BILDU': '#bbcb35', | |
'ERC-CATSI': '#ffaf32', | |
'ECP': '#a7235e', | |
'PNV': '#21843f', | |
'UCD': '#E04D07', | |
'PCE': '#BE1622', | |
'AP': '#03a1e2', | |
'FDI': '#A961A7', | |
'PDPC': '#F6BA1B', | |
'AP-PDP': '#03a1e2', | |
'CDS': '#61A457', | |
'AP-PDP-PL': '#03a1e2', | |
'CG': '#0064DC', | |
'HB': '#fabada', | |
'CIU': '#F6BA1B', | |
'AIC': '#0F47AF', | |
'CC': '#FFEE02', | |
'PSA-PA': '#00BC00', | |
'NA-BAI': '#DC022C' | |
}; | |
const colors = [...Object.values(parties), '#000'].map(color => s.hex(color)); | |
const ramps = years.map(year => s.ramp(s.buckets(s.prop(`ganador_${year}`), Object.keys(parties)), colors)); | |
const variables = {}; | |
variables.cod_municipio = s.prop('cod_municipio'); | |
variables.nombre_municipio = s.prop('nombre_municipio'); | |
years.forEach((year, index) => { | |
variables[`year_${year}`] = ramps[index] | |
}); | |
const map = new mapboxgl.Map({ | |
container: 'map', | |
style: { | |
version: 8, | |
sources: {}, | |
layers: [] | |
}, | |
center: [-4.38, 39.6], | |
zoom: 5, | |
dragRotate: false, | |
touchZoomRotate: false, | |
}); | |
// Define user | |
carto.setDefaultAuth({ | |
user: 'roman-carto', | |
apiKey: 'default_public' | |
}); | |
// Define layer | |
const source = getSQLSource(); | |
const viz = new carto.Viz({ | |
variables, | |
color: s.var('year_1977'), | |
strokeWidth: 0.3, | |
strokeColor: s.rgba(255, 255, 255, 0.5) | |
}); | |
const layer = new carto.Layer('layer', source, viz); | |
layer.addTo(map); | |
layer.on('loaded', hideLoader); | |
function hideLoader() { | |
document.getElementById('loader').style.opacity = '0'; | |
fitMapToBounds(); | |
} | |
function showLoader() { | |
document.getElementById('loader').style.opacity = '1'; | |
fitMapToBounds(); | |
} | |
// -- Year selector listener | |
const $selector = document.querySelector('.selector'); | |
$selector.addEventListener('click', event => { | |
const element = event.target; | |
if (element.className === 'selector--year') { | |
selectYear(element.dataset.year, true); | |
} | |
}); | |
// -- Define interactivity | |
const interactivity = new carto.Interactivity(layer); | |
const resultsCard = document.querySelector('.card--results'); | |
const mapboxGLCanvas = document.querySelector('.mapboxgl-canvas'); | |
let selectedMunicipio, codSelectedMunicipio; | |
interactivity.on('featureEnter', featureEvent => { | |
mapboxGLCanvas.style.cursor = 'pointer'; | |
}); | |
interactivity.on('featureLeave', featureEvent => { | |
mapboxGLCanvas.style.cursor = 'default'; | |
}); | |
interactivity.on('featureClick', event => { | |
if (event.features.length > 0) { | |
const feature = event.features[0]; | |
showLoader(); | |
showResultsPopup(feature.variables.nombre_municipio.value, feature.variables.cod_municipio.value); | |
} | |
}); | |
function showResultsPopup(nombreMunicipio, codMunicipio) { | |
const activeYear = $selector.querySelector('.active').getAttribute('data-year'); | |
selectedMunicipio = nombreMunicipio; | |
codSelectedMunicipio = codMunicipio; | |
fetchData(activeYear, codMunicipio) | |
.then(results => results.filter(e => e[1] > 0).sort((a, b) => b[1] - a[1])) | |
.then(results => generateResultsTemplate(results)) | |
.then(template => { | |
document.querySelector('.js-municipio').innerHTML = `${nombreMunicipio} in ${$selector.querySelector('.active').getAttribute('data-year')}`; | |
document.querySelector('.js-results').innerHTML = template; | |
resultsCard.style.display = 'block'; | |
hideLoader(); | |
}); | |
} | |
function generateResultsTemplate(results) { | |
if (results.length === 0) { | |
return `<h5>No data for this polygon</h5>`; | |
} | |
return ` | |
<ul class="results"> | |
${results.map(result => { | |
const partyColor = parties[result[0]]; | |
return ` | |
<li class="party"> | |
<div class="party--color" ${partyColor ? 'style="background-color: ' + partyColor + '"' : ''}></div> | |
<span class="party--name">${result[0]}</span> | |
<span class="party--votes">${result[1]}</span> | |
</li>`; | |
}).join('')} | |
</ul> | |
`; | |
} | |
function selectYear(year, stopsAnimation) { | |
if (stopsAnimation) { | |
stop(); | |
} | |
currentYear = years.indexOf(year); | |
document.querySelector('.selector--year.active').classList.remove('active'); | |
document.querySelector(`button[data-year="${year}"]`).classList.add('active'); | |
viz.color.blendTo(s.var(`year_${year}`)); | |
} | |
function toggleAnimation() { | |
animation ? stop() : animate(); | |
} | |
function animate() { | |
animation = setInterval(() => { | |
let nextYear = currentYear + 1; | |
if (nextYear === years.length) { | |
nextYear = 0; | |
} | |
selectYear(years[nextYear]); | |
}, 3000); | |
document.querySelector('.animation-button span').innerHTML = 'Pause'; | |
} | |
function stop() { | |
clearInterval(animation); | |
animation = undefined; | |
document.querySelector('.animation-button span').innerHTML = 'Play'; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment