Skip to content

Instantly share code, notes, and snippets.

@Crashillo
Created April 17, 2017 18:44
Show Gist options
  • Save Crashillo/d3b10527869ac4f932eed4ac1749af6c to your computer and use it in GitHub Desktop.
Save Crashillo/d3b10527869ac4f932eed4ac1749af6c to your computer and use it in GitHub Desktop.
Frontend CARTO test

Frontend CARTO test

Vanilla solution to styling CARTO data application. The map library behind is Leaflet. Due to performance purposes, the Leaflet plugin Leaflet.markercluster is also included, optionally selected by the user.

Issues found

The big problem here was to render all the data from the SQL query, as contains a lot of information useless for this exercise. So, a simplified query brings the following fields:

  • the_geom: Latlng values (mandatory).
  • name: Marker name.
  • adm0name: Marker countryname.
  • pop_max: population.

Once the data is served by AJAX, it's pretty important to draw only the elements you are seeing, otherwise, SVG shapes are not quite good handled by the DOM (that's the reason to use a cluster) when there are many.

map.getBounds().contains(latlng) // Check whether the current map view contains the element you have 

Basemaps

You can choose between:

  • OpenStreetMaps
  • GoogleMaps
  • CARTO Light
  • Mapbox Streets

Choropleth map

After take a look at the data, I've found the pop_max field, which can be used to depict their values in a range. There is a checkbox to turn the map into a choropleth one, using that field as reference.

To legend or not to legend

Add a legend is necessary when you cannot guess what's the map about. The choropleth map version is 100% recommended to add it, you need to know what the colors mean.

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/MarkerCluster.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/MarkerCluster.Default.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="hg__header">
<h2>CartoTEST<small>Customize markers visualization</small></h2></header>
<main class="hg__main">
<div id="mapid"></div>
</main>
<nav class="hg__top">
<div class="options">
<h4>Cluster <small>Use clustermarker for performance </small><input id="clustermarker" type="checkbox"></h4>
<h4>Extra <small>Depict population data as a choropleth map </small><input id="choropleth" type="checkbox"></h4>
</div>
<div class="palette">
<div>Stroke <small>Stroke width</small>
<div class="row">
<input id="stroke" type="range" max="10">
<span id="stroke--visor">5</span>px
</div>
</div>
<div>Opacity <small>Stroke opacity</small>
<div class="row">
<input id="opacity" type="range" max="1" step="0.1">
<span id="opacity--visor">0.5</span>
</div>
</div>
<div>Fill opacity <small>Fill opacity</small>
<div class="row">
<input id="fillopacity" type="range" max="1" step="0.1">
<span id="fillopacity--visor">0.5</span>
</div>
</div>
<div>Radius <small>Set radius</small>
<div class="row">
<input id="radius" type="range" max="20" step="1">
<span id="radius--visor">10</span>px
</div>
</div>
<div>Marker color <small>Guess it</small>
<div class="row">
<select id="colorpicker">
<option class="red">Red</option>
<option class="blue">Blue</option>
<option class="green">Green</option>
</select>
<span id="colorpicker--visor"></span>
</div>
</div>
</div>
</nav>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/leaflet.markercluster.js"></script>
<script src="script.js"></script>
</body>
</html>
// Default style options
var geojsonMarkerOptions = {
weight: 1,
opacity: 1
};
// Basemaps
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
});
var gmaps = L.tileLayer('https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', {
maxZoom: 18,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: '&copy; Google'
});
var carto = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attribution"> CARTO </a>'
});
var mapbox = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
id: 'mapbox.streets'
});
// Main variables
var map = L.map('mapid', {
layers: [osm]
}).setView([26.924, -13.663], 7);
var cluster = L.markerClusterGroup({
chunkedLoading: true
});
var legend = L.control({
position: 'bottomright'
});
var info = L.control({
position: 'topleft'
});
var datacached; //Store a copy of the data
var geojson;
L.control.layers({
"OpenStreetMap": osm,
"Google Maps": gmaps,
"CARTO": carto,
"MapBox Streets": mapbox
}, null).addTo(map);
// Getting data
var request = new XMLHttpRequest();
request.open('GET', 'https://xavijam.carto.com/api/v2/sql?q=SELECT%20the_geom,name,adm0name,pop_max%20FROM%20ne_10m_populated_places_simple&format=GeoJSON', true);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
// Success!
datacached = JSON.parse(request.responseText);
addData(datacached);
} else {
// We reached our target server, but it returned an error
console.error('Ajax error.');
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();
function addData(data) {
function onEachFeature(feature, layer) {
function highlightFeature(e) {
info.onAdd = function(map) {
this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
this.update(layer.feature.properties);
return this._div;
};
// method that we will use to update the control based on feature properties passed
info.update = function(props) {
this._div.innerHTML = '<div>' + props.name + ', ' + props.adm0name + '<br/><small>Population: ' + props.pop_max.toLocaleString() + '</small></div>';
};
info.addTo(map);
}
function resetHighlight(e) {
info.remove();
}
layer.on({
mouseover: highlightFeature,
mouseout: resetHighlight
});
}
function pointToLayer(feature, latlng) {
// Draw only what you need (that's the key)
return (map.getBounds().contains(latlng)) ? L.circleMarker(latlng, geojsonMarkerOptions) : '';
}
function style(feature) {
if (document.getElementById("choropleth").checked) {
geojsonMarkerOptions.color = getColor(feature.properties.pop_max);
geojsonMarkerOptions.fillOpacity = 0.7;
}
return geojsonMarkerOptions;
}
// Remove info from map
map.removeControl(info);
cluster.clearLayers();
if (geojson instanceof L.GeoJSON) {
map.removeLayer(geojson);
}
geojson = L.geoJSON((data.type === "FeatureCollection") ? data : datacached, {
onEachFeature: onEachFeature,
pointToLayer: pointToLayer,
style: style
});
// Whether user wants to clusterize
if (document.getElementById("clustermarker").checked) {
cluster.addLayer(geojson);
}
// If cluster has layers, then show them clustered, otherwise
map.addLayer((cluster.getLayers().length) ? cluster : geojson);
}
// Global functions
function getColor(d) {
return d > 1000000 ? '#b30000' :
d > 500000 ? '#e34a33' :
d > 100000 ? '#fc8d59' :
'#fdcc8a';
}
// Redraw layers
map.on('moveend', addData);
// Clusterize listener
document.getElementById("clustermarker").addEventListener('change', addData);
// Listeners
setListener('stroke', 'weight', 'change');
setListener('opacity', 'opacity', 'change');
setListener('fillopacity', 'fillOpacity', 'change');
setListener('colorpicker', 'color', 'change');
setListener('radius', 'radius', 'change');
function setListener(elemID, ref, event) {
var elem = document.getElementById(elemID);
elem.addEventListener(event, function() {
geojsonMarkerOptions[ref] = elem.value;
document.getElementById(elemID + "--visor").innerHTML = this.value;
geojson.setStyle(geojsonMarkerOptions);
});
}
// Choroplethize
function toggleChoropleth(cb) {
var nodelist = document.querySelectorAll("input[type=range], select");
if (cb.originalTarget.checked) {
// Simulates event to refresh data
map.fire('moveend');
document.getElementsByClassName('palette')[0].classList.add('disabled');
for (var i = 0; i < nodelist.length; i++) {
nodelist[i].disabled = true
}
// Create a legend
legend.onAdd = function(map) {
var div = L.DomUtil.create('div', 'info legend'),
grades = [0, 100000, 500000, 1000000],
labels = [];
div.innerHTML += '<p>Marker population</p>';
// loop through our density intervals and generate a label with a colored square for each interval
for (var i = 0; i < grades.length; i++) {
div.innerHTML +=
'<i style="background:' + getColor(grades[i] + 1).toLocaleString() + '"></i> ' +
grades[i].toLocaleString() + (grades[i + 1] ? '&ndash;' + grades[i + 1].toLocaleString() + '<br>' : '+');
}
return div;
};
legend.addTo(map);
} else {
// Remove legend from map
map.removeControl(legend);
geojson.setStyle(geojsonMarkerOptions);
document.getElementsByClassName('palette')[0].classList.remove('disabled');
for (var i = 0; i < nodelist.length; i++) {
nodelist[i].disabled = false;
}
}
}
document.getElementById("choropleth").addEventListener('change', toggleChoropleth);
.red {
color: #e03838;
}
.green {
color: #329932;
}
.blue {
color: #87CEEB;
}
body {
display: grid;
grid-template-areas: "header" "navigation" "main";
grid-template-columns: 1fr;
grid-template-rows: 50px 120px 1fr;
min-height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
.hg__header {
grid-area: header;
padding: 2%;
}
.hg__main {
grid-area: main;
background-color: #CDF8D7;
min-height: calc(100vh - 170px);
}
.hg__main .info {
padding: 6px 8px;
background: white;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
border-radius: 5px;
font-weight: bold;
}
.hg__main .legend {
line-height: 18px;
color: #555;
}
.hg__main .legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
.hg__top {
grid-area: navigation;
}
.hg__top h3, .hg__top h4 {
margin: 0;
padding: 0;
}
.hg__top small {
font-variant: small-caps;
font-weight: normal;
}
.hg__top input[type="checkbox"] {
float: right;
}
.hg__top .options {
display: flex;
justify-content: flex-end;
margin: 1% auto;
}
.hg__top .palette {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 95vw;
margin: 0 auto;
}
.hg__top .palette.disabled {
color: gray;
}
.hg__top .palette .row {
width: 100%;
display: flex;
align-items: center;
}
.hg__top .palette .row input[type="range"] {
max-width: 75%;
}
@media screen and (max-width: 720px) {
input[type="range"] {
width: 50%;
}
body {
grid-template-rows: 50px 150px 1fr;
}
.hg__left {
font-size: 0.8em;
}
}
#mapid {
width: 100%;
min-height: calc(100vh - 170px);
}
//Color scheme
$color-primary-0: #B6F2C3; // Main Primary color */
$color-primary-1: #E3FCE9;
$color-primary-2: #CDF8D7;
$color-primary-3: #9FEAAF;
$color-primary-4: #87E09B;
@mixin fontcolor($color) {
color: $color;
}
.red {
@include fontcolor(#e03838);
}
.green {
@include fontcolor(#329932);
}
.blue {
@include fontcolor(#87CEEB);
}
body {
display: grid;
grid-template-areas: "header" "navigation" "main";
grid-template-columns: 1fr;
grid-template-rows: 50px 120px 1fr;
min-height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
$no-header-height: calc(100vh - 170px);
.hg__header {
grid-area: header;
padding: 2%;
}
.hg__main {
grid-area: main;
background-color: $color-primary-2;
min-height: $no-header-height;
.info {
padding: 6px 8px;
background: white;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
font-weight: bold;
}
.legend {
line-height: 18px;
color: #555;
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
}
.hg__top {
grid-area: navigation;
h3, h4 {
margin: 0;
padding: 0;
}
small {
font-variant: small-caps;
font-weight: normal;
}
input[type="checkbox"] {
float: right;
}
.options {
display: flex;
justify-content: flex-end;
margin: 1% auto;
}
.palette {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 95vw;
margin: 0 auto;
&.disabled {
color: gray;
}
.row {
width: 100%;
display: flex;
align-items: center;
input[type="range"]{
max-width: 75%;
}
}
}
}
@media screen and (max-width: 720px) {
input[type="range"] {
width: 50%;
}
body {
grid-template-rows: 50px 150px 1fr;
}
.hg__left {
font-size: 0.8em;
}
}
#mapid {
width: 100%;
min-height: $no-header-height;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment