Skip to content

Commit a166761

Browse files
committed
improve mapbox hover after drag
- use _rehover wrapper similar to cartesian subplots and call it on moveend - split fx init code into prototype.initFx
1 parent 4e88075 commit a166761

File tree

2 files changed

+208
-117
lines changed

2 files changed

+208
-117
lines changed

src/plots/mapbox/mapbox.js

Lines changed: 142 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ proto.plot = function(calcData, fullLayout, promises) {
8383

8484
proto.createMap = function(calcData, fullLayout, resolve, reject) {
8585
var self = this;
86-
var gd = self.gd;
8786
var opts = fullLayout[self.id];
8887

8988
// store style id and URL or object
@@ -115,6 +114,10 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
115114

116115
self.rejectOnError(reject);
117116

117+
if(!self.isStatic) {
118+
self.initFx(calcData, fullLayout);
119+
}
120+
118121
var promises = [];
119122

120123
promises.push(new Promise(function(resolve) {
@@ -127,121 +130,7 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
127130
self.updateData(calcData);
128131
self.updateLayout(fullLayout);
129132
self.resolveOnRender(resolve);
130-
});
131-
132-
if(self.isStatic) return;
133-
134-
var wheeling = false;
135-
136-
// keep track of pan / zoom in user layout and emit relayout event
137-
map.on('moveend', function(eventData) {
138-
if(!self.map) return;
139-
140-
// 'moveend' gets triggered by map.setCenter, map.setZoom,
141-
// map.setBearing and map.setPitch.
142-
//
143-
// Here, we make sure that state updates amd 'plotly_relayout'
144-
// are triggered only when the 'moveend' originates from a
145-
// mouse target (filtering out API calls) to not
146-
// duplicate 'plotly_relayout' events.
147-
148-
if(eventData.originalEvent || wheeling) {
149-
var optsNow = gd._fullLayout[self.id];
150-
Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));
151-
152-
var viewNow = self.getView();
153-
optsNow._input.center = optsNow.center = viewNow.center;
154-
optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
155-
optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
156-
optsNow._input.pitch = optsNow.pitch = viewNow.pitch;
157-
158-
gd.emit('plotly_relayout', self.getViewEdits(viewNow));
159-
}
160-
wheeling = false;
161-
});
162-
163-
map.on('wheel', function() {
164-
wheeling = true;
165-
});
166-
167-
map.on('mousemove', function(evt) {
168-
var bb = self.div.getBoundingClientRect();
169-
170-
// some hackery to get Fx.hover to work
171-
evt.clientX = evt.point.x + bb.left;
172-
evt.clientY = evt.point.y + bb.top;
173-
174-
evt.target.getBoundingClientRect = function() { return bb; };
175-
176-
self.xaxis.p2c = function() { return evt.lngLat.lng; };
177-
self.yaxis.p2c = function() { return evt.lngLat.lat; };
178-
179-
Fx.hover(gd, evt, self.id);
180-
});
181-
182-
function unhover() {
183-
Fx.loneUnhover(fullLayout._toppaper);
184-
}
185-
186-
map.on('dragstart', unhover);
187-
map.on('zoomstart', unhover);
188-
189-
function emitUpdate() {
190-
var viewNow = self.getView();
191-
gd.emit('plotly_relayouting', self.getViewEdits(viewNow));
192-
}
193-
194-
map.on('drag', emitUpdate);
195-
map.on('zoom', emitUpdate);
196-
197-
map.on('dblclick', function() {
198-
var optsNow = gd._fullLayout[self.id];
199-
Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));
200-
201-
var viewInitial = self.viewInitial;
202-
map.setCenter(convertCenter(viewInitial.center));
203-
map.setZoom(viewInitial.zoom);
204-
map.setBearing(viewInitial.bearing);
205-
map.setPitch(viewInitial.pitch);
206-
207-
var viewNow = self.getView();
208-
optsNow._input.center = optsNow.center = viewNow.center;
209-
optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
210-
optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
211-
optsNow._input.pitch = optsNow.pitch = viewNow.pitch;
212-
213-
gd.emit('plotly_doubleclick', null);
214-
gd.emit('plotly_relayout', self.getViewEdits(viewNow));
215-
});
216-
217-
// define event handlers on map creation, to keep one ref per map,
218-
// so that map.on / map.off in updateFx works as expected
219-
self.clearSelect = function() {
220-
gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
221-
};
222-
223-
/**
224-
* Returns a click handler function that is supposed
225-
* to handle clicks in pan mode.
226-
*/
227-
self.onClickInPanFn = function(dragOptions) {
228-
return function(evt) {
229-
var clickMode = gd._fullLayout.clickmode;
230-
231-
if(clickMode.indexOf('select') > -1) {
232-
selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
233-
}
234-
235-
if(clickMode.indexOf('event') > -1) {
236-
// TODO: this does not support right-click. If we want to support it, we
237-
// would likely need to change mapbox to use dragElement instead of straight
238-
// mapbox event binding. Or perhaps better, make a simple wrapper with the
239-
// right mousedown, mousemove, and mouseup handlers just for a left/right click
240-
// pie would use this too.
241-
Fx.click(gd, evt.originalEvent);
242-
}
243-
};
244-
};
133+
}).catch(reject);
245134
};
246135

247136
proto.fetchMapData = function(calcData) {
@@ -251,6 +140,7 @@ proto.fetchMapData = function(calcData) {
251140
return new Promise(function(resolve, reject) {
252141
d3.json(url, function(err, d) {
253142
if(err) {
143+
delete PlotlyGeoAssets[url];
254144
var msg = err.status === 404 ?
255145
('GeoJSON at URL ' + url + ' does not exist.') :
256146
('Unexpected error while fetching from ' + url);
@@ -305,7 +195,7 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) {
305195
self.updateData(calcData);
306196
self.updateLayout(fullLayout);
307197
self.resolveOnRender(resolve);
308-
});
198+
}).catch(reject);
309199
};
310200

311201
var traceType2orderIndex = {
@@ -431,6 +321,141 @@ proto.createFramework = function(fullLayout) {
431321
Axes.setConvert(self.mockAxis, fullLayout);
432322
};
433323

324+
proto.initFx = function(calcData, fullLayout) {
325+
var self = this;
326+
var gd = self.gd;
327+
var map = self.map;
328+
329+
var wheeling = false;
330+
331+
// keep track of pan / zoom in user layout and emit relayout event
332+
map.on('moveend', function(evt) {
333+
if(!self.map) return;
334+
335+
var fullLayoutNow = gd._fullLayout;
336+
337+
// 'moveend' gets triggered by map.setCenter, map.setZoom,
338+
// map.setBearing and map.setPitch.
339+
//
340+
// Here, we make sure that state updates amd 'plotly_relayout'
341+
// are triggered only when the 'moveend' originates from a
342+
// mouse target (filtering out API calls) to not
343+
// duplicate 'plotly_relayout' events.
344+
345+
if(evt.originalEvent || wheeling) {
346+
var optsNow = fullLayoutNow[self.id];
347+
Registry.call('_storeDirectGUIEdit', gd.layout, fullLayoutNow._preGUI, self.getViewEdits(optsNow));
348+
349+
var viewNow = self.getView();
350+
optsNow._input.center = optsNow.center = viewNow.center;
351+
optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
352+
optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
353+
optsNow._input.pitch = optsNow.pitch = viewNow.pitch;
354+
355+
gd.emit('plotly_relayout', self.getViewEdits(viewNow));
356+
}
357+
wheeling = false;
358+
359+
if(fullLayoutNow._rehover) {
360+
fullLayoutNow._rehover();
361+
}
362+
});
363+
364+
map.on('wheel', function() {
365+
wheeling = true;
366+
});
367+
368+
map.on('mousemove', function(evt) {
369+
var bb = self.div.getBoundingClientRect();
370+
371+
// some hackery to get Fx.hover to work
372+
evt.clientX = evt.point.x + bb.left;
373+
evt.clientY = evt.point.y + bb.top;
374+
375+
evt.target.getBoundingClientRect = function() { return bb; };
376+
377+
self.xaxis.p2c = function() { return evt.lngLat.lng; };
378+
self.yaxis.p2c = function() { return evt.lngLat.lat; };
379+
380+
gd._fullLayout._rehover = function() {
381+
if(gd._fullLayout._hoversubplot === self.id) {
382+
Fx.hover(gd, evt, self.id);
383+
}
384+
};
385+
386+
Fx.hover(gd, evt, self.id);
387+
gd._fullLayout._hoversubplot = self.id;
388+
});
389+
390+
function unhover() {
391+
Fx.loneUnhover(fullLayout._hoverlayer);
392+
}
393+
394+
map.on('dragstart', unhover);
395+
map.on('zoomstart', unhover);
396+
397+
map.on('mouseout', function() {
398+
gd._fullLayout._hoversubplot = null;
399+
});
400+
401+
function emitUpdate() {
402+
var viewNow = self.getView();
403+
gd.emit('plotly_relayouting', self.getViewEdits(viewNow));
404+
}
405+
406+
map.on('drag', emitUpdate);
407+
map.on('zoom', emitUpdate);
408+
409+
map.on('dblclick', function() {
410+
var optsNow = gd._fullLayout[self.id];
411+
Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));
412+
413+
var viewInitial = self.viewInitial;
414+
map.setCenter(convertCenter(viewInitial.center));
415+
map.setZoom(viewInitial.zoom);
416+
map.setBearing(viewInitial.bearing);
417+
map.setPitch(viewInitial.pitch);
418+
419+
var viewNow = self.getView();
420+
optsNow._input.center = optsNow.center = viewNow.center;
421+
optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
422+
optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
423+
optsNow._input.pitch = optsNow.pitch = viewNow.pitch;
424+
425+
gd.emit('plotly_doubleclick', null);
426+
gd.emit('plotly_relayout', self.getViewEdits(viewNow));
427+
});
428+
429+
// define event handlers on map creation, to keep one ref per map,
430+
// so that map.on / map.off in updateFx works as expected
431+
self.clearSelect = function() {
432+
gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
433+
};
434+
435+
/**
436+
* Returns a click handler function that is supposed
437+
* to handle clicks in pan mode.
438+
*/
439+
self.onClickInPanFn = function(dragOptions) {
440+
return function(evt) {
441+
var clickMode = gd._fullLayout.clickmode;
442+
443+
if(clickMode.indexOf('select') > -1) {
444+
selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
445+
}
446+
447+
if(clickMode.indexOf('event') > -1) {
448+
// TODO: this does not support right-click. If we want to support it, we
449+
// would likely need to change mapbox to use dragElement instead of straight
450+
// mapbox event binding. Or perhaps better, make a simple wrapper with the
451+
// right mousedown, mousemove, and mouseup handlers just for a left/right click
452+
// pie would use this too.
453+
Fx.click(gd, evt.originalEvent);
454+
}
455+
};
456+
};
457+
};
458+
434459
proto.updateFx = function(fullLayout) {
435460
var self = this;
436461
var map = self.map;

test/jasmine/tests/mapbox_test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,72 @@ describe('@noCI, mapbox plots', function() {
12921292
}
12931293
});
12941294

1295+
describe('@noCI Test mapbox GeoJSON fetching:', function() {
1296+
var gd;
1297+
1298+
beforeEach(function() {
1299+
gd = createGraphDiv();
1300+
});
1301+
1302+
afterEach(function(done) {
1303+
Plotly.purge(gd);
1304+
destroyGraphDiv();
1305+
setTimeout(done, 200);
1306+
});
1307+
1308+
it('@gl should fetch GeoJSON using URLs found in the traces', function(done) {
1309+
var url = 'https://raw.githubusercontent.com/plotly/datasets/master/florida-red-data.json';
1310+
var url2 = 'https://raw.githubusercontent.com/plotly/datasets/master/florida-blue-data.json';
1311+
var cnt = 0;
1312+
1313+
Plotly.plot(gd, [{
1314+
type: 'choroplethmapbox',
1315+
locations: ['a'],
1316+
z: [1],
1317+
geojson: url
1318+
}, {
1319+
type: 'choroplethmapbox',
1320+
locations: ['a'],
1321+
z: [1],
1322+
geojson: url2
1323+
}])
1324+
.catch(function() {
1325+
cnt++;
1326+
})
1327+
.then(function() {
1328+
expect(cnt).toBe(0, 'no failures!');
1329+
expect(Lib.isPlainObject(window.PlotlyGeoAssets[url])).toBe(true, 'is a GeoJSON object');
1330+
expect(Lib.isPlainObject(window.PlotlyGeoAssets[url2])).toBe(true, 'is a GeoJSON object');
1331+
})
1332+
.then(done);
1333+
});
1334+
1335+
it('@gl should fetch GeoJSON using URLs found in the traces', function(done) {
1336+
var actual = '';
1337+
1338+
Plotly.plot(gd, [{
1339+
type: 'choroplethmapbox',
1340+
locations: ['a'],
1341+
z: [1],
1342+
geojson: 'invalidUrl'
1343+
}, {
1344+
type: 'choroplethmapbox',
1345+
locations: ['a'],
1346+
z: [1],
1347+
geojson: 'invalidUrl-two'
1348+
}])
1349+
.catch(function(reason) {
1350+
// bails up after first failure
1351+
actual = reason;
1352+
})
1353+
.then(function() {
1354+
expect(actual).toEqual(new Error('GeoJSON at URL invalidUrl does not exist.'));
1355+
expect(window.PlotlyGeoAssets.invalidUrl).toBe(undefined);
1356+
})
1357+
.then(done);
1358+
}, LONG_TIMEOUT_INTERVAL);
1359+
});
1360+
12951361
describe('@noCI, mapbox toImage', function() {
12961362
// decreased from 1e5 - perhaps chrome got better at encoding these
12971363
// because I get 99330 and the image still looks correct

0 commit comments

Comments
 (0)