Skip to content

Instantly share code, notes, and snippets.

@willgriffiths
Last active May 25, 2017 15:31
Show Gist options
  • Save willgriffiths/d85ef13cd284d3f7313c16819dcb44d2 to your computer and use it in GitHub Desktop.
Save willgriffiths/d85ef13cd284d3f7313c16819dcb44d2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
svg {
position: absolute;
}
.tooltip text {
font: 14px Helvetica, Arial, sans-serif;
}
.tooltip rect {
fill: rgba(200, 200, 200, 0.8);
}
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.1.0/d3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js"></script>
<script type="text/javascript" src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
function init() {
var meteoriteDataURL = 'https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/meteorite-strike-data.json';
var mapURL = 'https://raw.githubusercontent.com/mbostock/topojson/master/examples/world-110m.json';
function getRadius(mass) {
if (mass < 1000) {
return 0.5;
}
if (mass < 100000) {
return 1;
}
if (mass < 1000000) {
return 2;
}
if (mass < 10000000) {
return 4;
}
return 8;
};
var width = 960,
height = 500,
originalProjectionScale = height / 4;
var tooltipWidth = 170,
tooltipHeight = 74;
var formatTime = d3.timeFormat("%B %d, %Y");
var colors = d3.scaleSequential(d3.interpolateCool);
var projection = d3.geoOrthographic().scale(originalProjectionScale).translate([
width / 2,
height / 2
]).rotate([0, 0]).clipAngle(90).precision(1);
var canvas = d3.select("body")
.append("canvas")
.attr('width', width)
.attr('height', height),
hiddenCanvas = d3.select("body")
.append("canvas")
.attr('width', width)
.attr('height', height)
.style('display','none'),
path = d3.geoPath().projection(projection),
circle = d3.geoCircle(),
ctx,
hiddenCtx;
var cachedHiddenCanvas = false;
var svg = d3.select('body')
.append('svg')
.attr('width', 200)
.attr('height', 150)
.style('top', -200)
.style('left', -200);
d3.json(mapURL, function(error, world) {
if (error)
throw(error);
hiddenCtx = hiddenCanvas.node().getContext('2d');
ctx = canvas.node().getContext('2d');
var land = topojson.feature(world, world.objects.land),
globe = {
type: 'Sphere'
};
d3.json(meteoriteDataURL, function(error, meteoriteData) {
if (error)
throw(error);
var impacts = [],
impactsScaleable = [],
impactScale = 1;
var nextColor = 1,
colorKey = {};
//Remove meteors without mass or coordinates
//Convert mass to integers and year to time
//Sort decending by mass so we can draw them from largest to smallest
var meteorites = meteoriteData.features.filter(function(d) {
return d.geometry && d.properties.mass;
}).map(function(d) {
d.properties.mass = parseFloat(d.properties.mass,10);
d.properties.year = d3.isoParse(d.properties.year);
return d;
}).sort(function(a,b) {
return b.properties.mass - a.properties.mass;
});
//Colour scale for coloring meteorite impacts
colors.domain([d3.max(meteorites, function(d) {
return Math.log10(d.properties.mass);
}),d3.min(meteorites, function(d) {
return Math.log10(d.properties.mass);
})]);
meteorites = meteorites.map(function(d) {
d.properties.color = colors(Math.log10(d.properties.mass));
return d;
});
meteorites.forEach(function(d,i) {
var color = d3.hsl(d.properties.color),
hiddenColor = getColor()
radius = getRadius(d.properties.mass);
color.opacity = 0.75;
color = color + '';
impacts.push({
circle: circle.center(d.geometry.coordinates).radius(getRadius(d.properties.mass)).precision(20)(),
color: color,
mass: d.properties.mass,
hiddenColor: hiddenColor
});
});
meteorites.forEach(function(d,i) {
//Create a d3 color so we can change the opacity of it
var color = d3.hsl(d.properties.color),
hiddenColor = getColor();
color.opacity = 0.75;
color + '';
colorKey[hiddenColor] = d;
impactsScaleable.push({
circle: circle.precision(20),
color: color,
mass: d.properties.mass,
coordinates: d.geometry.coordinates,
hiddenColor: hiddenColor
});
});
drawCanvas(ctx, impactsScaleable, impactScale, false);
var zoomD3 = d3.zoom()
.scaleExtent([0, 32])
.on("zoom", zoomed);
var dragD3 = d3.drag()
.on("drag", dragged)
.on("start", dragstarted)
.on("end", dragended);
canvas.on('mousemove', mousemove);
canvas.call(dragD3);
canvas.call(zoomD3);
function mousemove() {
var pickedColor,
rgb,
target;
if (!cachedHiddenCanvas) {
drawCanvas(hiddenCtx, impactsScaleable, impactScale, true);
cachedHiddenCanvas = true;
}
pickedColor = hiddenCtx.getImageData(d3.event.offsetX, d3.event.offsetY, 1, 1).data;
rgb = 'rgb(' + pickedColor.slice(0,-1).join(',') + ')';
target = colorKey[rgb] || '';
if(target) {
showTooltip(ctx, target, d3.event.offsetX, d3.event.offsetY);
} else {
hideTooltip();
}
}
function zoomed() {
impactScale = (d3.event.transform.k/3 < 1) ? 1 : d3.event.transform.k/3;
currentK = d3.event.transform.k;
projection.scale((d3.event.transform.k * originalProjectionScale));
drawCanvas(ctx, impactsScaleable, impactScale, false);
cachedHiddenCanvas = false;
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
var scale = projection.scale(),
currentRotation = projection.rotate(),
rotationScale = scale/50,
newLambda,
newPhi;
newLambda = currentRotation[0] + (d3.event.dx/rotationScale);
newPhi = currentRotation[1] + (-d3.event.dy/rotationScale);
projection.rotate([newLambda,newPhi]);
drawCanvas(ctx, impactsScaleable, impactScale, false);
cachedHiddenCanvas = false;
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
function drawCanvas(canvas, impacts, impactScale, hidden) {
canvas.save();
canvas.clearRect(0, 0, width, height);
//Draw land
canvas.beginPath(); path.context(canvas)(land);
canvas.fillStyle='black'; canvas.fill(); canvas.closePath();
//Draw sphere outline
canvas.beginPath(); path(globe); canvas.stroke(); canvas.closePath();
//Draw impacts on canvas normally if shown
if (!hidden) {
impacts.forEach(function(d) {
canvas.beginPath();
path.context(canvas)(d.circle.center(d.coordinates).radius(getRadius(d.mass)/impactScale)());
canvas.fillStyle= d.color;
canvas.fill();
canvas.closePath;
});
} else {
impacts.forEach(function(d) {
canvas.beginPath();
path.context(canvas)(d.circle.center(d.coordinates).radius(getRadius(d.mass)/impactScale)());
canvas.fillStyle= d.hiddenColor;
canvas.fill();
canvas.closePath;
});
}
canvas.restore();
}
function getColor() {
var rgb = [];
rgb.push(nextColor % 256);
rgb.push(parseInt((nextColor / 256) % 256,10));
rgb.push(parseInt((nextColor / 65536) % 256,10));
nextColor++;
return 'rgb(' + rgb.join(',') + ')';
}
function showTooltip(canvas, meteor, mouseX, mouseY) {
var tooltip;
svg.selectAll('g').remove();
tooltip = svg.selectAll('g').data([meteor]).enter().append('g').attr('class', 'tooltip');
svg.style('top', mouseY + 5)
.style('left', mouseX + 5);
tooltip.append('rect')
.attr('width', tooltipWidth)
.attr('height', tooltipHeight);
tooltip.append('text')
.attr('transform', 'translate(' + (tooltipWidth/2) + ',21)')
.attr('text-anchor', 'middle')
.text(function(d) {
return 'Name: ' + d.properties.name;
});
tooltip.append('text')
.attr('transform', 'translate(' + (tooltipWidth/2) + ',42)')
.attr('text-anchor', 'middle')
.text(function(d) {
return 'Mass: ' + (d.properties.mass / 1000) + ' kg';
});
tooltip.append('text')
.attr('transform', 'translate(' + (tooltipWidth/2) + ',63)')
.attr('text-anchor', 'middle')
.text(function(d) {
return 'Year: ' + formatTime(d.properties.year);
});
}
function hideTooltip() {
svg.selectAll('g').remove();
svg.style('top', -200)
.style('left', -200);
}
});
});
};
document.addEventListener("DOMContentLoaded", init, false);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment