Skip to content

Commit 5be0e27

Browse files
committed
geo: make scattergeo use Fx.hover for hover / click interactions
- make Fx.hover pass subplot info to hoverPoints module - add scattergeo hoverPoints and eventData modules - mock xaxis and yaxis in geo instances - factor out is-over-edge logic to use it in hoverPoints - use 'mousemove' instead of 'mouseover' in test to trigger hover
1 parent 3a0c621 commit 5be0e27

File tree

6 files changed

+256
-29
lines changed

6 files changed

+256
-29
lines changed

src/plots/cartesian/graph_interact.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ function hover(gd, evt, subplot) {
379379
curvenum,
380380
cd,
381381
trace,
382+
subplotId,
382383
subploti,
383384
mode,
384385
xval,
@@ -468,7 +469,8 @@ function hover(gd, evt, subplot) {
468469
if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue;
469470

470471
trace = cd[0].trace;
471-
subploti = subplots.indexOf(getSubplot(trace));
472+
subplotId = getSubplot(trace);
473+
subploti = subplots.indexOf(subplotId);
472474

473475
// within one trace mode can sometimes be overridden
474476
mode = hovermode;
@@ -495,6 +497,11 @@ function hover(gd, evt, subplot) {
495497
text: undefined
496498
};
497499

500+
// add ref to subplot object (non-cartesian case)
501+
if(fullLayout[subplotId]) {
502+
pointData.subplot = fullLayout[subplotId]._subplot;
503+
}
504+
498505
closedataPreviousLength = hoverData.length;
499506

500507
// for a highlighting array, figure out what
@@ -545,7 +552,6 @@ function hover(gd, evt, subplot) {
545552
hoverData.splice(0, closedataPreviousLength);
546553
distance = hoverData[0].distance;
547554
}
548-
549555
}
550556

551557
// nothing left: remove all labels and quit
@@ -608,14 +614,10 @@ function hover(gd, evt, subplot) {
608614

609615
if(!hoverChanged(gd, evt, oldhoverdata)) return;
610616

611-
/* Emit the custom hover handler. Bind this like:
612-
* gd.on('hover.plotly', function(extras) {
613-
* // do something with extras.data
614-
* });
615-
*/
616617
if(oldhoverdata) {
617618
gd.emit('plotly_unhover', { points: oldhoverdata });
618619
}
620+
619621
gd.emit('plotly_hover', {
620622
points: gd._hoverdata,
621623
xaxes: xaArray,
@@ -628,7 +630,7 @@ function hover(gd, evt, subplot) {
628630
// look for either .subplot (currently just ternary)
629631
// or xaxis and yaxis attributes
630632
function getSubplot(trace) {
631-
return trace.subplot || (trace.xaxis + trace.yaxis);
633+
return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo;
632634
}
633635

634636
fx.getDistanceFunction = function(mode, dx, dy, dxy) {
@@ -732,6 +734,7 @@ function cleanPoint(d, hovermode) {
732734
if(infomode.indexOf('text') === -1) d.text = undefined;
733735
if(infomode.indexOf('name') === -1) d.name = undefined;
734736
}
737+
735738
return d;
736739
}
737740

src/plots/geo/geo.js

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var d3 = require('d3');
1616
var Color = require('../../components/color');
1717
var Drawing = require('../../components/drawing');
1818
var Axes = require('../../plots/cartesian/axes');
19+
var Fx = require('../../plots/cartesian/graph_interact');
1920

2021
var addProjectionsToD3 = require('./projections');
2122
var createGeoScale = require('./set_scale');
@@ -32,7 +33,6 @@ addProjectionsToD3(d3);
3233

3334

3435
function Geo(options, fullLayout) {
35-
3636
this.id = options.id;
3737
this.graphDiv = options.graphDiv;
3838
this.container = options.container;
@@ -53,6 +53,9 @@ function Geo(options, fullLayout) {
5353
this.zoom = null;
5454
this.zoomReset = null;
5555

56+
this.xaxis = null;
57+
this.yaxis = null;
58+
5659
this.makeFramework();
5760
this.updateFx(fullLayout.hovermode);
5861

@@ -88,6 +91,30 @@ proto.plot = function(geoCalcData, fullLayout, promises) {
8891
.call(_this.zoom)
8992
.on('dblclick.zoom', _this.zoomReset);
9093

94+
_this.framework.on('mousemove', function() {
95+
var mouse = d3.mouse(this),
96+
lonlat = _this.projection.invert(mouse);
97+
98+
if(isNaN(lonlat[0]) || isNaN(lonlat[1])) return;
99+
100+
var evt = {
101+
target: true,
102+
xpx: mouse[0],
103+
ypx: mouse[1]
104+
};
105+
106+
_this.xaxis.c2p = function() { return mouse[0]; };
107+
_this.xaxis.p2c = function() { return lonlat[0]; };
108+
_this.yaxis.c2p = function() { return mouse[1]; };
109+
_this.yaxis.p2c = function() { return lonlat[1]; };
110+
111+
Fx.hover(_this.graphDiv, evt, _this.id);
112+
});
113+
114+
_this.framework.on('click', function() {
115+
Fx.click(_this.graphDiv, { target: true });
116+
});
117+
91118
topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout);
92119

93120
if(_this.topojson === null || topojsonNameNew !== _this.topojsonName) {
@@ -265,6 +292,8 @@ proto.makeFramework = function() {
265292
.attr('id', this.id)
266293
.style('position', 'absolute');
267294

295+
// only choropleth traces use this,
296+
// scattergeo traces use Fx.hover and fullLayout._hoverlayer
268297
var hoverContainer = this.hoverContainer = geoDiv.append('svg');
269298
hoverContainer
270299
.attr(xmlnsNamespaces.svgAttrs)
@@ -294,14 +323,20 @@ proto.makeFramework = function() {
294323
framework.on('dblclick.zoom', null);
295324

296325
// TODO use clip paths instead of nested SVG
326+
327+
this.xaxis = { _id: 'x' };
328+
this.yaxis = { _id: 'y' };
297329
};
298330

299331
proto.adjustLayout = function(geoLayout, graphSize) {
300332
var domain = geoLayout.domain;
301333

334+
var left = graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX,
335+
top = graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY;
336+
302337
this.geoDiv.style({
303-
left: graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX + 'px',
304-
top: graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY + 'px',
338+
left: left + 'px',
339+
top: top + 'px',
305340
width: geoLayout._width + 'px',
306341
height: geoLayout._height + 'px'
307342
});
@@ -322,6 +357,12 @@ proto.adjustLayout = function(geoLayout, graphSize) {
322357
height: geoLayout._height
323358
})
324359
.call(Color.fill, geoLayout.bgcolor);
360+
361+
this.xaxis._offset = left;
362+
this.xaxis._length = geoLayout._width;
363+
364+
this.yaxis._offset = top;
365+
this.yaxis._length = geoLayout._height;
325366
};
326367

327368
proto.drawTopo = function(selection, layerName, geoLayout) {
@@ -445,27 +486,36 @@ proto.styleLayout = function(geoLayout) {
445486
}
446487
};
447488

489+
proto.isLonLatOverEdges = function(lonlat) {
490+
var clipAngle = this.clipAngle;
491+
492+
if(clipAngle === null) return false;
493+
494+
var p = this.projection.rotate(),
495+
angle = d3.geo.distance(lonlat, [-p[0], -p[1]]),
496+
maxAngle = clipAngle * Math.PI / 180;
497+
498+
return angle > maxAngle;
499+
};
500+
448501
// [hot code path] (re)draw all paths which depend on the projection
449502
proto.render = function() {
450-
var framework = this.framework,
503+
var _this = this,
504+
framework = _this.framework,
451505
gChoropleth = framework.select('g.choroplethlayer'),
452506
gScatterGeo = framework.select('g.scattergeolayer'),
453-
projection = this.projection,
454-
path = this.path,
455-
clipAngle = this.clipAngle;
507+
path = _this.path;
456508

457509
function translatePoints(d) {
458-
var lonlat = projection([d.lon, d.lat]);
459-
if(!lonlat) return null;
460-
return 'translate(' + lonlat[0] + ',' + lonlat[1] + ')';
510+
var lonlatPx = _this.projection(d.lonlat);
511+
if(!lonlatPx) return null;
512+
513+
return 'translate(' + lonlatPx[0] + ',' + lonlatPx[1] + ')';
461514
}
462515

463516
// hide paths over edges of clipped projections
464517
function hideShowPoints(d) {
465-
var p = projection.rotate(),
466-
angle = d3.geo.distance([d.lon, d.lat], [-p[0], -p[1]]),
467-
maxAngle = clipAngle * Math.PI / 180;
468-
return (angle > maxAngle) ? '0' : '1.0';
518+
return _this.isLonLatOverEdges(d.lonlat) ? '0' : '1.0';
469519
}
470520

471521
framework.selectAll('path.basepath').attr('d', path);
@@ -476,7 +526,7 @@ proto.render = function() {
476526

477527
gScatterGeo.selectAll('path.js-line').attr('d', path);
478528

479-
if(clipAngle !== null) {
529+
if(_this.clipAngle !== null) {
480530
gScatterGeo.selectAll('path.point')
481531
.style('opacity', hideShowPoints)
482532
.attr('transform', translatePoints);

src/traces/scattergeo/event_data.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
13+
module.exports = function eventData(out, pt) {
14+
out.lon = pt.lon;
15+
out.lat = pt.lat;
16+
out.location = pt.lon ? pt.lon : null;
17+
18+
return out;
19+
};

src/traces/scattergeo/hover.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var Fx = require('../../plots/cartesian/graph_interact');
13+
var Axes = require('../../plots/cartesian/axes');
14+
15+
var getTraceColor = require('../scatter/get_trace_color');
16+
var attributes = require('./attributes');
17+
18+
19+
module.exports = function hoverPoints(pointData) {
20+
var cd = pointData.cd,
21+
trace = cd[0].trace,
22+
xa = pointData.xa,
23+
ya = pointData.ya,
24+
geo = pointData.subplot;
25+
26+
if(cd[0].placeholder) return;
27+
28+
function c2p(lonlat) {
29+
return geo.projection(lonlat);
30+
}
31+
32+
function distFn(d) {
33+
var lonlat = d.lonlat;
34+
35+
// this handles the not-found location feature case
36+
if(lonlat[0] === null || lonlat[1] === null) return Infinity;
37+
38+
if(geo.isLonLatOverEdges(lonlat)) return Infinity;
39+
40+
var pos = c2p(lonlat);
41+
42+
var xPx = xa.c2p(),
43+
yPx = ya.c2p();
44+
45+
var dx = Math.abs(xPx - pos[0]),
46+
dy = Math.abs(yPx - pos[1]),
47+
rad = Math.max(3, d.mrc || 0);
48+
49+
return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad);
50+
}
51+
52+
Fx.getClosest(cd, distFn, pointData);
53+
54+
// skip the rest (for this trace) if we didn't find a close point
55+
if(pointData.index === false) return;
56+
57+
var di = cd[pointData.index],
58+
lonlat = di.lonlat,
59+
pos = c2p(lonlat),
60+
rad = di.mrc || 1;
61+
62+
pointData.x0 = pos[0] - rad;
63+
pointData.x1 = pos[0] + rad;
64+
pointData.y0 = pos[1] - rad;
65+
pointData.y1 = pos[1] + rad;
66+
67+
pointData.loc = di.loc;
68+
pointData.lat = lonlat[0];
69+
pointData.lon = lonlat[1];
70+
71+
pointData.color = getTraceColor(trace, di);
72+
pointData.extraText = getExtraText(trace, di, geo.mockAxis);
73+
74+
return [pointData];
75+
};
76+
77+
function getExtraText(trace, pt, axis) {
78+
var hoverinfo = trace.hoverinfo;
79+
80+
var parts = (hoverinfo === 'all') ?
81+
attributes.hoverinfo.flags :
82+
hoverinfo.split('+');
83+
84+
var hasLocation = parts.indexOf('location') !== -1 && Array.isArray(trace.locations),
85+
hasLon = (parts.indexOf('lon') !== -1),
86+
hasLat = (parts.indexOf('lat') !== -1),
87+
hasText = (parts.indexOf('text') !== -1);
88+
89+
var text = [];
90+
91+
function format(val) {
92+
return Axes.tickText(axis, axis.c2l(val), 'hover').text + '\u00B0';
93+
}
94+
95+
if(hasLocation) text.push(pt.loc);
96+
else if(hasLon && hasLat) {
97+
text.push('(' + format(pt.lonlat[0]) + ', ' + format(pt.lonlat[1]) + ')');
98+
}
99+
else if(hasLon) text.push('lon: ' + format(pt.lonlat[0]));
100+
else if(hasLat) text.push('lat: ' + format(pt.lonlat[1]));
101+
102+
if(hasText) text.push(pt.tx || trace.text);
103+
104+
return text.join('<br>');
105+
}

src/traces/scattergeo/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ ScatterGeo.supplyDefaults = require('./defaults');
1616
ScatterGeo.colorbar = require('../scatter/colorbar');
1717
ScatterGeo.calc = require('./calc');
1818
ScatterGeo.plot = require('./plot');
19+
ScatterGeo.hoverPoints = require('./hover');
20+
ScatterGeo.eventData = require('./event_data');
1921

2022
ScatterGeo.moduleType = 'trace';
2123
ScatterGeo.name = 'scattergeo';

0 commit comments

Comments
 (0)