Skip to content

Instantly share code, notes, and snippets.

@kbtz
Forked from TennisVisuals/.block
Created January 21, 2018 23:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kbtz/5249faff2a8cde16032f4c78009bde40 to your computer and use it in GitHub Desktop.
Save kbtz/5249faff2a8cde16032f4c78009bde40 to your computer and use it in GitHub Desktop.
d3v4 reusable updateable global mashup
license: mit

This block enters demo mode immediately. Once complete you can open a javascript console to programatically control the globe using the examples below.

D3v3 version here

Javascript Console

Open full screen in a new tab, then from the javascript console:

  globe.g2m();
  globe.m2g();
  globe.g2m().then(() => globe.pause(2000)).then(() => globe.m2g())

  globe.zoom2('Russia');
  globe.rotate2().then(() => globe.zoom2('Canada'))
  globe.bounce2('Chile');
  globe.g2m().then(() => globe.zoom2('Indonesia'));
  globe.duration(3000).m2g();
  globe.coords([150, -150, 150]);

  globe.options({graticule: { width: '1px' }}).update();
  globe.oceans('blue').update();
  globe.options({graticule: { width: '0px' }, geodesic: { width: '1px'}}).update();
  globe.options({geodesic: { width: '0px' }}).update();
  globe.reset();

  globe.duration(800).tour();        // or globe.tour(true, true);  // for bouncy tour
  globe.tour(false);   // stop tour

  globe.spin();
  globe.land('black').oceans('black').surround('black').boundaries('black').cities("#ffba00").update();
  worldFile(); // load topo file which includes Topology, but possibly no name ids
  globe.spin(false);
  globe.land('green').oceans('blue').surround('white').boundaries('white').cities('blue').update();

  globe.reset();
  globe.options({display: {cities: false}}).update();  // animation slows down with too many elements
  globe.zoom2('USA');
  globe.scale(200);  // 200% of default
  occasions();
  globe.options({display: {flows: true}}).update();
  globe.options({world: {surround: 'black'}, display: { stars: 300 }}).update();

  globe.pulse();     // starts the animation for occasions

Inspirations

  1.  http://bl.ocks.org/PatrickStotz/1f19b3e4cb848100ffd7  Placing Cities on map
  2.  http://bl.ocks.org/KoGor/7025316                      Globe to map transitions
  3.  http://bl.ocks.org/KoGor/5994804                      Highlighting Countries
  4.  http://bl.ocks.org/enjalot/31168147b88a1748bc8b       User Location
  5.  https://bl.ocks.org/mbostock/9656675                  Zoom to Bounding Box
  6.  https://www.jasondavies.com/maps/zoom/                Zooming in and out
  7.  http://projectsdemo.net/globe/v4/                     Geodesic
  8.  http://bl.ocks.org/jasondavies/4188334                Coloring Neighbors
  9.  http://stackoverflow.com/questions/10692100/invoke-a-callback-at-the-end-of-a-transition
  10. https://anthonyskelton.com/2016/d3-js-earthquake-visualizations/

forked from TennisVisuals's block: reusable updateable global mashup

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
.cities:hover{
fill: #33CC33;
stroke-width:0.90em;
stroke:green;
}
.city-selected{
animation: pulseSelected 2s infinite linear; -webkit-animation: pulseSelected 2s infinite linear; -moz-animation: pulseSelected 2s infinite linear; -ms-animation: pulseSelected 2s infinite linear; -o-animation: pulseSelected 2s infinite linear;
}
@keyframes pulseSelected {
0% {
fill: rgba(255,222,47,1.00);
stroke-width:0.5em;
stroke:#ffd600;
}
10% {
fill: rgba(255,222,47,1.00);
stroke-width:0.7em;
stroke:#d3b100;
}
30% {
fill: rgba(255,222,47,1.00);
stroke-width:0.9em;
stroke:#9c8300;
}
50% {
fill: rgba(255,222,47,1.00);
stroke-width:0.11em;
stroke:#5f5000;
}
70% {
fill: rgba(255,222,47,1.00);
stroke-width:0.13em;
stroke:#5f5000;
}
100% {
fill: rgba(255,222,47,1.00);
stroke-width:0.5em;
stroke:#ffd600;
}
}
(function() {
var φ = 1.618033988749895,
ρ = 180 / Math.PI;
var vertices = [
[1,φ,0], [-1,φ,0], [1,-φ,0], [-1,-φ,0],
[0,1,φ], [0,-1,φ], [0,1,-φ], [0,-1,-φ],
[φ,0,1], [-φ,0,1], [φ,0,-1], [-φ,0,-1]
];
var faces = [
[0,1,4], [1,9,4], [4,9,5], [5,9,3], [2,3,7],
[3,2,5], [7,10,2], [0,8,10], [0,4,8], [8,2,10],
[8,4,5], [8,5,2], [1,0,6], [11,1,6], [3,9,11],
[6,10,7], [3,11,7], [11,6,7], [6,0,10], [9,1,11]
].map(function(face) {
return face.map(function(i) {
return vertices[i];
});
});
d3.geodesic = {
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.geodesic.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n).map(function(edge) {
return edge.map(project);
})
};
}
};
function subdivideFaces(n) {
return d3.merge(faces.map(function(face) {
var i01 = interpolate(face[0], face[1]),
i02 = interpolate(face[0], face[2]),
faces = [];
faces.push([
face[0],
i01(1 / n),
i02(1 / n)
]);
for (var i = 1; i < n; ++i) {
var i1 = interpolate(i01(i / n), i02(i / n)),
i2 = interpolate(i01((i + 1) / n), i02((i + 1) / n));
for (var j = 0; j <= i; ++j) {
faces.push([
i1(j / i),
i2(j / (i + 1)),
i2((j + 1) / (i + 1))
]);
}
for (var j = 0; j < i; ++j) {
faces.push([
i1(j / i),
i2((j + 1) / (i + 1)),
i1((j + 1) / i)
]);
}
}
return faces;
}));
}
function subdivideEdges(n) {
var edges = {};
subdivideFaces(n).forEach(function(face) {
add(face[0], face[1]);
add(face[1], face[2]);
add(face[2], face[0]);
});
function add(p0, p1) {
var t;
if (p0[0] < p1[0] || (p0[0] == p1[0] && (p0[1] < p1[1] || (p0[1] == p1[1] && p0[2] < p1[2])))) t = p0, p0 = p1, p1 = t;
edges[p0.map(round) + " " + p1.map(round)] = [p0, p1];
}
function round(d) {
return d3.format(".2f")(d);
}
return d3.values(edges);
}
function interpolate(p0, p1) {
var x0 = p0[0],
y0 = p0[1],
z0 = p0[2],
x1 = p1[0] - x0,
y1 = p1[1] - y0,
z1 = p1[2] - z0;
return function(t) {
return [
x0 + t * x1,
y0 + t * y1,
z0 + t * z1
];
};
}
function project(p) {
var x = p[0],
y = p[1],
z = p[2];
return [
Math.atan2(y, x) * ρ,
Math.acos(z / Math.sqrt(x * x + y * y + z * z)) * ρ - 90
];
}
})();
function geoFlow() {
var data = { countries: { features: [] }, cities: { features: [] }, occasions: { features: [] } };
var options = {
id: undefined,
width: window.innerWidth,
height: window.innerHeight,
display: {
stars: 0,
cities: true,
occasions: true,
flows: false,
transform: 1.6,
sizeToFit: false, // parent element externally sized
},
zoom: {
northup: true,
lngLmt: 80,
time: 1500,
},
user: {
location: true,
update: false, // updates on load; when true can interrupt transitions
stroke: "blue",
fillOpacity: 0.2,
fill: "white"
},
world: {
surround: undefined,
velocity: [.01, -0],
},
map: {
dragging: true,
ortho: true
},
cities: {
"fill": "#1075fe",
"fillOpacity": 0.5,
"stroke": "#1075fe",
"strokeOpacity": 0.2,
},
graticule: {
outline: {
fill: '#def',
stroke: '#000',
width: '0px',
},
fill: '#def',
stroke: '#FFF',
width: '0px'
},
geodesic: {
fill: 'none',
stroke: '#FFF',
width: '0px'
},
oceans: {
fill: '#def',
},
land: {
focus: undefined,
fill: undefined,
stroke: '#fff'
},
tour: {
delay: 1500,
}
};
// consider replacing with an externally definable function
options.radius = Math.min(options.height * .45, options.width * .45);
var events = {
'ready': null,
'settings': { 'click': null },
'update': { 'begin': null, 'end': null },
'land': { 'mouseover': null, 'mouseout': null, 'click': null },
'city': { 'mouseover': null, 'mouseout': null, 'click': null },
};
var starList = createStars(300);
var country_names = {};
var spin_rotation;
var spin_start;
var projectionGlobe;
var projectionGraticule;
var projectionMap;
var projection;
var spacePath;
var background;
var geoPath;
var world;
var surface;
var poi;
var update;
var user_position;
navigator.geolocation.getCurrentPosition(function(pos){
user_position = pos;
if (typeof update == 'function' && options.user.update) update();
});
function globe(selection) {
selection.each(function () {
var dom_parent = d3.select(this);
var color = d3.scaleOrdinal(d3.schemeCategory20);
world = dom_parent.append("svg")
background = world.append("g").attr('id', 'space');
surface = world.append("g").attr('id', 'surface');
poi = world.append("g").attr('id', 'poi');
var graticule = d3.geoGraticule();
var g_outline = surface.append("g")
.attr("id", "g_outline")
.append("path")
.attr("class", "graticule outline path")
.datum(graticule.outline)
var oceans = surface.append("g")
.attr('id', 'oceans')
var ocean_defs = oceans.append('defs');
var ocean = ocean_defs.append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("class", "sphere ocean path")
oceans.append("use")
.attr("class", "sphere")
.attr("xlink:href", "#sphere");
var grid = surface.append("g").attr('id', 'graticule');
var landfeatures = surface.append("g").attr('id', 'land');
var cityfeatures = poi.append("g").attr('id', 'cities');
var occasions = poi.append('g').attr('id', 'occasions');
var g_line = grid.append("path")
.datum(graticule)
.attr("class", "graticule line path");
var gd_line;
if (d3.geodesic) {
gd_line = grid.append("path")
.datum(d3.geodesic.multilinestring(7))
.attr("class", "graticule path")
}
update = function(opts) {
// resize to dimensions of containing element
if (options.display.sizeToFit || (opts && opts.sizeToFit)) {
var dims = dom_parent.node().getBoundingClientRect();
options.width = Math.max(dims.width, 200);
options.height = Math.max(Math.min(dims.height, window.innerHeight), 200);
}
options.radius = Math.min(options.height * .45, options.width * .45);
world
// .attr({
.attrs({
'width': options.width,
'height': options.height,
})
// .style({
.styles({
'background': options.world.surround,
'pointer-events': 'all'
});
// preserve any prior scale
if (options.map.ortho) {
var scale = projection ? projection.scale() : options.radius;
var scale = opts && opts.sizeToFit ? options.radius : scale;
} else {
var scale = projection ? projection.scale() : options.radius / options.display.transform;
var scale = opts && opts.sizeToFit ? options.radius / options.display.transform : scale;
}
var space = d3.geoAzimuthalEquidistant()
.scale(450)
.rotate([80,-90,0])
.center([0, 0]);
spacePath = d3.geoPath()
.projection(space)
.pointRadius(2);
projectionGlobe = d3.geoOrthographic()
.scale(scale)
.center([0, 0])
.translate([options.width / 2, options.height / 2])
.clipAngle(90);
projectionMap = d3.geoEquirectangular()
.scale(scale)
.center([0, 0])
.translate([options.width / 2, options.height / 2])
// preserve any prior rotation
var rotation = projection ? projection.rotate() : [0, 0, 0];
projection = options.map.ortho ? projectionGlobe : projectionMap;
projection.rotate(rotation);
geoPath = d3.geoPath().projection(projection);
ocean
.attr("d", geoPath)
.styles({
'fill': options.oceans.fill,
'stroke': 'none',
'stroke-width': '1px',
});
g_line
.attr("d", geoPath)
.styles({
'fill': options.graticule.fill,
'stroke': options.graticule.stroke,
'stroke-width': options.graticule.width,
});
if (d3.geodesic) {
gd_line
.attr("d", geoPath)
.styles({
'fill': options.geodesic.fill,
'stroke': options.geodesic.stroke,
'stroke-width': options.geodesic.width,
});
}
if (rotation[0] == 0 && rotation[1] == 0 && rotation[2] == 0) {
g_outline
.attr("d", geoPath)
.styles({
'fill': options.graticule.outline.fill,
'stroke': options.graticule.outline.stroke,
'stroke-width': options.graticule.outline.width
});
}
var λ = d3.scaleLinear()
.domain([0, options.width])
.range([-180, 180]);
var φ = d3.scaleLinear()
.domain([0, options.width])
.range([90, -90]);
world
.call(d3.drag()
.subject(function() { var r = projection.rotate(); return {x: λ.invert(r[0]), y: φ.invert(r[1]) }; })
.on("drag", function() {
if (options.map.dragging) {
var lat = λ(d3.event.x);
var lng = φ(d3.event.y);
lng = lng > options.zoom.lngLmt ? options.zoom.lngLmt : lng < -options.zoom.lngLmt ? -options.zoom.lngLmt : lng;
// insure that spinning globe is in sync with this transition
spin_rotation = [lat, lng];
spin_start = Date.now();
projection.rotate([lat, lng]);
space.rotate([-lat, -lng]);
reDraw();
}
}))
addLand();
addCities();
addStars();
addOccasions();
addFlows();
addUserLocation();
reDraw();
function addLand() {
var land = landfeatures.selectAll("path.countries")
.data(data.countries.features)
land.exit().remove();
land.enter()
.append("path")
.attr("class", "countries path")
.on("mouseover", events.land.mouseover)
.on("mouseout", events.land.mouseout)
.on("mousemove", events.land.mousemove)
.on("click", events.land.click)
.merge(land)
.attr("d", geoPath)
.styles({
'fill': landColor, // disables CSS functionality
'stroke': options.land.stroke,
});
function landColor(d, i) {
if (data.neighbors) {
if (d.color) return d.color;
return color(d.color = d3.max(data.neighbors[i], function(n) { return data.countries.features[n].color; }) + 1 | 0);
} else {
return options.land.fill;
}
}
}
function addCities() {
if (!options.display.cities) {
cityfeatures.selectAll("path.cities").remove();
return;
}
var population_array = data.cities.features.map(function(f) { return f.properties.population; });
var max_population = population_array.sort(d3.descending)[0];
if (max_population) {
var rMin = 0;
var peoplePerPixel = 20000000 / options.radius; // consider making this configurable
var rMax = Math.sqrt(max_population / (peoplePerPixel * Math.PI));
// var rScale = d3.scale.sqrt();
var rScale = d3.scaleSqrt();
rScale.domain([0, max_population]);
rScale.range([rMin, rMax]);
data.cities.features.forEach(function(c) {
c.properties.radius = rScale(c.properties.population);
});
}
var cities = cityfeatures.selectAll("path.cities")
.data(data.cities.features)
cities.exit().remove();
cities.enter()
.append("path")
.attr("class", "cities")
.merge(cities)
.attr("d", pointPath)
.styles({
"fill": options.cities.fill,
"fill-opacity": options.cities.fillOpacity,
"stroke": options.cities.stroke,
"stroke-opacity": options.cities.strokeOpacity,
})
.on("click", function(d, i) {
globe.zoom2(i, 'city');
d3.selectAll('.cities').classed('city-selected', false);
d3.select(this).classed("city-selected", true);
});
}
function addOccasions() {
if (!options.display.occasions || !data.occasions.features.length) {
occasions.selectAll('g').remove();
return;
}
var og = occasions.selectAll('g')
.data(data.occasions.features);
og.exit().remove();
og.enter().append('g')
.attrs({
'class': 'occasion',
'id': function(d) { return d.id; },
})
var pc = og.selectAll('.pulse-circle')
.data(function(d) { return [d]; }, get_key);
pc.exit().remove();
pc.enter()
.append("path")
.attr('class', 'occ pulse-circle')
.merge(pc)
.style("fill", 'white')
.attr("d", pointPath)
var oc = og.selectAll('.oc-circle')
.data(function(d) { return [d]; }, get_key);
oc.exit().remove();
oc.enter()
.append('path')
.attr('class', 'occ oc-circle')
.merge(oc)
.attr("d", pointPath)
.styles({
"fill" : 'red',
'opacity': 0.75,
})
var oct = oc.selectAll('.oct')
.data(function(d) { return [d]; }, get_key);
oct.exit().remove();
oct.enter().append('title')
.attr('class', 'oct')
.merge(oct)
.text(function(d) {
return 'Magnitue ' + d.properties.mag + ' ' + d.properties.place;
})
}
function addFlows() {
if (!options.display.flows || !data.occasions.features.length) {
cityfeatures.selectAll(".arcPath").remove();
return;
}
var points = data.occasions.features.map(m => m.geometry.coordinates)
for (var n = 1, e = points.length, coords = []; ++n < e;) {
coords.push({ type: "LineString", coordinates: [points[n-1], points[n]] });
}
var arcs = cityfeatures.selectAll(".arcPath")
.data(coords);
arcs.exit().remove();
arcs
.enter()
.append("path")
.merge(arcs)
.attrs({
'class': 'arcPath path',
'fill': 'none',
'stroke': 'red',
'stroke-width': '1px'
})
.attr("d", geoPath)
}
function addUserLocation() {
if (!options.user.location || !user_position) {
cityfeatures.selectAll('.userloc').remove();
return;
}
var coords = [user_position.coords.longitude, user_position.coords.latitude];
var locations = cityfeatures.selectAll('.userloc')
.data([coords]);
locations.exit().remove();
locations.enter().append("circle")
.attr('class', 'userloc')
.merge(locations)
.attrs({
cx: function(d) { return projection(d)[0] },
cy: function(d) { return projection(d)[1] },
r: options.radius / 20, // consider making this configurable
})
.styles({
"stroke": options.user.stroke,
"fill-opacity": options.user.fillOpacity,
"fill": options.user.fill
});
}
function addStars() {
if (!options.display.stars) {
background.selectAll('path').remove();
}
var constellations = background.selectAll(".star")
.data(starList);
constellations.exit().remove();
constellations.enter()
.append("path")
.attrs({
"class": "star",
"fill": "white"
})
.attr("d", function(d){
spacePath.pointRadius(d.properties.radius);
return spacePath(d);
});
}
}
});
}
// -------------------------- FUNCTIONS ----------------------------
var get_key = function(d) { return d && d.key; };
function animateTransition(interProj) {
return new Promise(function (resolve, reject) {
world.transition()
.duration(2500)
.tween("projection", function() {
return function(_) {
interProj.alpha(_);
reDraw(true);
};
})
.on('end', resolve);
});
}
var pointPath = function(d, i, data, r) {
if (d.properties && d.properties.radius != undefined) {
r = r || d.properties.radius;
}
r = r || 1.5;
var coords = [d.geometry.coordinates[0], d.geometry.coordinates[1]];
var pr = geoPath.pointRadius(globe.scale() / 100 * r);
var rez = pr({ type: "Point", coordinates: coords });
return rez;
}
var circlePath = function(d) {
var circle = d3.geo.circle();
var coords = [d.geometry.coordinates[0], d.geometry.coordinates[1]];
var cc = circle.origin(coords).angle(.5)();
return geoPath(cc);
}
function reDraw() {
background.selectAll("path").attr("d", spacePath);
surface.selectAll("path").attr("d", geoPath);
poi.selectAll(".path").attr("d", geoPath);
poi.selectAll('.cities').attr('d', pointPath)
poi.selectAll('.occ').attr('d', pointPath)
poi.selectAll(".userloc")
.attr('cx', function(d) { return projection(d)[0] })
.attr('cy', function(d) { return projection(d)[1] })
}
globe.changeFocus = changeFocus;
function changeFocus(focusID) {
surface.selectAll("path")
.classed("focused", function(d, i) {
return focusID && d && d.id && d.id == focusID ? options.land.focus = d.id : false;
});
}
// scale_pct is % of parent element (visible space); won't go below 100;
globe.scale = function(scale_pct) {
if (!arguments.length) return +((projection.scale() / options.radius) * 100).toFixed(2);
var scale = scale_pct / 100 * options.radius;
return globe.coords(undefined, scale, true);
}
globe.coords = function(coords, scale) {
return new Promise(function (resolve, reject) {
var current = projection.rotate();
coords = coords || projection.rotate();
scale = scale || projection.scale();
scale = Math.max(options.map.ortho ? options.radius : options.radius / options.display.transform, scale);
// if already at target coordinates, do nothing; resolve.
if (current[0] == coords[0] && current[1] == coords[1] && current[2] == coords[2] && scale == projection.scale()) {
resolve();
return;
}
// insure that spinning globe is in sync with this transition
spin_rotation = coords;
spin_start = Date.now();
world.transition()
.duration(options.zoom.time)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), coords);
var s = d3.interpolate(projection.scale(), scale);
return function(t) {
projection.rotate(r(t)).scale(s(t));
reDraw();
};
})
.on('end', resolve);
});
};
globe.g2m = function() {
return new Promise(function (resolve, reject) {
if (!options.map.ortho) {
globe.rotate2().then(resolve);
return;
}
var current = projection.rotate();
if (current[0] == 0 && current[1] == 0 && current[2] == 0) {
transform();
} else {
globe.rotate2().then(transform);
}
function transform() {
projection = interpolatedProjection(projectionGlobe, projectionMap, false);
projection.scale(options.radius / options.display.transform);
geoPath.projection(projection);
animateTransition(projection)
.then(resolve, reject);
surface.selectAll("path").classed("ortho", options.map.ortho = false);
}
});
}
globe.m2g = function() {
return new Promise(function (resolve, reject) {
if (options.map.ortho) {
globe.rotate2().then(resolve);
return;
}
var current = projection.rotate();
if (current[0] == 0 && current[1] == 0 && current[2] == 0) {
transform();
} else {
globe.rotate2().then(transform);
}
function transform() {
projection = interpolatedProjection(projectionMap, projectionGlobe, true);
projection.scale(options.radius);
geoPath.projection(projection);
animateTransition(projection)
.then(resolve, reject);
surface.selectAll("path").classed("ortho", options.map.ortho = true);
}
})
}
globe.snap2 = function(coords, scale) {
coords = coords || [0, 0, 0];
scale = scale || options.radius;
projection.rotate(coords).scale(scale);
reDraw();
}
globe.rotate2 = function(coords) {
coords = coords || [0, 0, 0];
return globe.coords(coords, options.map.ortho ? options.radius : options.radius / options.display.transform);
}
globe.reset = function() {
return new Promise(function (resolve, reject) {
globe.spin(false);
globe.tour(false);
changeFocus(undefined);
globe.rotate2()
.then(finish, reject)
function finish() {
globe.update({sizeToFit: true});
resolve();
}
});
}
globe.pause = function(time) {
return new Promise(function (resolve, reject) {
setTimeout(function() { resolve(); }, time);
});
}
globe.bounce2 = function(what, which) {
return new Promise(function (resolve, reject) {
globe.rotate2().then(function() { globe.zoom2(what, which).then(resolve, reject); }, reject)
});
}
var tour_countries;
// start can specify first country or false to terminate tour
globe.tour = function(start, bounce) {
start = start == undefined ? true : start;
return new Promise(function (resolve, reject) {
if (!start) {
tour_countries = [];
return resolve();
}
tour_countries = Object.keys(country_names).sort();
if (typeof start == 'string') {
var index = tour_countries.indexOf(start);
if (index >= 0) {
tour_countries = tour_countries.slice(index).concat(tour_countries.slice(0, index)).reverse();
} else {
tour_countries.reverse();
}
}
nextCountry();
function nextCountry() {
if (!tour_countries.length) { return resolve(); }
country = tour_countries.pop();
if (!country) {
nextCountry();
} else {
console.log(country);
if (bounce) {
globe.bounce2(country)
.then(delayNext, reject);
} else {
globe.zoom2(country, undefined, false)
.then(delayNext, reject);
}
}
}
function delayNext(result) {
setTimeout(function() { nextCountry(); }, options.tour.delay);
}
});
}
globe.zoom2 = function(what, which, zoom) {
return new Promise(function (resolve, reject) {
which = which || 'country';
zoom = zoom == undefined ? true : zoom;
var coords = [0, 0, 0];
var scale = options.radius;
if (!isNaN(what) && what < data.countries.features.length && which == 'country') {
coords = d3.geoCentroid(data.countries.features[what]).map(function(m) { return -1 * m; });
changeFocus(undefined);
} else if (!isNaN(what) && what < data.cities.features.length && which == 'city') {
coords = d3.geoCentroid(data.cities.features[what]).map(function(m) { return -1 * m; });
changeFocus(undefined);
} else if (Object.keys(country_names).indexOf(what) >= 0) {
coords = d3.geoCentroid(country_names[what]).map(function(m) { return -1 * m; });
var b = d3.geoBounds(country_names[what]);
var dx = b[1][0] - b[0][0];
var dy = b[1][1] - b[0][1];
var x = (b[0][0] + b[1][0]) / 2;
var y = (b[0][1] + b[1][1]) / 2;
bbox = .1 / Math.max(dx / options.width, dy / options.height);
scale = zoom ? options.radius * bbox : options.radius;
scale = Math.max(scale, options.radius);
changeFocus(country_names[what].id);
} else {
changeFocus(undefined);
}
if (options.zoom.northup && coords.length == 2) coords.push(0);
globe.coords(coords, scale)
.then(resolve, reject);
});
}
function interpolatedProjection(a, b, ortho) {
// var projection = d3.geo.projection(raw).scale(1),
var projection = d3.geoProjection(raw).scale(1),
center = projection.center,
translate = projection.translate,
clip = projection.clipAngle,
α;
function raw(λ, φ) {
var pa = a([λ *= 180 / Math.PI, φ *= 180 / Math.PI]), pb = b([λ, φ]);
return [(1 - α) * pa[0] + α * pb[0], (α - 1) * pa[1] - α * pb[1]];
}
projection.alpha = function(_) {
if (!arguments.length) return α;
α = +_;
var ca = a.center(), cb = b.center(),
ta = a.translate(), tb = b.translate();
center([(1 - α) * ca[0] + α * cb[0], (1 - α) * ca[1] + α * cb[1]]);
translate([(1 - α) * ta[0] + α * tb[0], (1 - α) * ta[1] + α * tb[1]]);
if (ortho === true) {clip(180 - α * 90);}
return projection;
};
projection.alpha(0).scale = b.scale;
delete projection.translate;
delete projection.center;
return projection.alpha(0);
}
globe.update = function(opts) {
if (events.update.begin) events.update.begin();
if (typeof update === 'function') update(opts);
if (events.update.end) events.update.end();
}
// allows updating individual options and suboptions
// while preserving state of other options
globe.options = function(values) {
if (!arguments.length) return options;
keyWalk(values, options);
return globe;
}
function keyWalk(valuesObject, optionsObject) {
if (!valuesObject || !optionsObject) return;
var vKeys = Object.keys(valuesObject);
var oKeys = Object.keys(optionsObject);
for (var k=0; k < vKeys.length; k++) {
if (oKeys.indexOf(vKeys[k]) >= 0) {
var oo = optionsObject[vKeys[k]];
var vo = valuesObject[vKeys[k]];
if (typeof oo == 'object' && typeof vo !== 'function' && oo.constructor !== Array) {
keyWalk(valuesObject[vKeys[k]], optionsObject[vKeys[k]]);
} else {
optionsObject[vKeys[k]] = valuesObject[vKeys[k]];
}
}
}
}
globe.events = function(functions) {
if (!arguments.length) return events;
keyWalk(functions, events);
return globe;
}
globe.width = function(value) {
if (!arguments.length) return options.width;
options.width = value;
return globe;
};
globe.height = function(value) {
if (!arguments.length) return options.height;
options.height = value;
return globe;
};
globe.neighbors = function(neighbors) {
if (!arguments.length) return data.neighbors;
if (!neighbors || typeof neighbors != 'object' || !neighbors.length) return globe;
data.neighbors = neighbors;
return globe;
}
function geometryTypes(features) {
types = [];
for (element in features) {
var type = features[element].geometry.type;
if (types.indexOf(type) < 0) types.push(type);
}
return types;
}
globe.data = function(new_data) {
if (!arguments.length) return data;
if (!new_data || typeof new_data != 'object') return globe;
if (new_data.type == 'Topology') {
var countries = topojson.feature(new_data, new_data.objects.countries);
globe.neighbors(topojson.neighbors(new_data.objects.countries.geometries));
if (!countries.features) return globe;
data.countries = countries;
data.countries.features.forEach(function(c) { country_names[c.properties.name] = c; });
} else if (new_data.type == 'FeatureCollection') {
var elements = new_data
if (!elements.features) return globe;
var feature_types = geometryTypes(elements.features);
if (feature_types.indexOf('Polygon') >= 0 || feature_types.indexOf('MultiPolygon') >=0) {
data.countries = elements;
data.countries.features.forEach(function(c) { country_names[c.properties.name] = c; });
} else if (feature_types.length == 1 && feature_types.indexOf('Point') >= 0) {
data.cities = elements;
}
}
return globe;
}
globe.occasions = function(new_occasions) {
if (!arguments.length) return data.occasions;
data.occasions = new_occasions;
return globe;
}
globe.duration = function(time) {
if (!arguments.length) return options.zoom.time;
options.zoom.time = time;
return globe;
}
// both graticule fill, outline fill, and a sphere are used in an attempt to
// maintain consistent background color during transitions...
globe.oceans = function(color) {
if (!arguments.length) return options.oceans.fill;
options.oceans.fill = color;
options.graticule.fill = color;
options.graticule.outline.fill = color;
return globe;
}
// set color to 'undefined' to re-enable CSS functionality
globe.land = function(color) {
if (!arguments.length) return options.land.fill;
options.land.fill = color;
return globe;
}
globe.boundaries = function(color) {
if (!arguments.length) return options.land.stroke;
options.land.stroke = color;
return globe;
}
globe.cities = function(color) {
if (!arguments.length) return options.cities.fill;
options.cities.fill = color;
options.cities.stroke = color;
return globe;
}
globe.surround = function(color) {
if (!arguments.length) return options.world.surround;
options.world.surround = color;
return globe;
}
globe.initialize = function(error, world, cities) {
if (error) { return error; }
globe.data(world);
globe.data(cities);
update();
if (typeof events.ready == 'function') events.ready();
}
var spin_timer;
globe.spin = function(spin){
spin = spin == undefined ? true : spin;
spin_rotation = projection.rotate();
spin_start = Date.now();
if (spin) {
spin_timer = d3.timer(function() {
var dt = Date.now() - spin_start;
projection.rotate([spin_rotation[0] + options.world.velocity[0] * dt, spin_rotation[1] + options.world.velocity[1] * dt]);
reDraw();
});
} else if (spin_timer) {
spin_timer.stop();
spin_timer = undefined;
}
}
globe.pulse = function() {
poi.selectAll('.pulse-circle')
.attr("d", function(d) { return pointPath(d, 0, 0, 0); })
.style("fill-opacity", 1)
.transition()
.delay(function(d, i) { return i * 200; })
.duration(3000)
.style("fill-opacity", 0)
.attrTween("d", function(d) {
rinterp = d3.interpolate(0, 10);
var fn = function(t) {
d.r = rinterp(t);
return pointPath(d, 0, 0, d.r) || 'M0,0';
};
return fn;
});
}
globe.flow = function() {
d3.selectAll('.arcPath')
.transition()
.ease('linear')
.duration(750)
.attrTween("stroke-dashoffset", function() {
return d3.interpolate(16, 0);
})
.each("end", globe.flow);
}
function createStars(number){
var data = [];
for(var i = 0; i < number; i++){
data.push({
geometry: {
type: 'Point',
coordinates: randomLonLat()
},
type: 'Feature',
properties: {
radius: Math.random() * 1.5
}
});
}
return data;
}
function randomLonLat(){
return [Math.random() * 360 - 180, Math.random() * 180 - 90];
}
return globe;
}
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
demo = function(tasks) {
tasks.reverse();
return new Promise(function (resolve, reject) {
if (!tasks) return reject();
nextTask();
function nextTask() {
if (!tasks.length) {
return resolve();
}
var task = tasks.pop();
if (!task || !task.fx || !task.params) {
nextTask();
} else {
// var i = task.fx(...task.params); // not supported for mobile
var i = task.fx.apply(this, task.params);
if (Promise.resolve(i) == i) {
i.then(nextTask, reject);
} else {
delayNext(task.delay);
}
}
}
function delayNext(delay) {
setTimeout(function() { nextTask(); }, delay);
}
});
}
<!DOCTYPE html>
<html>
<head>
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<title>geoFlow</title>
</head>
<style>
.landTooltip {
position: absolute;
display: none;
pointer-events: none;
background: #fff;
padding: 5px;
text-align: left;
border: solid #ccc 1px;
color: #666;
font-size: 14px;
font-family: sans-serif;
}
.countries {
fill: #aaa;
}
.focused {
fill: #22DD22;
}
.countries:hover {
fill:#FFCC11;
stroke-width: 1px;
}
.wrapper, html, body {
min-height: 100%;
height:100%;
margin:0;
}
.wrapper {
display: flex;
flex-direction: column;
}
#world {
flex: 1 1;
}
/* works but very choppy */
.flowPath {
fill: none;
opacity: 0.5;
stroke-width: 1;
stroke-miterlimit: 10;
stroke-dasharray: 12, 4;
}
.arcPath {
fill: none;
opacity: 0.5;
stroke-width: 1;
stroke-dasharray: 10, 4;
animation: flow 1s linear infinite;
-webkit-animation: flow 1s linear infinite;
}
@keyframes flow {
from { stroke-dashoffset: 14; }
to { stroke-dashoffset: 0; }
}
@-webkit-keyframes flow {
from { stroke-dashoffset: 14; }
to { stroke-dashoffset: 0; }
}
</style>
<body>
<div class="wrapper" id="cosmos">
<div id="world"></div>
</div>
</body>
<link rel="stylesheet" type="text/css" href="cities.css" />
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<script src="https://d3js.org/d3-timer.v1.min.js"></script>
<script src="d3.geodesic.js"></script>
<script src="geoFlow.js"></script>
<script src="gfDemo.js"></script>
<script>
globe = geoFlow();
d3.select('#world').call(globe);
var landTooltip = d3.select("body").append("div").attr("class", "landTooltip");
function landMouseOver(d) {
if (d.properties.name) {
landTooltip.text(d.properties.name)
.styles({
"left": (d3.event.pageX + 7) + "px",
"top": (d3.event.pageY - 15) + "px",
"display": "block",
"opacity": 1,
})
}
}
function landMouseMove(d) {
landTooltip
.styles({
"left": (d3.event.pageX + 7) + "px",
"top": (d3.event.pageY - 15) + "px",
})
}
function landMouseOut(d) {
landTooltip
.styles({
"opacity": 0,
"display": "none",
})
}
function landMouseClick(d) {
var current_focus = globe.options().land.focus;
var new_focus = current_focus == d.id ? undefined : d.id;
globe.changeFocus(new_focus);
globe.options({ land: { focus: new_focus }}).update();
}
function runDemo() {
demo(tasks);
}
globe.events({
'ready': runDemo,
'land': {
'mouseover': landMouseOver,
'mousemove': landMouseMove,
'mouseout': landMouseOut,
'click': landMouseClick,
},
});
window.addEventListener( 'resize', onWindowResize, false );
queue()
.defer(d3.json, "world-countries.json")
.defer(d3.json, "geonames_cities_100k.geojson")
.await(globe.initialize);
function onWindowResize() {
globe.update({sizeToFit: true});
}
worldFile = function(file) {
file = file || "world-110m.json";
d3.json(file, function(error, land) {
globe.data(land).update();
});
}
occasions = function() {
return new Promise(function (resolve, reject) {
// d3.json('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson', function(error, data) {
d3.json('all_day.geojson', function(error, data) {
if (!error) {
globe.occasions(data).update();
globe.update();
globe.pulse();
resolve();
} else {
reject(error);
}
});
});
}
var tasks = [
{ fx: globe.g2m, params: [], delay: undefined },
{ fx: globe.m2g, params: [], delay: undefined },
{ fx: globe.zoom2, params: ['Russia'], delay: undefined },
{ fx: globe.zoom2, params: ['Canada'], delay: undefined },
{ fx: globe.bounce2, params: ['Chile'], delay: undefined },
{ fx: globe.g2m, params: [], delay: undefined },
{ fx: globe.zoom2, params: ['Indonesia'], delay: undefined },
{ fx: globe.duration, params: [3000], delay: 0 },
{ fx: globe.m2g, params: [], delay: undefined },
{ fx: globe.options, params: [{graticule: { width: '1px' }}], delay: 0 },
{ fx: globe.oceans, params: ['blue'], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
{ fx: globe.coords, params: [[150, -150, 150]], delay: undefined },
{ fx: globe.options, params: [{graticule: { width: '0px' }, geodesic: { width: '1px'}}], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
{ fx: globe.reset, params: [], delay: undefined },
{ fx: globe.options, params: [{geodesic: { width: '0px' }}], delay: 0 },
{ fx: globe.land, params: ['black'], delay: 0 },
{ fx: globe.oceans, params: ['black'], delay: 0 },
{ fx: globe.surround, params: ['black'], delay: 0 },
{ fx: globe.boundaries, params: ['black'], delay: 0 },
{ fx: globe.cities, params: ['#ffba00'], delay: 0 },
{ fx: globe.update, params: [], delay: 500 },
{ fx: globe.spin, params: [], delay: 5000 },
{ fx: worldFile, params: [], delay: 3000 },
{ fx: globe.spin, params: [false], delay: 1000 },
{ fx: globe.land, params: ['green'], delay: 0 },
{ fx: globe.oceans, params: ['blue'], delay: 0 },
{ fx: globe.surround, params: ['white'], delay: 0 },
{ fx: globe.boundaries, params: ['white'], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
{ fx: globe.options, params: [{display: {cities: false}}], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
{ fx: globe.zoom2, params: ['USA'], delay: undefined },
{ fx: globe.scale, params: [200], delay: undefined },
{ fx: occasions, params: [], delay: undefined },
{ fx: globe.update, params: [], delay: 10000 },
{ fx: globe.options, params: [{display: {flows: true} }], delay: 0 },
{ fx: globe.update, params: [], delay: 5000 },
{ fx: globe.options, params: [{world: {surround: 'black'}, display: { stars: true }}], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
{ fx: globe.reset, params: [], delay: 4000 },
{ fx: globe.options, params: [{display: {flows: false} }], delay: 0 },
{ fx: globe.update, params: [], delay: 0 },
];
</script>
</script>
</html>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment