🥰D3.js render Vietnam map (ok)
https://jsfiddle.net/5gxacnj2/3/
Last updated
https://jsfiddle.net/5gxacnj2/3/
Last updated
<div id="wrapper">
<div class="scatterplot-wrapper"></div>
<div class="legend-wrapper">
<h1>Population density</h1>
<div class="index-level-bar">
<div class="bar-legend"></div>
<div class="bar-label">Lower denisty</div>
<div class="bar-label second">Higher denisty</div>
<div style="clear: both;"></div>
</div>
</div>
<div id="info">
<div id="header-info">
<span>Area, population and population density in 2011 by province (https://www.gso.gov.vn)</span>
<table id="feature-info">
<tbody>
<tr>
<th class="name" style="width:25%;">Name</th>
<th class="population" style="width:20%;">Population</th>
<th class="area" style="width:15%;">Area</th>
<th class="density" style="width:15%;">Density</th>
<th class="capital" style="width:25%;">Capital</th>
</tr>
</tbody>
</table>
</div>
<div id="loading" class="hidden">
<a class="fa fa-spinner"></a>
</div>
</div>
<div id="map-canvas"></div>
</div>
var width = 960,
height = 700;
var active = d3.select(null);
var zoomScale, zoomTranslate;
// We create a quantile scale to categorize the values in 5 groups.
// The domain is static and has a minimum/maximum of population/density.
// The scale returns text values which can be used for the color CSS
// classes (q0-9, q1-9 ... q8-9)
var quantiles = d3.scale.quantile()
.range(d3.range(9).map(function(i) {
return 'q' + i + '-9';
}));
var svg, g, path, zoom, projection, tooltip, scatterplot;
$(document).ready(function() {
// Area, population and population density in 2011 by province
// get from https://www.gso.gov.vn
// Average population (Thous. pers.)/Area (Km2)/Population density (Person/km2)
// Load in popuplation data with D3 (or jQuery)
d3.select('#loading').classed('hidden', false);
tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0)
.on('click', stopped, true);
d3.csv('https://raw.githubusercontent.com/gponster/d3tuts/master/vn-population-2011.csv', function(error, rows) {
if (error) {
return console.warn(error);
}
loadTopoJson(rows);
});
});
function loadTopoJson(data) {
// @see http://www.gadm.org/
// GADM is a spatial database of the location of the world's
// administrative areas (or adminstrative boundaries) for use in GIS and similar software.
// @see http://mapshaper.org/ for simplify
// A tool for topologically aware shape simplification. Reads and
// writes Shapefile, GeoJSON and TopoJSON formats.
d3.json('https://raw.githubusercontent.com/gponster/d3tuts/master/vn-states.json', function(error, json) {
if (error) {
return console.warn(error);
}
d3.select('#loading').classed('hidden', true);
// While our data can be stored more efficiently in TopoJSON,
// we must convert back to GeoJSON for display.
var features = topojson.feature(json, json.objects.states).features;
// Merge the ag. data and GeoJSON
// Loop through once for each ag. data value
for (var i = 0; i < data.length; i++) {
// Grab state name
var dataIso = data[i].iso;
// Grab data value, and convert from string to float
var density = parseFloat(data[i].density);
var population = parseFloat(data[i].population);
var area = parseFloat(data[i].area);
//Find the corresponding state inside the GeoJSON
for (var j = 0; j < features.length; j++) {
var jsonIso = features[j].properties.iso;
if (dataIso == jsonIso) {
// Copy the data value into the JSON
features[j].properties.density = density;
features[j].properties.population = population;
features[j].properties.area = area;
// Stop looking through the JSON
break;
}
}
}
// Set the domain of the values
quantiles.domain(features.map(function(d) {
return d.properties.density;
}));
var legend = d3.select('.bar-legend').append('svg')
.attr('width', 240)
.attr('height', 12);
legend.selectAll('rect')
.data(d3.range(9).map(function(i) {
return 'q' + i + '-9';
}))
.enter().append('rect')
.attr('width', 240 / 9)
.attr('height', 12)
.attr('x', function(d, i) {
return (240 / 9) * i;
})
.attr('data-level', function(d, i) {
return i;
})
.attr('class', function(d) {
return 'legend ' + d;
}).on('mouseover', function(type) {
d3.selectAll('.legend')
.style('opacity', .3);
d3.select(this)
.style('opacity', 1);
var level = d3.select(this).attr('data-level');
d3.selectAll('.feature')
.style('opacity', .1)
.filter('.q' + level + '-9')
.style('opacity', 1);
d3.selectAll('.bubble')
.style('fill-opacity', .1)
.filter('.q' + level + '-9')
.style('fill-opacity', .75);
d3.selectAll('.state-boundary')
.style('stroke-opacity', .3);
})
.on('mouseout', function(type) {
d3.selectAll('.legend')
.style('opacity', 1);
d3.selectAll('.feature')
.style('opacity', 1);
d3.selectAll('.bubble')
.style('fill-opacity', .75);
d3.selectAll('.state-boundary')
.style('stroke-opacity', 1);
});
drawMap(json, features);
drawScatterplot(features);
});
};
function drawScatterplot(data) {
var margin = {
top: 20,
right: 10,
bottom: 80,
left: 40
};
var w = 270 - margin.left - margin.right;
var h = 270 - margin.top - margin.bottom;
var scatter = d3.select('.scatterplot-wrapper')
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
/// Population (x)
var xscale = d3.scale.linear()
.domain([d3.min(data, function(d) {
return d.properties.population;
}),
d3.max(data, function(d) {
return d.properties.population;
})
])
.range([0, w]);
// Area (y)
var yscale = d3.scale.linear()
.domain([d3.min(data, function(d) {
return d.properties.area / 1000;
}),
d3.max(data, function(d) {
return d.properties.area / 1000;
})
])
.range([h, 0]);
var rscale = d3.scale.sqrt()
.domain([d3.min(data, function(d) {
return d.properties.density;
}),
d3.max(data, function(d) {
return d.properties.density;
})
])
.range([3, 15]);
// Define X axis
var xaxis = d3.svg.axis()
.scale(xscale)
.orient('bottom')
.tickSize(-h)
.tickFormat(d3.format('s'));
// Define Y axis
var yaxis = d3.svg.axis()
.scale(yscale)
.orient('left')
.ticks(6)
.tickSize(-w);
// Create X axis
scatter.append('g')
.attr('class', 'x-axis axis')
.attr('transform', 'translate(0,' + (h) + ')')
.call(xaxis);
// Create Y axis
scatter.append('g')
.attr('class', 'y-axis axis')
.attr('transform', 'translate(' + 0 + ',0)')
.call(yaxis);
// Add label to X axis
scatter.append('text')
.attr('class', 'x label')
.attr('text-anchor', 'middle')
.attr('x', w - w / 2)
.attr('y', h + margin.bottom / 2)
.text('Population');
// Add label to Y axis
scatter.append('text')
.attr('class', 'y label')
.attr('text-anchor', 'middle')
.attr('y', -margin.left + 5)
.attr('x', 0 - (h / 2))
.attr('dy', '1em')
.attr('transform', 'rotate(-90)')
.text('Area (1000 km2)');
var clr = d3.scale.category20();
var circles = scatter.selectAll('circle')
.data(data).enter()
.append('circle')
.attr('cx', function(d) {
return xscale(d.properties.population);
})
.attr('cy', function(d) {
return yscale(d.properties.area / 1000);
})
.attr('r', function(d) {
return rscale(d.properties.density);
})
.attr('class', function(d) {
return 'bubble state-' + d.properties.iso + ' ' +
quantiles(d.properties.density);
})
.on('mouseover', function(d) {
tooltip.transition().duration(300)
.style('opacity', 1);
tooltip.text(d.properties.name)
.style('left', d3.event.pageX + 'px')
.style('top', (d3.event.pageY - 50) + 'px');
}).on('mouseout', function(d) {
tooltip.transition().duration(300)
.style('opacity', 0);
});
}
function drawMap(json, features) {
// @see http://geojson.org/
// Create a first guess for the projection
//var center = d3.geo.centroid(json);
var center = [106.34899620666437, 16.553160650957434];
var scale = 2500;
var offset = [width / 2, height / 2 - 50];
// The projection function takes a location [longitude, latitude]
// and returns a Cartesian coordinates [x,y] (in pixels).
//
// D3 has several built-in projections. Albers USA is a composite projection
// that nicely tucks Alaska and Hawaii beneath the Southwest.
//
// Albers USA (albersUsa) is actually the default projection for d3.path.geo()
// The default scale value is 1,000. Anything smaller will shrink the map;
// anything larger will expand it.
//
// Add a scale() method with 800 to our projection in order to shrink things down a bit
//var projection = d3.geo.albersUsa()
// .translate([w / 2, h / 2]).scale([800]);
projection = d3.geo.mercator()
.translate(offset)
.scale([scale])
.center(center);
// We define our first path generator for translating that
// mess of GeoJSON coordinates into even messier messes of SVG path codes.
// Tell the path generator explicitly that it should reference our customized
// projection when generating all those paths
path = d3.geo.path()
.projection(projection);
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 13])
.on('zoom', zoomed);
svg = d3.select('#map-canvas').append('svg')
.attr('width', width)
.attr('height', height)
.on('click', stopped, true);
svg.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.on('click', function() {
zoomScale = 1;
zoomTranslate = [0, 0];
reset();
}, true);
g = svg.append('g');
// Create g before call zoom
//-----------------------------------------------------------------
// For country boundary and state mesh/not data binding
//-----------------------------------------------------------------
var boundary = g.append('g')
.attr('class', 'boundary');
g.attr('class', 'states')
.selectAll('path') // select all the current path nodes
.data(features) // bind these to the features array in json
.enter().append('g') // if not enough elements create a new group
.attr('class', function(d) {
return 'state state-' + d.properties.iso;
})
.on('mouseover', function(d) {
//---------------------------------------------------------
// Tooltip
//---------------------------------------------------------
tooltip.transition().duration(300)
.style('opacity', 1);
tooltip.text(d.properties.name)
.style('left', d3.event.pageX + 'px')
.style('top', (d3.event.pageY - 50) + 'px');
//---------------------------------------------------------
//---------------------------------------------------------
// Feature info
//---------------------------------------------------------
$('#feature-info').find('tr:gt(0)').remove();
var html = '<tr><td>' + d.properties.name + '</td>' +
'<td>' + d.properties.population.toFixed(2) + '</td>' +
'<td>' + d.properties.area.toFixed(2) + '</td>' +
'<td>' + d.properties.density.toFixed(2) + '</td>' +
'<td>' + d.properties.capital + '</td></tr>';
$('#feature-info tr:last').after(html);
//---------------------------------------------------------
//---------------------------------------------------------
// Bubble
//---------------------------------------------------------
d3.selectAll('.bubble')
.style('fill-opacity', .1)
.filter('.state-' + d.properties.iso)
.classed('highlight', true);
})
.on('mouseout', function(d) {
tooltip.transition().duration(300)
.style('opacity', 0);
$('#feature-info').find('tr:gt(0)').remove();
d3.selectAll('.bubble')
.style('fill-opacity', .75)
.classed('highlight', false);
})
.on('click', clicked)
.append('path')
.attr('class', function(d) {
// Use the quantiled value for the class
return 'feature ' + quantiles(d.properties.density);
}) // add attribute class and fill with result from quantiles
.attr('d', path);
//-----------------------------------------------------------------
// Now we can draw boundary, prevent lost data cause by merging and meshing
//-----------------------------------------------------------------
// Country boundary from merge all geometries
boundary.append('path')
.attr('class', 'country-boundary')
.datum(topojson.merge(json, json.objects.states.geometries))
.attr('d', path);
// State mesh
boundary.append('path')
.attr('class', 'state-boundary')
.datum(topojson.mesh(json, json.objects.states, function(a, b) {
return a !== b;
})).attr('d', path);
//-----------------------------------------------------------------
//-----------------------------------------------------------------
// State names
//-----------------------------------------------------------------
g.append('g')
.attr('class', 'state-labels')
.selectAll('text') // select all the current path nodes
.data(features)
.enter().append('text') // if not enough elements create a text
.attr('class', function(d) {
// To make contract text
var className = 'state-label state-' + d.properties.iso;
return className + ' ' + quantiles(d.properties.density);
})
.text(function(d) {
// Name from bound data we already binded using .data(features)
return d.properties.name;
})
// Using transform equivalent to x, y
.attr('transform', function(d) {
return 'translate(' + path.centroid(d) + ')';
})
//.attr('x', function (d) {
// return path.centroid(d)[0];
//})
//.attr('y', function (d) {
// return path.centroid(d)[1];
//})
// The dy attribute indicates a shift along the y-axis on the position
// of an element or its content. What exactly is shifted
// depends on the element for which this attribute is set.
.attr('dy', '.35em');
drawCities();
d3.select(self.frameElement).style('height', height + 'px');
}
function drawCities() {
// Cities group
g.append('g')
.attr('class', 'cities');
d3.csv('vn-cities.csv', function(error, rows) {
if (error) {
return console.warn(error);
}
rows.forEach(function(row, i) {
// Create new group and binding data
var sg = g.selectAll('.cities')
.append('g').datum(row)
.attr('class', function(d) {
return 'city city-' + d.code + ' level-' + d.level;
});
// Append circle to group of city
sg.append('circle')
.attr('class', function(d) {
return 'city-place';
})
.attr('visibility', function(d) {
return d.level < 3 ? 'visible' : 'hidden';
})
.attr('cx', function(d) {
return projection([d.lng, d.lat])[0];
})
.attr('cy', function(d) {
return projection([d.lng, d.lat])[1];
})
.attr('r', 2)
.style('fill', 'white')
.style('stroke', 'black')
.style('stroke-width', 2)
.style('opacity', 0.85)
// Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
// http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
.on('mouseover', function(d) {
//div.transition()
// .duration(200)
// .style('opacity', .9);
})
// fade out
.on('mouseout', function(d) {
//div.transition()
// .duration(500)
// .style('opacity', 0);
});
sg.append('text')
.attr('class', function(d) {
return 'city-label';
})
.text(function(d) {
return d.name;
})
.attr('visibility', function(d) {
return d.level < 2 ? 'visible' : 'hidden';
})
.attr('x', function(d) {
return projection([d.lng, d.lat])[0];
})
.attr('y', function(d) {
return projection([d.lng, d.lat])[1];
})
.attr('text-anchor', function(d) {
return d.lng > 105.7 ? 'start' : 'end';
})
.attr('dx', function(d) {
return (d.lng > 105.7 ? 1 : -1) * 0.7 + 'em';
})
// The dy attribute indicates a shift along the y-axis on the position
// of an element or its content. What exactly is shifted
// depends on the element for which this attribute is set.
.attr('dy', '.35em');
});
});
}
function zoomed() {
g.selectAll('.country-boundary').style('stroke-width', 1 / d3.event.scale + 'px');
g.selectAll('.feature').style('stroke-width', 2 / (d3.event.scale + 0.5) + 'px');
g.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');
g.selectAll('.state-label').style('font-size', (8 / d3.event.scale + 2) + 'px')
g.selectAll('.city-place')
.style('r', 1 / d3.event.scale + 0.7)
.style('stroke-width', 2 / (d3.event.scale + 0.5) + 'px');
g.selectAll('.city-label')
.style('font-size', (12 / d3.event.scale + 1.5) + 'px')
.attr('dy', d3.event.scale == 1 ? '0.35em' : (((1 / d3.event.scale) * 0.35 + 0.2) + 'em'));
g.selectAll('.level-3 .city-label')
.style('font-size', (6 / d3.event.scale + 1.5) + 'px');
g.selectAll('.level-4 .city-label')
.style('font-size', (5 / d3.event.scale + 1.5) + 'px');
if (d3.event.scale < 2) {
g.selectAll('.level-2 .city-label').attr('visibility', 'hidden');
} else {
g.selectAll('.level-2 .city-label').attr('visibility', 'visible');
}
if (d3.event.scale < 3.3) {
g.selectAll('.level-3 .city-label').attr('visibility', 'hidden');
g.selectAll('.level-3 .city-place').attr('visibility', 'hidden');
g.selectAll('.level-4 .city-label').attr('visibility', 'hidden');
g.selectAll('.level-4 .city-place').attr('visibility', 'hidden');
} else {
g.selectAll('.level-3 .city-label').attr('visibility', 'visible');
g.selectAll('.level-3 .city-place').attr('visibility', 'visible');
g.selectAll('.level-4 .city-label').attr('visibility', 'visible');
g.selectAll('.level-4 .city-place').attr('visibility', 'visible');
}
}
// If the drag behavior prevents the default click,
// also stop propagation so we don’t click-to-zoom.
function stopped() {
if (d3.event.defaultPrevented) {
d3.event.stopPropagation();
}
}
function reset() {
active.classed('active', false);
active = d3.select(null);
zoomScale = zoomScale || 1;
zoomTranslate = zoomTranslate || [0, 0];
svg.transition()
.duration(750)
.call(zoom.translate(zoomTranslate).scale(zoomScale).event);
}
function clicked(d) {
if (active.node() === this) {
return reset();
}
active.classed('active', false);
active = d3.select(this).classed('active', true);
// Save current zoom and translate
zoomScale = zoom.scale();
zoomTranslate = zoom.translate();
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.transition()
.duration(750)
.call(zoom.translate(translate).scale(scale).event);
}
#wrapper {
padding-top: 15px;
}
.bar-label {
float: left;
margin-top: 5px;
line-height: 12px;
font-size: 12px;
font-family: 'Helvetica';
}
.bar-label.second {
float: right;
}
table {
display: table;
border-collapse: separate;
border-spacing: 2px;
border-color: grey;
padding: 0px;
}
th {
font-size: 16px;
font-family: sans-serif;
font-weight: normal;
text-align: left;
}
th.name {
color: #E7BA52;
}
th.area {
color: #FC9E27;
}
th.population {
color: #3A7FA3;
}
th.density {
color: #B5CF6B;
}
th.capital {
color: #D6616B;
}
th,
td {
color: #555;
}
.h2,
h2 {
font-size: 30px;
}
.h1,
.h2,
.h3,
h1,
h2,
h3 {
margin-top: 20px;
margin-bottom: 10px;
color: #555;
}
.scatterplot-wrapper {
position: fixed;
z-index: 2;
left: 15px;
bottom: 300px;
width: 270px;
text-align: center;
}
.axis path,
.axis line {
fill: none;
stroke-width: 1px;
stroke: #e7e7e7;
stroke-opacity: .5;
shape-rendering: crispEdges;
}
.axis text {
line-height: 12px;
font-size: 12px;
font-family: 'Helvetica';
fill: #666;
}
.label {
line-height: 12px;
font-size: 12px;
font-family: 'Helvetica';
}
.scatterplot-wrapper circle {
fill-opacity: .75;
}
.scatterplot-wrapper circle:hover {
fill-opacity: 1;
}
.legend-wrapper {
position: fixed;
z-index: 2;
left: 15px;
bottom: 150px;
width: 270px;
text-align: center;
}
.legend-wrapper h1 {
font: 22px 'oswaldregular';
}
#map-canvas {
width: 900px;
margin-left: 290px;
}
#info {
height: 90px;
}
#header-info {
margin-left: 290px;
}
#feature-info {
width: 650px;
}
#loading {
width: 50px;
margin: 0 auto;
}
/* On mouse hover, lighten state color */
.feature:hover {
fill: yellow;
fill-opacity: .35;
}
.bubble.highlight {
stroke: #3A7FA3;
fill-opacity: .75;
}
.active .feature {
fill: yellow;
opacity: 0.75;
}
.city-label {
font: 12px "Helvetica Neue", Helvetica, Arial, sans-serif;
pointer-events: none;
fill: #444;
fill-opacity: .75;
}
.state-label {
fill: #777 !important;
fill-opacity: .5;
font-size: 8px;
text-anchor: middle;
pointer-events: none;
}
.feature,
.legend {
cursor: pointer;
}
.country-boundary {
fill: none;
stroke: #37C3BC;
stroke-linejoin: round;
}
.state-boundary {
fill: none;
stroke: #003568;
stroke-dasharray: 5, 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* http://colorbrewer2.org/ */
.q0-9 {
fill: #F7FCF0;
}
.q1-9 {
fill: #E0F3DB;
}
.q2-9 {
fill: #CCEBC5;
}
.q3-9 {
fill: #A8DDB5;
}
.q4-9 {
/* lighten #7BCCC4 */
fill: #BBF3FF;
}
.q5-9 {
/* lighten #4EB3D3 */
fill: #A1D9FF;
}
.q6-9 {
/* lighten #2B8CBE */
fill: #87BFFF;
}
.q7-9 {
/* lighten #0868AC */
fill: #6EA6E7;
}
.q8-9 {
/* lighten #084081 */
fill: #558DCE;
}
.state-label.q5-9 {
fill: #A8DDB5 !important;
}
.state-label.q6-9 {
fill: #CCEBC5 !important;
}
.state-label.q7-9 {
fill: #E0F3DB !important;
}
.state-label.q8-9 {
fill: #F7FCF0 !important;
}
.overlay {
fill: none;
pointer-events: all;
}
.fa-spinner {
-webkit-animation: spin 1300ms infinite linear;
-moz-animation: spin 1300ms infinite linear;
-ms-animation: spin 1300ms infinite linear;
-o-animation: spin 1300ms infinite linear;
animation: spin 1300ms infinite linear;
font-size: 41px;
text-decoration: none;
color: #C0C0C0;
}
.hidden {
display: none;
visibility: hidden;
}
div.tooltip {
position: absolute;
text-align: center;
border-radius: 4px;
pointer-events: none;
font-family: arial, helvetica, sans-serif;
font-size: 12px;
border: solid 1px #ccc;
background: rgba(255, 255, 255, .75);
padding: 4px 10px;
position: absolute;
z-index: 1000;
box-shadow: 0 0 8px rgba(0, 0, 0, .2);
pointer-events: none;
width: 100px;
margin-left: -55px;
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes spin {
to {
-moz-transform: rotate(360deg);
}
}
@-ms-keyframes spin {
to {
-ms-transform: rotate(360deg);
}
}
@-o-keyframes spin {
to {
-o-transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}