diff --git a/package-lock.json b/package-lock.json index 4471d4f357f..d4d2123367d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,9 +209,9 @@ } }, "@plotly/d3": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.6.1.tgz", - "integrity": "sha512-lM2dmUqRX1qGtrWczC7QNbQ4Bdgp9sII9i7NV6Hokw06kLH1++x0Ehlj193+PkLYvi1us1IcYjC7IIw+h6GywA==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.7.0.tgz", + "integrity": "sha512-uSVIiWXmc1RKmVXOLAR8wS6xuTvLsOPkB+ZSDernOOc774zG/3hX91qsaXWOQXQJkdPNCMFM4uSFGiBcM6DsnA==" }, "@plotly/d3-sankey": { "version": "0.7.2", @@ -2457,6 +2457,25 @@ "d3-timer": "1" } }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "requires": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + } + }, "d3-hierarchy": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", diff --git a/package.json b/package.json index 6f254075b21..27688415040 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ ] }, "dependencies": { - "@plotly/d3": "^3.6.1", + "@plotly/d3": "3.7.0", "@plotly/d3-sankey": "0.7.2", "@plotly/d3-sankey-circular": "0.33.1", "@plotly/point-cluster": "^3.1.9", @@ -78,6 +78,8 @@ "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3-force": "^1.2.1", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", "d3-hierarchy": "^1.1.9", "d3-interpolate": "^1.4.0", "d3-time-format": "^2.2.3", diff --git a/src/plots/geo/constants.js b/src/plots/geo/constants.js index ccd39c19298..9e92f65f352 100644 --- a/src/plots/geo/constants.js +++ b/src/plots/geo/constants.js @@ -2,7 +2,6 @@ // projection names to d3 function name exports.projNames = { - // d3.geo.projection 'equirectangular': 'equirectangular', 'mercator': 'mercator', 'orthographic': 'orthographic', @@ -24,7 +23,107 @@ exports.projNames = { 'albers usa': 'albersUsa', 'winkel tripel': 'winkel3', 'aitoff': 'aitoff', - 'sinusoidal': 'sinusoidal' + 'sinusoidal': 'sinusoidal', +/* + // potential projections that could be added to the API + + 'airy': 'airy', + // 'albers': 'albers', + 'armadillo': 'armadillo', + 'august': 'august', + 'baker': 'baker', + 'berghaus': 'berghaus', + 'bertin1953': 'bertin1953', + 'boggs': 'boggs', + 'bonne': 'bonne', + 'bottomley': 'bottomley', + 'bromley': 'bromley', + // 'chamberlin': 'chamberlin', + 'chamberlin africa': 'chamberlinAfrica', + 'collignon': 'collignon', + 'craig': 'craig', + 'craster': 'craster', + 'cylindrical equal area': 'cylindricalEqualArea', + 'cylindrical stereographic': 'cylindricalStereographic', + 'eckert1': 'eckert1', + 'eckert2': 'eckert2', + 'eckert3': 'eckert3', + 'eckert5': 'eckert5', + 'eckert6': 'eckert6', + 'eisenlohr': 'eisenlohr', + 'fahey': 'fahey', + 'foucaut': 'foucaut', + 'foucaut sinusoidal': 'foucautSinusoidal', + 'gilbert': 'gilbert', + 'gingery': 'gingery', + 'ginzburg4': 'ginzburg4', + 'ginzburg5': 'ginzburg5', + 'ginzburg6': 'ginzburg6', + 'ginzburg8': 'ginzburg8', + 'ginzburg9': 'ginzburg9', + 'gringorten': 'gringorten', + 'guyou': 'guyou', + 'hammer retroazimuthal': 'hammerRetroazimuthal', + 'healpix': 'healpix', + 'hill': 'hill', + 'homolosine': 'homolosine', + 'hufnagel': 'hufnagel', + 'hyperelliptical': 'hyperelliptical', + 'lagrange': 'lagrange', + 'larrivee': 'larrivee', + 'laskowski': 'laskowski', + 'littrow': 'littrow', + 'loximuthal': 'loximuthal', + // 'modified stereographic': 'modifiedStereographic', + 'modified stereographic alaska': 'modifiedStereographicAlaska', + 'modified stereographic gs48': 'modifiedStereographicGs48', + 'modified stereographic gs50': 'modifiedStereographicGs50', + 'modified stereographic miller': 'modifiedStereographicMiller', + 'modified stereographic lee': 'modifiedStereographicLee', + 'mt flat polar parabolic': 'mtFlatPolarParabolic', + 'mt flat polar quartic': 'mtFlatPolarQuartic', + 'mt flat polar sinusoidal': 'mtFlatPolarSinusoidal', + 'natural earth1': 'naturalEarth1', + 'natural earth2': 'naturalEarth2', + 'nell hammer': 'nellHammer', + 'nicolosi': 'nicolosi', + 'patterson': 'patterson', + 'polyconic': 'polyconic', + 'rectangular polyconic': 'rectangularPolyconic', + 'satellite': 'satellite', + 'sinu mollweide': 'sinuMollweide', + 'times': 'times', + // 'two point azimuthal': 'twoPointAzimuthal', + // 'two point azimuthalUsa': 'twoPointAzimuthalUsa', + // 'two point equidistant': 'twoPointEquidistant', + // 'two point equidistantUsa': 'twoPointEquidistantUsa', + 'van der grinten': 'vanDerGrinten', + 'van der grinten2': 'vanDerGrinten2', + 'van der grinten3': 'vanDerGrinten3', + 'van der grinten4': 'vanDerGrinten4', + // 'wagner': 'wagner', + 'wagner4': 'wagner4', + 'wagner6': 'wagner6', + // 'wagner7': 'wagner7', + 'wiechel': 'wiechel', + 'winkel3': 'winkel3', + + // 'interrupt': 'interrupt', + 'interrupted homolosine': 'interruptedHomolosine', + 'interrupted sinusoidal': 'interruptedSinusoidal', + 'interrupted boggs': 'interruptedBoggs', + 'interrupted sinu mollweide': 'interruptedSinuMollweide', + 'interrupted mollweide': 'interruptedMollweide', + 'interrupted mollweide hemispheres': 'interruptedMollweideHemispheres', + 'interrupted quartic authalic': 'interruptedQuarticAuthalic', + + 'polyhedral butterfly': 'polyhedralButterfly', + 'polyhedral collignon': 'polyhedralCollignon', + 'polyhedral waterman': 'polyhedralWaterman', + + 'gringorten quincuncial': 'gringortenQuincuncial', + 'peirce quincuncial': 'peirceQuincuncial', +*/ }; // name of the axes diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 5ed53ca379f..0e44c35be81 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -3,6 +3,10 @@ /* global PlotlyGeoAssets:false */ var d3 = require('@plotly/d3'); +var geo = require('d3-geo'); +var geoPath = geo.geoPath; +var geoDistance = geo.geoDistance; +var geoProjection = require('d3-geo-projection'); var Registry = require('../../registry'); var Lib = require('../../lib'); @@ -25,8 +29,6 @@ var geoUtils = require('../../lib/geo_location_utils'); var topojsonUtils = require('../../lib/topojson_utils'); var topojsonFeature = require('topojson-client').feature; -require('./projections')(d3); - function Geo(opts) { this.id = opts.id; this.graphDiv = opts.graphDiv; @@ -247,29 +249,6 @@ proto.updateProjection = function(geoCalcData, fullLayout) { var s = this.fitScale = projection.scale(); var t = projection.translate(); - if( - !isFinite(b[0][0]) || !isFinite(b[0][1]) || - !isFinite(b[1][0]) || !isFinite(b[1][1]) || - isNaN(t[0]) || isNaN(t[0]) - ) { - var attrToUnset = ['fitbounds', 'projection.rotation', 'center', 'lonaxis.range', 'lataxis.range']; - var msg = 'Invalid geo settings, relayout\'ing to default view.'; - var updateObj = {}; - - // clear all attributes that could cause invalid bounds, - // clear viewInitial to update reset-view behavior - - for(var i = 0; i < attrToUnset.length; i++) { - updateObj[this.id + '.' + attrToUnset[i]] = null; - } - - this.viewInitial = null; - - Lib.warn(msg); - gd._promises.push(Registry.call('relayout', gd, updateObj)); - return msg; - } - if(geoLayout.fitbounds) { var b2 = projection.getBounds(makeRangeBox(axLon.range, axLat.range)); var k2 = Math.min( @@ -508,7 +487,7 @@ proto.updateFx = function(fullLayout, geoLayout) { bgRect.on('mousemove', function() { var lonlat = _this.projection.invert(Lib.getPositionFromD3Event()); - if(!lonlat || isNaN(lonlat[0]) || isNaN(lonlat[1])) { + if(!lonlat) { return dragElement.unhover(gd, d3.event); } @@ -648,9 +627,8 @@ proto.render = function() { } }; -// Helper that wraps d3.geo[/* projection name /*]() which: +// Helper that wraps d3[geo + /* Projection name /*]() which: // -// - adds 'fitExtent' (available in d3 v4) // - adds 'getPath', 'getBounds' convenience methods // - scopes logic related to 'clipAngle' // - adds 'isLonLatOverEdges' method @@ -663,7 +641,11 @@ function getProjection(geoLayout) { var projLayout = geoLayout.projection; var projType = projLayout.type; - var projection = d3.geo[constants.projNames[projType]](); + var projName = constants.projNames[projType]; + // uppercase the first letter and add geo to the start of method name + projName = 'geo' + projName.charAt(0).toUpperCase() + projName.slice(1); + var projFn = geo[projName] || geoProjection[projName]; + var projection = projFn(); var clipAngle = geoLayout._isClipped ? constants.lonaxisSpan[projType] / 2 : @@ -686,7 +668,7 @@ function getProjection(geoLayout) { if(clipAngle) { var r = projection.rotate(); - var angle = d3.geo.distance(lonlat, [-r[0], -r[1]]); + var angle = geoDistance(lonlat, [-r[0], -r[1]]); var maxAngle = clipAngle * Math.PI / 180; return angle > maxAngle; } else { @@ -695,38 +677,13 @@ function getProjection(geoLayout) { }; projection.getPath = function() { - return d3.geo.path().projection(projection); + return geoPath().projection(projection); }; projection.getBounds = function(object) { return projection.getPath().bounds(object); }; - // adapted from d3 v4: - // https://github.com/d3/d3-geo/blob/master/src/projection/fit.js - projection.fitExtent = function(extent, object) { - var w = extent[1][0] - extent[0][0]; - var h = extent[1][1] - extent[0][1]; - var clip = projection.clipExtent && projection.clipExtent(); - - projection - .scale(150) - .translate([0, 0]); - - if(clip) projection.clipExtent(null); - - var b = projection.getBounds(object); - var k = Math.min(w / (b[1][0] - b[0][0]), h / (b[1][1] - b[0][1])); - var x = +extent[0][0] + (w - k * (b[1][0] + b[0][0])) / 2; - var y = +extent[0][1] + (h - k * (b[1][1] + b[0][1])) / 2; - - if(clip) projection.clipExtent(clip); - - return projection - .scale(k * 150) - .translate([x, y]); - }; - projection.precision(constants.precision); if(clipAngle) { diff --git a/src/plots/geo/projections.js b/src/plots/geo/projections.js deleted file mode 100644 index 45698e2bf08..00000000000 --- a/src/plots/geo/projections.js +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Generated by https://github.com/etpinard/d3-geo-projection-picker - * - * which is hand-picks projection from https://github.com/d3/d3-geo-projection - * - * into a CommonJS require-able module. - */ - -'use strict'; - -/* eslint-disable */ - -function addProjectionsToD3(d3) { - d3.geo.project = function(object, projection) { - var stream = projection.stream; - if (!stream) throw new Error("not yet supported"); - return (object && d3_geo_projectObjectType.hasOwnProperty(object.type) ? d3_geo_projectObjectType[object.type] : d3_geo_projectGeometry)(object, stream); - }; - function d3_geo_projectFeature(object, stream) { - return { - type: "Feature", - id: object.id, - properties: object.properties, - geometry: d3_geo_projectGeometry(object.geometry, stream) - }; - } - function d3_geo_projectGeometry(geometry, stream) { - if (!geometry) return null; - if (geometry.type === "GeometryCollection") return { - type: "GeometryCollection", - geometries: object.geometries.map(function(geometry) { - return d3_geo_projectGeometry(geometry, stream); - }) - }; - if (!d3_geo_projectGeometryType.hasOwnProperty(geometry.type)) return null; - var sink = d3_geo_projectGeometryType[geometry.type]; - d3.geo.stream(geometry, stream(sink)); - return sink.result(); - } - var d3_geo_projectObjectType = { - Feature: d3_geo_projectFeature, - FeatureCollection: function(object, stream) { - return { - type: "FeatureCollection", - features: object.features.map(function(feature) { - return d3_geo_projectFeature(feature, stream); - }) - }; - } - }; - var d3_geo_projectPoints = [], d3_geo_projectLines = []; - var d3_geo_projectPoint = { - point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); - }, - result: function() { - var result = !d3_geo_projectPoints.length ? null : d3_geo_projectPoints.length < 2 ? { - type: "Point", - coordinates: d3_geo_projectPoints[0] - } : { - type: "MultiPoint", - coordinates: d3_geo_projectPoints - }; - d3_geo_projectPoints = []; - return result; - } - }; - var d3_geo_projectLine = { - lineStart: d3_geo_projectNoop, - point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); - }, - lineEnd: function() { - if (d3_geo_projectPoints.length) d3_geo_projectLines.push(d3_geo_projectPoints), - d3_geo_projectPoints = []; - }, - result: function() { - var result = !d3_geo_projectLines.length ? null : d3_geo_projectLines.length < 2 ? { - type: "LineString", - coordinates: d3_geo_projectLines[0] - } : { - type: "MultiLineString", - coordinates: d3_geo_projectLines - }; - d3_geo_projectLines = []; - return result; - } - }; - var d3_geo_projectPolygon = { - polygonStart: d3_geo_projectNoop, - lineStart: d3_geo_projectNoop, - point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); - }, - lineEnd: function() { - var n = d3_geo_projectPoints.length; - if (n) { - do d3_geo_projectPoints.push(d3_geo_projectPoints[0].slice()); while (++n < 4); - d3_geo_projectLines.push(d3_geo_projectPoints), d3_geo_projectPoints = []; - } - }, - polygonEnd: d3_geo_projectNoop, - result: function() { - if (!d3_geo_projectLines.length) return null; - var polygons = [], holes = []; - d3_geo_projectLines.forEach(function(ring) { - if (d3_geo_projectClockwise(ring)) polygons.push([ ring ]); else holes.push(ring); - }); - holes.forEach(function(hole) { - var point = hole[0]; - polygons.some(function(polygon) { - if (d3_geo_projectContains(polygon[0], point)) { - polygon.push(hole); - return true; - } - }) || polygons.push([ hole ]); - }); - d3_geo_projectLines = []; - return !polygons.length ? null : polygons.length > 1 ? { - type: "MultiPolygon", - coordinates: polygons - } : { - type: "Polygon", - coordinates: polygons[0] - }; - } - }; - var d3_geo_projectGeometryType = { - Point: d3_geo_projectPoint, - MultiPoint: d3_geo_projectPoint, - LineString: d3_geo_projectLine, - MultiLineString: d3_geo_projectLine, - Polygon: d3_geo_projectPolygon, - MultiPolygon: d3_geo_projectPolygon, - Sphere: d3_geo_projectPolygon - }; - function d3_geo_projectNoop() {} - function d3_geo_projectClockwise(ring) { - if ((n = ring.length) < 4) return false; - var i = 0, n, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1]; - while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1]; - return area <= 0; - } - function d3_geo_projectContains(ring, point) { - var x = point[0], y = point[1], contains = false; - for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) { - var pi = ring[i], xi = pi[0], yi = pi[1], pj = ring[j], xj = pj[0], yj = pj[1]; - if (yi > y ^ yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) contains = !contains; - } - return contains; - } - var ε = 1e-6, ε2 = ε * ε, π = Math.PI, halfπ = π / 2, sqrtπ = Math.sqrt(π), radians = π / 180, degrees = 180 / π; - function sinci(x) { - return x ? x / Math.sin(x) : 1; - } - function sgn(x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; - } - function asin(x) { - return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); - } - function acos(x) { - return x > 1 ? 0 : x < -1 ? π : Math.acos(x); - } - function asqrt(x) { - return x > 0 ? Math.sqrt(x) : 0; - } - var projection = d3.geo.projection, projectionMutator = d3.geo.projectionMutator; - d3.geo.interrupt = function(project) { - var lobes = [ [ [ [ -π, 0 ], [ 0, halfπ ], [ π, 0 ] ] ], [ [ [ -π, 0 ], [ 0, -halfπ ], [ π, 0 ] ] ] ]; - var bounds; - function forward(λ, φ) { - var sign = φ < 0 ? -1 : +1, hemilobes = lobes[+(φ < 0)]; - for (var i = 0, n = hemilobes.length - 1; i < n && λ > hemilobes[i][2][0]; ++i) ; - var coordinates = project(λ - hemilobes[i][1][0], φ); - coordinates[0] += project(hemilobes[i][1][0], sign * φ > sign * hemilobes[i][0][1] ? hemilobes[i][0][1] : φ)[0]; - return coordinates; - } - function reset() { - bounds = lobes.map(function(hemilobes) { - return hemilobes.map(function(lobe) { - var x0 = project(lobe[0][0], lobe[0][1])[0], x1 = project(lobe[2][0], lobe[2][1])[0], y0 = project(lobe[1][0], lobe[0][1])[1], y1 = project(lobe[1][0], lobe[1][1])[1], t; - if (y0 > y1) t = y0, y0 = y1, y1 = t; - return [ [ x0, y0 ], [ x1, y1 ] ]; - }); - }); - } - if (project.invert) forward.invert = function(x, y) { - var hemibounds = bounds[+(y < 0)], hemilobes = lobes[+(y < 0)]; - for (var i = 0, n = hemibounds.length; i < n; ++i) { - var b = hemibounds[i]; - if (b[0][0] <= x && x < b[1][0] && b[0][1] <= y && y < b[1][1]) { - var coordinates = project.invert(x - project(hemilobes[i][1][0], 0)[0], y); - coordinates[0] += hemilobes[i][1][0]; - return pointEqual(forward(coordinates[0], coordinates[1]), [ x, y ]) ? coordinates : null; - } - } - }; - var projection = d3.geo.projection(forward), stream_ = projection.stream; - projection.stream = function(stream) { - var rotate = projection.rotate(), rotateStream = stream_(stream), sphereStream = (projection.rotate([ 0, 0 ]), - stream_(stream)); - projection.rotate(rotate); - rotateStream.sphere = function() { - d3.geo.stream(sphere(), sphereStream); - }; - return rotateStream; - }; - projection.lobes = function(_) { - if (!arguments.length) return lobes.map(function(lobes) { - return lobes.map(function(lobe) { - return [ [ lobe[0][0] * 180 / π, lobe[0][1] * 180 / π ], [ lobe[1][0] * 180 / π, lobe[1][1] * 180 / π ], [ lobe[2][0] * 180 / π, lobe[2][1] * 180 / π ] ]; - }); - }); - lobes = _.map(function(lobes) { - return lobes.map(function(lobe) { - return [ [ lobe[0][0] * π / 180, lobe[0][1] * π / 180 ], [ lobe[1][0] * π / 180, lobe[1][1] * π / 180 ], [ lobe[2][0] * π / 180, lobe[2][1] * π / 180 ] ]; - }); - }); - reset(); - return projection; - }; - function sphere() { - var ε = 1e-6, coordinates = []; - for (var i = 0, n = lobes[0].length; i < n; ++i) { - var lobe = lobes[0][i], λ0 = lobe[0][0] * 180 / π, φ0 = lobe[0][1] * 180 / π, φ1 = lobe[1][1] * 180 / π, λ2 = lobe[2][0] * 180 / π, φ2 = lobe[2][1] * 180 / π; - coordinates.push(resample([ [ λ0 + ε, φ0 + ε ], [ λ0 + ε, φ1 - ε ], [ λ2 - ε, φ1 - ε ], [ λ2 - ε, φ2 + ε ] ], 30)); - } - for (var i = lobes[1].length - 1; i >= 0; --i) { - var lobe = lobes[1][i], λ0 = lobe[0][0] * 180 / π, φ0 = lobe[0][1] * 180 / π, φ1 = lobe[1][1] * 180 / π, λ2 = lobe[2][0] * 180 / π, φ2 = lobe[2][1] * 180 / π; - coordinates.push(resample([ [ λ2 - ε, φ2 - ε ], [ λ2 - ε, φ1 + ε ], [ λ0 + ε, φ1 + ε ], [ λ0 + ε, φ0 - ε ] ], 30)); - } - return { - type: "Polygon", - coordinates: [ d3.merge(coordinates) ] - }; - } - function resample(coordinates, m) { - var i = -1, n = coordinates.length, p0 = coordinates[0], p1, dx, dy, resampled = []; - while (++i < n) { - p1 = coordinates[i]; - dx = (p1[0] - p0[0]) / m; - dy = (p1[1] - p0[1]) / m; - for (var j = 0; j < m; ++j) resampled.push([ p0[0] + j * dx, p0[1] + j * dy ]); - p0 = p1; - } - resampled.push(p1); - return resampled; - } - function pointEqual(a, b) { - return Math.abs(a[0] - b[0]) < ε && Math.abs(a[1] - b[1]) < ε; - } - return projection; - }; - function eckert4(λ, φ) { - var k = (2 + halfπ) * Math.sin(φ); - φ /= 2; - for (var i = 0, δ = Infinity; i < 10 && Math.abs(δ) > ε; i++) { - var cosφ = Math.cos(φ); - φ -= δ = (φ + Math.sin(φ) * (cosφ + 2) - k) / (2 * cosφ * (1 + cosφ)); - } - return [ 2 / Math.sqrt(π * (4 + π)) * λ * (1 + Math.cos(φ)), 2 * Math.sqrt(π / (4 + π)) * Math.sin(φ) ]; - } - eckert4.invert = function(x, y) { - var A = .5 * y * Math.sqrt((4 + π) / π), k = asin(A), c = Math.cos(k); - return [ x / (2 / Math.sqrt(π * (4 + π)) * (1 + c)), asin((k + A * (c + 2)) / (2 + halfπ)) ]; - }; - (d3.geo.eckert4 = function() { - return projection(eckert4); - }).raw = eckert4; - var hammerAzimuthalEqualArea = d3.geo.azimuthalEqualArea.raw; - function hammer(A, B) { - if (arguments.length < 2) B = A; - if (B === 1) return hammerAzimuthalEqualArea; - if (B === Infinity) return hammerQuarticAuthalic; - function forward(λ, φ) { - var coordinates = hammerAzimuthalEqualArea(λ / B, φ); - coordinates[0] *= A; - return coordinates; - } - forward.invert = function(x, y) { - var coordinates = hammerAzimuthalEqualArea.invert(x / A, y); - coordinates[0] *= B; - return coordinates; - }; - return forward; - } - function hammerProjection() { - var B = 2, m = projectionMutator(hammer), p = m(B); - p.coefficient = function(_) { - if (!arguments.length) return B; - return m(B = +_); - }; - return p; - } - function hammerQuarticAuthalic(λ, φ) { - return [ λ * Math.cos(φ) / Math.cos(φ /= 2), 2 * Math.sin(φ) ]; - } - hammerQuarticAuthalic.invert = function(x, y) { - var φ = 2 * asin(y / 2); - return [ x * Math.cos(φ / 2) / Math.cos(φ), φ ]; - }; - (d3.geo.hammer = hammerProjection).raw = hammer; - function kavrayskiy7(λ, φ) { - return [ 3 * λ / (2 * π) * Math.sqrt(π * π / 3 - φ * φ), φ ]; - } - kavrayskiy7.invert = function(x, y) { - return [ 2 / 3 * π * x / Math.sqrt(π * π / 3 - y * y), y ]; - }; - (d3.geo.kavrayskiy7 = function() { - return projection(kavrayskiy7); - }).raw = kavrayskiy7; - function miller(λ, φ) { - return [ λ, 1.25 * Math.log(Math.tan(π / 4 + .4 * φ)) ]; - } - miller.invert = function(x, y) { - return [ x, 2.5 * Math.atan(Math.exp(.8 * y)) - .625 * π ]; - }; - (d3.geo.miller = function() { - return projection(miller); - }).raw = miller; - function mollweideBromleyθ(Cp) { - return function(θ) { - var Cpsinθ = Cp * Math.sin(θ), i = 30, δ; - do θ -= δ = (θ + Math.sin(θ) - Cpsinθ) / (1 + Math.cos(θ)); while (Math.abs(δ) > ε && --i > 0); - return θ / 2; - }; - } - function mollweideBromley(Cx, Cy, Cp) { - var θ = mollweideBromleyθ(Cp); - function forward(λ, φ) { - return [ Cx * λ * Math.cos(φ = θ(φ)), Cy * Math.sin(φ) ]; - } - forward.invert = function(x, y) { - var θ = asin(y / Cy); - return [ x / (Cx * Math.cos(θ)), asin((2 * θ + Math.sin(2 * θ)) / Cp) ]; - }; - return forward; - } - var mollweideθ = mollweideBromleyθ(π), mollweide = mollweideBromley(Math.SQRT2 / halfπ, Math.SQRT2, π); - (d3.geo.mollweide = function() { - return projection(mollweide); - }).raw = mollweide; - function naturalEarth(λ, φ) { - var φ2 = φ * φ, φ4 = φ2 * φ2; - return [ λ * (.8707 - .131979 * φ2 + φ4 * (-.013791 + φ4 * (.003971 * φ2 - .001529 * φ4))), φ * (1.007226 + φ2 * (.015085 + φ4 * (-.044475 + .028874 * φ2 - .005916 * φ4))) ]; - } - naturalEarth.invert = function(x, y) { - var φ = y, i = 25, δ; - do { - var φ2 = φ * φ, φ4 = φ2 * φ2; - φ -= δ = (φ * (1.007226 + φ2 * (.015085 + φ4 * (-.044475 + .028874 * φ2 - .005916 * φ4))) - y) / (1.007226 + φ2 * (.015085 * 3 + φ4 * (-.044475 * 7 + .028874 * 9 * φ2 - .005916 * 11 * φ4))); - } while (Math.abs(δ) > ε && --i > 0); - return [ x / (.8707 + (φ2 = φ * φ) * (-.131979 + φ2 * (-.013791 + φ2 * φ2 * φ2 * (.003971 - .001529 * φ2)))), φ ]; - }; - (d3.geo.naturalEarth = function() { - return projection(naturalEarth); - }).raw = naturalEarth; - var robinsonConstants = [ [ .9986, -.062 ], [ 1, 0 ], [ .9986, .062 ], [ .9954, .124 ], [ .99, .186 ], [ .9822, .248 ], [ .973, .31 ], [ .96, .372 ], [ .9427, .434 ], [ .9216, .4958 ], [ .8962, .5571 ], [ .8679, .6176 ], [ .835, .6769 ], [ .7986, .7346 ], [ .7597, .7903 ], [ .7186, .8435 ], [ .6732, .8936 ], [ .6213, .9394 ], [ .5722, .9761 ], [ .5322, 1 ] ]; - robinsonConstants.forEach(function(d) { - d[1] *= 1.0144; - }); - function robinson(λ, φ) { - var i = Math.min(18, Math.abs(φ) * 36 / π), i0 = Math.floor(i), di = i - i0, ax = (k = robinsonConstants[i0])[0], ay = k[1], bx = (k = robinsonConstants[++i0])[0], by = k[1], cx = (k = robinsonConstants[Math.min(19, ++i0)])[0], cy = k[1], k; - return [ λ * (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), (φ > 0 ? halfπ : -halfπ) * (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) ]; - } - robinson.invert = function(x, y) { - var yy = y / halfπ, φ = yy * 90, i = Math.min(18, Math.abs(φ / 5)), i0 = Math.max(0, Math.floor(i)); - do { - var ay = robinsonConstants[i0][1], by = robinsonConstants[i0 + 1][1], cy = robinsonConstants[Math.min(19, i0 + 2)][1], u = cy - ay, v = cy - 2 * by + ay, t = 2 * (Math.abs(yy) - by) / u, c = v / u, di = t * (1 - c * t * (1 - 2 * c * t)); - if (di >= 0 || i0 === 1) { - φ = (y >= 0 ? 5 : -5) * (di + i); - var j = 50, δ; - do { - i = Math.min(18, Math.abs(φ) / 5); - i0 = Math.floor(i); - di = i - i0; - ay = robinsonConstants[i0][1]; - by = robinsonConstants[i0 + 1][1]; - cy = robinsonConstants[Math.min(19, i0 + 2)][1]; - φ -= (δ = (y >= 0 ? halfπ : -halfπ) * (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) - y) * degrees; - } while (Math.abs(δ) > ε2 && --j > 0); - break; - } - } while (--i0 >= 0); - var ax = robinsonConstants[i0][0], bx = robinsonConstants[i0 + 1][0], cx = robinsonConstants[Math.min(19, i0 + 2)][0]; - return [ x / (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), φ * radians ]; - }; - (d3.geo.robinson = function() { - return projection(robinson); - }).raw = robinson; - function sinusoidal(λ, φ) { - return [ λ * Math.cos(φ), φ ]; - } - sinusoidal.invert = function(x, y) { - return [ x / Math.cos(y), y ]; - }; - (d3.geo.sinusoidal = function() { - return projection(sinusoidal); - }).raw = sinusoidal; - function aitoff(λ, φ) { - var cosφ = Math.cos(φ), sinciα = sinci(acos(cosφ * Math.cos(λ /= 2))); - return [ 2 * cosφ * Math.sin(λ) * sinciα, Math.sin(φ) * sinciα ]; - } - aitoff.invert = function(x, y) { - if (x * x + 4 * y * y > π * π + ε) return; - var λ = x, φ = y, i = 25; - do { - var sinλ = Math.sin(λ), sinλ_2 = Math.sin(λ / 2), cosλ_2 = Math.cos(λ / 2), sinφ = Math.sin(φ), cosφ = Math.cos(φ), sin_2φ = Math.sin(2 * φ), sin2φ = sinφ * sinφ, cos2φ = cosφ * cosφ, sin2λ_2 = sinλ_2 * sinλ_2, C = 1 - cos2φ * cosλ_2 * cosλ_2, E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, F, fx = 2 * E * cosφ * sinλ_2 - x, fy = E * sinφ - y, δxδλ = F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ), δxδφ = F * (.5 * sinλ * sin_2φ - E * 2 * sinφ * sinλ_2), δyδλ = F * .25 * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), δyδφ = F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ), denominator = δxδφ * δyδλ - δyδφ * δxδλ; - if (!denominator) break; - var δλ = (fy * δxδφ - fx * δyδφ) / denominator, δφ = (fx * δyδλ - fy * δxδλ) / denominator; - λ -= δλ, φ -= δφ; - } while ((Math.abs(δλ) > ε || Math.abs(δφ) > ε) && --i > 0); - return [ λ, φ ]; - }; - (d3.geo.aitoff = function() { - return projection(aitoff); - }).raw = aitoff; - function winkel3(λ, φ) { - var coordinates = aitoff(λ, φ); - return [ (coordinates[0] + λ / halfπ) / 2, (coordinates[1] + φ) / 2 ]; - } - winkel3.invert = function(x, y) { - var λ = x, φ = y, i = 25; - do { - var cosφ = Math.cos(φ), sinφ = Math.sin(φ), sin_2φ = Math.sin(2 * φ), sin2φ = sinφ * sinφ, cos2φ = cosφ * cosφ, sinλ = Math.sin(λ), cosλ_2 = Math.cos(λ / 2), sinλ_2 = Math.sin(λ / 2), sin2λ_2 = sinλ_2 * sinλ_2, C = 1 - cos2φ * cosλ_2 * cosλ_2, E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, F, fx = .5 * (2 * E * cosφ * sinλ_2 + λ / halfπ) - x, fy = .5 * (E * sinφ + φ) - y, δxδλ = .5 * F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ) + .5 / halfπ, δxδφ = F * (sinλ * sin_2φ / 4 - E * sinφ * sinλ_2), δyδλ = .125 * F * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), δyδφ = .5 * F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ) + .5, denominator = δxδφ * δyδλ - δyδφ * δxδλ, δλ = (fy * δxδφ - fx * δyδφ) / denominator, δφ = (fx * δyδλ - fy * δxδλ) / denominator; - λ -= δλ, φ -= δφ; - } while ((Math.abs(δλ) > ε || Math.abs(δφ) > ε) && --i > 0); - return [ λ, φ ]; - }; - (d3.geo.winkel3 = function() { - return projection(winkel3); - }).raw = winkel3; -} - -module.exports = addProjectionsToD3; diff --git a/test/image/baselines/geo_fitbounds-locations.png b/test/image/baselines/geo_fitbounds-locations.png index 2b51b2ebf5d..7b6215fa3be 100644 Binary files a/test/image/baselines/geo_fitbounds-locations.png and b/test/image/baselines/geo_fitbounds-locations.png differ diff --git a/test/image/baselines/geo_stereographic.png b/test/image/baselines/geo_stereographic.png index fb7b258283c..cf0b24814ca 100644 Binary files a/test/image/baselines/geo_stereographic.png and b/test/image/baselines/geo_stereographic.png differ diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index a9d9b0c0b4d..732406e0368 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -1362,8 +1362,8 @@ describe('Test geo interactions', function() { .toBe(hoverLabelCnt, msg); } - var px = 390; - var py = 290; + var px = 200; + var py = 200; var cnt = 0; Plotly.newPlot(gd, fig).then(function() { @@ -1374,14 +1374,14 @@ describe('Test geo interactions', function() { return new Promise(function(resolve) { var interval = setInterval(function() { - px += 2; + py -= 2; mouseEvent('mousemove', px, py); - if(px < 402) { - _assert('- px ' + px, 1); + if(py > 175) { + _assert('- py ' + py, 1); expect(cnt).toBe(0, 'no plotly_unhover event so far'); } else { - _assert('- px ' + px, 0); + _assert('- py ' + py, 0); expect(cnt).toBe(1, 'plotly_unhover event count'); clearInterval(interval); @@ -1421,66 +1421,6 @@ describe('Test geo interactions', function() { .then(done, done.fail); }); - it('should plot to scope defaults when user setting lead to NaN map bounds', function(done) { - var gd = createGraphDiv(); - - spyOn(Lib, 'warn'); - - Plotly.newPlot(gd, [{ - type: 'scattergeo', - lon: [0], - lat: [0] - }], { - geo: { - projection: { - type: 'kavrayskiy7', - rotation: { - lat: 38.794799, - lon: -81.622334, - } - }, - center: { - lat: -81 - }, - lataxis: { - range: [38.794799, 45.122292] - }, - lonaxis: { - range: [-82.904731, -81.622334] - } - }, - width: 700, - heigth: 500 - }) - .then(function() { - var geoLayout = gd._fullLayout.geo; - var geo = geoLayout._subplot; - - expect(geoLayout.projection.rotation).toEqual({ - lon: 0, lat: 0, roll: 0, - }); - expect(geoLayout.center).toEqual({ - lon: 0, lat: 0 - }); - expect(geoLayout.lonaxis.range).toEqual([-180, 180]); - expect(geoLayout.lataxis.range).toEqual([-90, 90]); - - expect(geo.viewInitial).toEqual({ - 'fitbounds': false, - 'projection.rotation.lon': 0, - 'center.lon': 0, - 'center.lat': 0, - 'projection.scale': 1 - }); - - expect(Lib.warn).toHaveBeenCalledTimes(1); - expect(Lib.warn).toHaveBeenCalledWith( - 'Invalid geo settings, relayout\'ing to default view.' - ); - }) - .then(done, done.fail); - }); - it('should get hover right for choropleths involving landmasses that cross antimeridian', function(done) { var gd = createGraphDiv();