Skip to content

Instantly share code, notes, and snippets.

@rubenmoya
Last active June 6, 2018 18:00
Show Gist options
  • Save rubenmoya/a20f488f87335258080f82c4f266850e to your computer and use it in GitHub Desktop.
Save rubenmoya/a20f488f87335258080f82c4f266850e to your computer and use it in GitHub Desktop.
Hackarto.vl
<!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