<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<title>Interactive Map</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Abril+Fatface|Yanone+Kaffeesatz:200" rel="stylesheet">
</head>
<body>
<div id="map-holder"></div>
</body>
</html>
// DEFINE VARIABLES
// Define size of map group
// Full world map is 2:1 ratio
// Using 12:5 because we will crop top and bottom of map
w = 3000;
h = 1250;
// variables for catching min and max zoom factors
var minZoom;
var maxZoom;
// DEFINE FUNCTIONS/OBJECTS
// Define map projection
var projection = d3
.geoEquirectangular()
.center([0, 15]) // set centre to further North as we are cropping more off bottom of map
.scale([w / (2 * Math.PI)]) // scale to fit group width
.translate([w / 2, h / 2]) // ensure centred in group
;
// Define map path
var path = d3
.geoPath()
.projection(projection)
;
// Create function to apply zoom to countriesGroup
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
// Define map zoom behaviour
var zoom = d3
.zoom()
.on("zoom", zoomed)
;
function getTextBox(selection) {
selection
.each(function(d) {
d.bbox = this
.getBBox();
})
;
}
// Function that calculates zoom/pan limits and sets zoom to default value
function initiateZoom() {
// Define a "minzoom" whereby the "Countries" is as small possible without leaving white space at top/bottom or sides
minZoom = Math.max($("#map-holder").width() / w, $("#map-holder").height() / h);
// set max zoom to a suitable factor of this value
maxZoom = 20 * minZoom;
// set extent of zoom to chosen values
// set translate extent so that panning can't cause map to move out of viewport
zoom
.scaleExtent([minZoom, maxZoom])
.translateExtent([[0, 0], [w, h]])
;
// define X and Y offset for centre of map to be shown in centre of holder
midX = ($("#map-holder").width() - minZoom * w) / 2;
midY = ($("#map-holder").height() - minZoom * h) / 2;
// change zoom transform to min zoom and centre offsets
svg.call(zoom.transform, d3.zoomIdentity.translate(midX, midY).scale(minZoom));
}
// zoom to show a bounding box, with optional additional padding as percentage of box size
function boxZoom(box, centroid, paddingPerc) {
minXY = box[0];
maxXY = box[1];
// find size of map area defined
zoomWidth = Math.abs(minXY[0] - maxXY[0]);
zoomHeight = Math.abs(minXY[1] - maxXY[1]);
// find midpoint of map area defined
zoomMidX = centroid[0];
zoomMidY = centroid[1];
// increase map area to include padding
zoomWidth = zoomWidth * (1 + paddingPerc / 100);
zoomHeight = zoomHeight * (1 + paddingPerc / 100);
// find scale required for area to fill svg
maxXscale = $("svg").width() / zoomWidth;
maxYscale = $("svg").height() / zoomHeight;
zoomScale = Math.min(maxXscale, maxYscale);
// handle some edge cases
// limit to max zoom (handles tiny countries)
zoomScale = Math.min(zoomScale, maxZoom);
// limit to min zoom (handles large countries and countries that span the date line)
zoomScale = Math.max(zoomScale, minZoom);
// Find screen pixel equivalent once scaled
offsetX = zoomScale * zoomMidX;
offsetY = zoomScale * zoomMidY;
// Find offset to centre, making sure no gap at left or top of holder
dleft = Math.min(0, $("svg").width() / 2 - offsetX);
dtop = Math.min(0, $("svg").height() / 2 - offsetY);
// Make sure no gap at bottom or right of holder
dleft = Math.max($("svg").width() - w * zoomScale, dleft);
dtop = Math.max($("svg").height() - h * zoomScale, dtop);
// set zoom
svg
.transition()
.duration(500)
.call(
zoom.transform,
d3.zoomIdentity.translate(dleft, dtop).scale(zoomScale)
);
}
// on window resize
$(window).resize(function() {
// Resize SVG
svg
.attr("width", $("#map-holder").width())
.attr("height", $("#map-holder").height())
;
initiateZoom();
});
// create an SVG
var svg = d3
.select("#map-holder")
.append("svg")
// set to the same size as the "map-holder" div
.attr("width", $("#map-holder").width())
.attr("height", $("#map-holder").height())
// add zoom functionality
.call(zoom);
// Define the div for the tooltip
const tooltipDiv = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// get map data
d3.json(
"https://raw.githubusercontent.com/andybarefoot/andybarefoot-www/master/maps/mapdata/custom50.json",
function(json) {
//Bind data and create one path per GeoJSON feature
countriesGroup = svg.append("g").attr("id", "map");
// add a background rectangle
countriesGroup
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", w)
.attr("height", h);
// draw a path for each feature/country
countries = countriesGroup
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.attr("id", function(d, i) {
return "country" + d.properties.iso_a3;
})
.attr("class", "country")
// .attr("stroke-width", 10)
// .attr("stroke", "#ff0000")
// add a mouseover action to show name label for feature/country
.on("mouseover", function(d, i) {
d3.select("#countryLabel" + d.properties.iso_a3).style("display", "block");
})
.on("mouseout", function(d, i) {
d3.select("#countryLabel" + d.properties.iso_a3).style("display", "none");
})
// add an onclick action to zoom into clicked country
.on("click", function(d, i) {
var eventLocation=d3.mouse(this);
var coordinates = projection.invert(eventLocation);
var proxy = "https://cors-anywhere.herokuapp.com/";
var wetherInfoUrl =
"https://api.darksky.net/forecast/c68e9aaf0d467528b9363e383bde6254/" +
coordinates[1] +
"," +
coordinates[0] +
"?exclude=minutely,hourly,daily";
$.ajax({
url: proxy + wetherInfoUrl,
success: function(response) {
tooltipDiv
.transition()
.duration(200)
.style("opacity", 0.9);
tooltipDiv
.html('test html tooltip')
.style("left", (eventLocation[0] -250)+ "px")
.style("top", (eventLocation[1]-100) + "px");
console.log("wether info:", response);
console.log("eventLocation", eventLocation);
}
});
d3.selectAll(".country").classed("country-on", false);
d3.select(this).classed("country-on", true);
boxZoom(path.bounds(d), path.centroid(d), 20);
});
// Add a label group to each feature/country. This will contain the country name and a background rectangle
// Use CSS to have class "countryLabel" initially hidden
countryLabels = countriesGroup
.selectAll("g")
.data(json.features)
.enter()
.append("g")
.attr("class", "countryLabel")
.attr("id", function(d) {
return "countryLabel" + d.properties.iso_a3;
})
.attr("transform", function(d) {
return (
"translate(" + path.centroid(d)[0] + "," + path.centroid(d)[1] + ")"
);
})
// add mouseover functionality to the label
.on("mouseover", function(d, i) {
d3.select(this).style("display", "block");
})
.on("mouseout", function(d, i) {
d3.select(this).style("display", "none");
})
// add an onlcick action to zoom into clicked country
.on("click", function(d, i) {
d3.selectAll(".country").classed("country-on", false);
d3.select("#country" + d.properties.iso_a3).classed("country-on", true);
boxZoom(path.bounds(d), path.centroid(d), 20);
});
// add the text to the label group showing country name
countryLabels
.append("text")
.attr("class", "countryName")
.style("text-anchor", "middle")
.attr("dx", 0)
.attr("dy", 0)
.text(function(d) {
return d.properties.name;
})
.call(getTextBox);
// add a background rectangle the same size as the text
countryLabels
.insert("rect", "text")
.attr("class", "countryLabelBg")
.attr("transform", function(d) {
return "translate(" + (d.bbox.x - 2) + "," + d.bbox.y + ")";
})
.attr("width", function(d) {
return d.bbox.width + 4;
})
.attr("height", function(d) {
return d.bbox.height;
});
initiateZoom();
}
);
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |