Skip to content

Commit 34eb326

Browse files
authored
Merge pull request #1401 from plotly/choropleth-hover-better
Better hover for choropleth traces
2 parents 9dfc929 + c32d445 commit 34eb326

30 files changed

+195
-270
lines changed

src/plot_api/plot_api.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,11 +2829,6 @@ function makePlotFramework(gd) {
28292829
fullLayout._glcontainer.enter().append('div')
28302830
.classed('gl-container', true);
28312831

2832-
fullLayout._geocontainer = fullLayout._paperdiv.selectAll('.geo-container')
2833-
.data([0]);
2834-
fullLayout._geocontainer.enter().append('div')
2835-
.classed('geo-container', true);
2836-
28372832
fullLayout._paperdiv.selectAll('.main-svg').remove();
28382833

28392834
fullLayout._paper = fullLayout._paperdiv.insert('svg', ':first-child')
@@ -2877,6 +2872,9 @@ function makePlotFramework(gd) {
28772872
// single ternary layer for the whole plot
28782873
fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true);
28792874

2875+
// single geo layer for the whole plot
2876+
fullLayout._geolayer = fullLayout._paper.append('g').classed('geolayer', true);
2877+
28802878
// upper shape layer
28812879
// (only for shapes to be drawn above the whole plot, including subplots)
28822880
var layerAbove = fullLayout._paper.append('g')
@@ -2891,7 +2889,6 @@ function makePlotFramework(gd) {
28912889

28922890
// fill in image server scrape-svg
28932891
fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true);
2894-
fullLayout._geoimages = fullLayout._paper.append('g').classed('geoimages', true);
28952892

28962893
// lastly info (legend, annotations) and hover layers go on top
28972894
// these are in a different svg element normally, but get collapsed into a single

src/plot_api/subroutines.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,6 @@ exports.doModeBar = function(gd) {
306306
// no need to do this for gl2d subplots,
307307
// Plots.linkSubplots takes care of it all.
308308

309-
subplotIds = Plots.getSubplotIds(fullLayout, 'geo');
310-
for(i = 0; i < subplotIds.length; i++) {
311-
var geo = fullLayout[subplotIds[i]]._subplot;
312-
geo.updateFx(fullLayout.hovermode);
313-
}
314-
315309
return Plots.previousPromises(gd);
316310
};
317311

src/plots/cartesian/graph_interact.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,8 @@ function createHoverText(hoverData, opts) {
977977
contrastColor = tinycolor(traceColor).getBrightness() > 128 ?
978978
'#000' : Color.background;
979979

980+
// to get custom 'name' labels pass cleanPoint
981+
if(d.nameOverride !== undefined) d.name = d.nameOverride;
980982

981983
if(d.name && d.zLabelVal === undefined) {
982984
// strip out our pseudo-html elements from d.name (if it exists at all)

src/plots/geo/geo.js

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,19 @@ var createGeoZoom = require('./zoom');
2525
var createGeoZoomReset = require('./zoom_reset');
2626
var constants = require('./constants');
2727

28-
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
2928
var topojsonUtils = require('../../lib/topojson_utils');
3029
var topojsonFeature = require('topojson-client').feature;
3130

3231
// add a few projection types to d3.geo
3332
addProjectionsToD3(d3);
3433

3534

36-
function Geo(options, fullLayout) {
35+
function Geo(options) {
3736
this.id = options.id;
3837
this.graphDiv = options.graphDiv;
3938
this.container = options.container;
4039
this.topojsonURL = options.topojsonURL;
4140

42-
this.hoverContainer = null;
43-
4441
this.topojsonName = null;
4542
this.topojson = null;
4643

@@ -54,11 +51,7 @@ function Geo(options, fullLayout) {
5451
this.zoom = null;
5552
this.zoomReset = null;
5653

57-
this.xaxis = null;
58-
this.yaxis = null;
59-
6054
this.makeFramework();
61-
this.updateFx(fullLayout.hovermode);
6255

6356
this.traceHash = {};
6457
}
@@ -178,15 +171,6 @@ proto.onceTopojsonIsLoaded = function(geoCalcData, geoLayout) {
178171
this.render();
179172
};
180173

181-
proto.updateFx = function(hovermode) {
182-
this.showHover = (hovermode !== false);
183-
184-
// TODO should more strict, any layout.hovermode other
185-
// then false will make all geo subplot display hover text.
186-
// Instead each geo should have its own geo.hovermode
187-
// to control hover visibility independently of other subplots.
188-
};
189-
190174
proto.makeProjection = function(geoLayout) {
191175
var projLayout = geoLayout.projection,
192176
projType = projLayout.type,
@@ -232,38 +216,30 @@ proto.makePath = function() {
232216
this.path = d3.geo.path().projection(this.projection);
233217
};
234218

235-
/*
236-
* <div this.container>
237-
* <div this.geoDiv>
238-
* <svg this.hoverContainer>
239-
* <svg this.framework>
240-
*/
241219
proto.makeFramework = function() {
242-
var geoDiv = this.geoDiv = d3.select(this.container).append('div');
243-
geoDiv
244-
.attr('id', this.id)
245-
.style('position', 'absolute');
246-
247-
// only choropleth traces use this,
248-
// scattergeo traces use Fx.hover and fullLayout._hoverlayer
249-
var hoverContainer = this.hoverContainer = geoDiv.append('svg');
250-
hoverContainer
251-
.attr(xmlnsNamespaces.svgAttrs)
252-
.style({
253-
'position': 'absolute',
254-
'z-index': 20,
255-
'pointer-events': 'none'
256-
});
257-
258-
var framework = this.framework = geoDiv.append('svg');
220+
var fullLayout = this.graphDiv._fullLayout;
221+
var clipId = 'clip' + fullLayout._uid + this.id;
222+
223+
var defGroup = fullLayout._defs.selectAll('g.clips')
224+
.data([0]);
225+
defGroup.enter().append('g')
226+
.classed('clips', true);
227+
228+
var clipDef = this.clipDef = defGroup.selectAll('#' + clipId)
229+
.data([0]);
230+
231+
clipDef.enter().append('clipPath').attr('id', clipId)
232+
.append('rect');
233+
234+
var framework = this.framework = d3.select(this.container).append('g');
235+
259236
framework
260-
.attr(xmlnsNamespaces.svgAttrs)
261-
.attr({
262-
'position': 'absolute',
263-
'preserveAspectRatio': 'none'
264-
});
237+
.attr('class', 'geo ' + this.id)
238+
.style('pointer-events', 'all')
239+
.call(Drawing.setClipUrl, clipId);
265240

266-
framework.append('g').attr('class', 'bglayer')
241+
framework.append('g')
242+
.attr('class', 'bglayer')
267243
.append('rect');
268244

269245
framework.append('g').attr('class', 'baselayer');
@@ -274,8 +250,6 @@ proto.makeFramework = function() {
274250
// N.B. disable dblclick zoom default
275251
framework.on('dblclick.zoom', null);
276252

277-
// TODO use clip paths instead of nested SVG
278-
279253
this.xaxis = { _id: 'x' };
280254
this.yaxis = { _id: 'y' };
281255
};
@@ -286,28 +260,20 @@ proto.adjustLayout = function(geoLayout, graphSize) {
286260
var left = graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX,
287261
top = graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY;
288262

289-
this.geoDiv.style({
290-
left: left + 'px',
291-
top: top + 'px',
292-
width: geoLayout._width + 'px',
293-
height: geoLayout._height + 'px'
294-
});
263+
Drawing.setTranslate(this.framework, left, top);
295264

296-
this.hoverContainer.attr({
265+
var dimsAttrs = {
266+
x: 0,
267+
y: 0,
297268
width: geoLayout._width,
298269
height: geoLayout._height
299-
});
270+
};
300271

301-
this.framework.attr({
302-
width: geoLayout._width,
303-
height: geoLayout._height
304-
});
272+
this.clipDef.select('rect')
273+
.attr(dimsAttrs);
305274

306275
this.framework.select('.bglayer').select('rect')
307-
.attr({
308-
width: geoLayout._width,
309-
height: geoLayout._height
310-
})
276+
.attr(dimsAttrs)
311277
.call(Color.fill, geoLayout.bgcolor);
312278

313279
this.xaxis._offset = left;

src/plots/geo/index.js

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,13 @@ exports.plot = function plotGeo(gd) {
4848
geoCalcData = Plots.getSubplotCalcData(calcData, 'geo', geoId),
4949
geo = fullLayout[geoId]._subplot;
5050

51-
// If geo is not instantiated, create one!
5251
if(!geo) {
5352
geo = new Geo({
5453
id: geoId,
5554
graphDiv: gd,
56-
container: fullLayout._geocontainer.node(),
55+
container: fullLayout._geolayer.node(),
5756
topojsonURL: gd._context.topojsonURL
58-
},
59-
fullLayout
60-
);
57+
});
6158

6259
fullLayout[geoId]._subplot = geo;
6360
}
@@ -74,31 +71,8 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
7471
var oldGeo = oldFullLayout[oldGeoKey]._subplot;
7572

7673
if(!newFullLayout[oldGeoKey] && !!oldGeo) {
77-
oldGeo.geoDiv.remove();
74+
oldGeo.framework.remove();
75+
oldGeo.clipDef.remove();
7876
}
7977
}
8078
};
81-
82-
exports.toSVG = function(gd) {
83-
var fullLayout = gd._fullLayout,
84-
geoIds = Plots.getSubplotIds(fullLayout, 'geo'),
85-
size = fullLayout._size;
86-
87-
for(var i = 0; i < geoIds.length; i++) {
88-
var geoLayout = fullLayout[geoIds[i]],
89-
domain = geoLayout.domain,
90-
geoFramework = geoLayout._subplot.framework;
91-
92-
geoFramework.attr('style', null);
93-
geoFramework
94-
.attr({
95-
x: size.l + size.w * domain.x[0] + geoLayout._marginX,
96-
y: size.t + size.h * (1 - domain.y[1]) + geoLayout._marginY,
97-
width: geoLayout._width,
98-
height: geoLayout._height
99-
});
100-
101-
fullLayout._geoimages.node()
102-
.appendChild(geoFramework.node());
103-
}
104-
};

src/plots/geo/zoom_reset.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99

1010
'use strict';
1111

12-
var Fx = require('../cartesian/graph_interact');
13-
14-
function createGeoZoomReset(geo, geoLayout) {
12+
module.exports = function createGeoZoomReset(geo, geoLayout) {
1513
var projection = geo.projection,
1614
zoom = geo.zoom;
1715

@@ -22,12 +20,8 @@ function createGeoZoomReset(geo, geoLayout) {
2220
zoom.scale(projection.scale());
2321
zoom.translate(projection.translate());
2422

25-
Fx.loneUnhover(geo.hoverContainer);
26-
2723
geo.render();
2824
};
2925

3026
return zoomReset;
31-
}
32-
33-
module.exports = createGeoZoomReset;
27+
};

src/traces/choropleth/event_data.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright 2012-2017, 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+
module.exports = function eventData(out, pt) {
13+
out.location = pt.location;
14+
out.z = pt.z;
15+
16+
return out;
17+
};

src/traces/choropleth/hover.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright 2012-2017, 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 Axes = require('../../plots/cartesian/axes');
13+
var attributes = require('./attributes');
14+
15+
module.exports = function hoverPoints(pointData) {
16+
var cd = pointData.cd;
17+
var trace = cd[0].trace;
18+
var geo = pointData.subplot;
19+
20+
// set on choropleth paths 'mouseover'
21+
var pt = geo.choroplethHoverPt;
22+
23+
if(!pt) return;
24+
25+
var centroid = geo.projection(pt.properties.ct);
26+
27+
pointData.x0 = pointData.x1 = centroid[0];
28+
pointData.y0 = pointData.y1 = centroid[1];
29+
30+
pointData.index = pt.index;
31+
pointData.location = pt.id;
32+
pointData.z = pt.z;
33+
34+
makeHoverInfo(pointData, trace, pt, geo.mockAxis);
35+
36+
return [pointData];
37+
};
38+
39+
function makeHoverInfo(pointData, trace, pt, axis) {
40+
var hoverinfo = trace.hoverinfo;
41+
42+
var parts = (hoverinfo === 'all') ?
43+
attributes.hoverinfo.flags :
44+
hoverinfo.split('+');
45+
46+
var hasName = (parts.indexOf('name') !== -1),
47+
hasLocation = (parts.indexOf('location') !== -1),
48+
hasZ = (parts.indexOf('z') !== -1),
49+
hasText = (parts.indexOf('text') !== -1),
50+
hasIdAsNameLabel = !hasName && hasLocation;
51+
52+
var text = [];
53+
54+
function formatter(val) {
55+
return Axes.tickText(axis, axis.c2l(val), 'hover').text;
56+
}
57+
58+
if(hasIdAsNameLabel) pointData.nameOverride = pt.id;
59+
else {
60+
if(hasName) pointData.nameOverride = trace.name;
61+
if(hasLocation) text.push(pt.id);
62+
}
63+
64+
if(hasZ) text.push(formatter(pt.z));
65+
if(hasText) text.push(pt.tx);
66+
67+
pointData.extraText = text.join('<br>');
68+
}

src/traces/choropleth/index.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ Choropleth.attributes = require('./attributes');
1515
Choropleth.supplyDefaults = require('./defaults');
1616
Choropleth.colorbar = require('../heatmap/colorbar');
1717
Choropleth.calc = require('./calc');
18-
Choropleth.plot = require('./plot').plot;
19-
20-
// add dummy hover handler to skip Fx.hover w/o warnings
21-
Choropleth.hoverPoints = function() {};
18+
Choropleth.plot = require('./plot');
19+
Choropleth.hoverPoints = require('./hover');
20+
Choropleth.eventData = require('./event_data');
2221

2322
Choropleth.moduleType = 'trace';
2423
Choropleth.name = 'choropleth';

0 commit comments

Comments
 (0)