Skip to content

Commit 567ba90

Browse files
authored
Merge pull request #673 from plotly/hoveron-fill
Hover on fills
2 parents e8b9ca2 + 1e86eb2 commit 567ba90

18 files changed

+390
-115
lines changed

src/lib/coerce.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ exports.valObjects = {
192192
propOut.set(dflt);
193193
return;
194194
}
195-
if(opts.extras.indexOf(v) !== -1) {
195+
if((opts.extras || []).indexOf(v) !== -1) {
196196
propOut.set(v);
197197
return;
198198
}

src/traces/scatter/attributes.js

+11
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ module.exports = {
9292
'then the default is *lines+markers*. Otherwise, *lines*.'
9393
].join(' ')
9494
},
95+
hoveron: {
96+
valType: 'flaglist',
97+
flags: ['points', 'fills'],
98+
role: 'info',
99+
description: [
100+
'Do the hover effects highlight individual points (markers or',
101+
'line points) or do they highlight filled regions?',
102+
'If the fill is *toself* or *tonext* and there are no markers',
103+
'or text, then the default is *fills*, otherwise it is *points*.'
104+
].join(' ')
105+
},
95106
line: {
96107
color: {
97108
valType: 'color',

src/traces/scatter/defaults.js

+8
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
5353
handleTextDefaults(traceIn, traceOut, layout, coerce);
5454
}
5555

56+
var dfltHoverOn = [];
57+
5658
if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) {
5759
coerce('marker.maxdisplayed');
60+
dfltHoverOn.push('points');
5861
}
5962

6063
coerce('fill');
@@ -63,6 +66,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
6366
if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce);
6467
}
6568

69+
if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') {
70+
dfltHoverOn.push('fills');
71+
}
72+
coerce('hoveron', dfltHoverOn.join('+') || 'points');
73+
6674
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'});
6775
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'});
6876
};

src/traces/scatter/hover.js

+136-47
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,150 @@
99

1010
'use strict';
1111

12+
var Lib = require('../../lib');
1213
var Fx = require('../../plots/cartesian/graph_interact');
14+
var constants = require('../../plots/cartesian/constants');
1315
var ErrorBars = require('../../components/errorbars');
1416
var getTraceColor = require('./get_trace_color');
17+
var Color = require('../../components/color');
1518

1619

1720
module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
1821
var cd = pointData.cd,
1922
trace = cd[0].trace,
2023
xa = pointData.xa,
2124
ya = pointData.ya,
22-
dx = function(di) {
23-
// scatter points: d.mrc is the calculated marker radius
24-
// adjust the distance so if you're inside the marker it
25-
// always will show up regardless of point size, but
26-
// prioritize smaller points
27-
var rad = Math.max(3, di.mrc || 0);
28-
return Math.max(Math.abs(xa.c2p(di.x) - xa.c2p(xval)) - rad, 1 - 3 / rad);
29-
},
30-
dy = function(di) {
31-
var rad = Math.max(3, di.mrc || 0);
32-
return Math.max(Math.abs(ya.c2p(di.y) - ya.c2p(yval)) - rad, 1 - 3 / rad);
33-
},
34-
dxy = function(di) {
35-
var rad = Math.max(3, di.mrc || 0),
36-
dx = Math.abs(xa.c2p(di.x) - xa.c2p(xval)),
37-
dy = Math.abs(ya.c2p(di.y) - ya.c2p(yval));
38-
return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad);
39-
},
40-
distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
41-
42-
Fx.getClosest(cd, distfn, pointData);
43-
44-
// skip the rest (for this trace) if we didn't find a close point
45-
if(pointData.index === false) return;
46-
47-
// the closest data point
48-
var di = cd[pointData.index],
49-
xc = xa.c2p(di.x, true),
50-
yc = ya.c2p(di.y, true),
51-
rad = di.mrc || 1;
52-
53-
pointData.color = getTraceColor(trace, di);
54-
55-
pointData.x0 = xc - rad;
56-
pointData.x1 = xc + rad;
57-
pointData.xLabelVal = di.x;
58-
59-
pointData.y0 = yc - rad;
60-
pointData.y1 = yc + rad;
61-
pointData.yLabelVal = di.y;
62-
63-
if(di.tx) pointData.text = di.tx;
64-
else if(trace.text) pointData.text = trace.text;
65-
66-
ErrorBars.hoverInfo(di, trace, pointData);
67-
68-
return [pointData];
25+
xpx = xa.c2p(xval),
26+
ypx = ya.c2p(yval),
27+
pt = [xpx, ypx];
28+
29+
// look for points to hover on first, then take fills only if we
30+
// didn't find a point
31+
if(trace.hoveron.indexOf('points') !== -1) {
32+
var dx = function(di) {
33+
// scatter points: d.mrc is the calculated marker radius
34+
// adjust the distance so if you're inside the marker it
35+
// always will show up regardless of point size, but
36+
// prioritize smaller points
37+
var rad = Math.max(3, di.mrc || 0);
38+
return Math.max(Math.abs(xa.c2p(di.x) - xpx) - rad, 1 - 3 / rad);
39+
},
40+
dy = function(di) {
41+
var rad = Math.max(3, di.mrc || 0);
42+
return Math.max(Math.abs(ya.c2p(di.y) - ypx) - rad, 1 - 3 / rad);
43+
},
44+
dxy = function(di) {
45+
var rad = Math.max(3, di.mrc || 0),
46+
dx = xa.c2p(di.x) - xpx,
47+
dy = ya.c2p(di.y) - ypx;
48+
return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad);
49+
},
50+
distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
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) {
56+
57+
// the closest data point
58+
var di = cd[pointData.index],
59+
xc = xa.c2p(di.x, true),
60+
yc = ya.c2p(di.y, true),
61+
rad = di.mrc || 1;
62+
63+
Lib.extendFlat(pointData, {
64+
color: getTraceColor(trace, di),
65+
66+
x0: xc - rad,
67+
x1: xc + rad,
68+
xLabelVal: di.x,
69+
70+
y0: yc - rad,
71+
y1: yc + rad,
72+
yLabelVal: di.y
73+
});
74+
75+
if(di.tx) pointData.text = di.tx;
76+
else if(trace.text) pointData.text = trace.text;
77+
78+
ErrorBars.hoverInfo(di, trace, pointData);
79+
80+
return [pointData];
81+
}
82+
}
83+
84+
// even if hoveron is 'fills', only use it if we have polygons too
85+
if(trace.hoveron.indexOf('fills') !== -1 && trace._polygons) {
86+
var polygons = trace._polygons,
87+
polygonsIn = [],
88+
inside = false,
89+
xmin = Infinity,
90+
xmax = -Infinity,
91+
ymin = Infinity,
92+
ymax = -Infinity,
93+
i, j, polygon, pts, xCross, x0, x1, y0, y1;
94+
95+
for(i = 0; i < polygons.length; i++) {
96+
polygon = polygons[i];
97+
// TODO: this is not going to work right for curved edges, it will
98+
// act as though they're straight. That's probably going to need
99+
// the elements themselves to capture the events. Worth it?
100+
if(polygon.contains(pt)) {
101+
inside = !inside;
102+
// TODO: need better than just the overall bounding box
103+
polygonsIn.push(polygon);
104+
ymin = Math.min(ymin, polygon.ymin);
105+
ymax = Math.max(ymax, polygon.ymax);
106+
}
107+
}
108+
109+
if(inside) {
110+
// find the overall left-most and right-most points of the
111+
// polygon(s) we're inside at their combined vertical midpoint.
112+
// This is where we will draw the hover label.
113+
// Note that this might not be the vertical midpoint of the
114+
// whole trace, if it's disjoint.
115+
var yAvg = (ymin + ymax) / 2;
116+
for(i = 0; i < polygonsIn.length; i++) {
117+
pts = polygonsIn[i].pts;
118+
for(j = 1; j < pts.length; j++) {
119+
y0 = pts[j - 1][1];
120+
y1 = pts[j][1];
121+
if((y0 > yAvg) !== (y1 >= yAvg)) {
122+
x0 = pts[j - 1][0];
123+
x1 = pts[j][0];
124+
xCross = x0 + (x1 - x0) * (yAvg - y0) / (y1 - y0);
125+
xmin = Math.min(xmin, xCross);
126+
xmax = Math.max(xmax, xCross);
127+
}
128+
}
129+
}
130+
131+
// get only fill or line color for the hover color
132+
var color = Color.defaultLine;
133+
if(Color.opacity(trace.fillcolor)) color = trace.fillcolor;
134+
else if(Color.opacity((trace.line || {}).color)) {
135+
color = trace.line.color;
136+
}
137+
138+
Lib.extendFlat(pointData, {
139+
// never let a 2D override 1D type as closest point
140+
distance: constants.MAXDIST + 10,
141+
x0: xmin,
142+
x1: xmax,
143+
y0: yAvg,
144+
y1: yAvg,
145+
color: color
146+
});
147+
148+
delete pointData.index;
149+
150+
if(trace.text && !Array.isArray(trace.text)) {
151+
pointData.text = String(trace.text);
152+
}
153+
else pointData.text = trace.name;
154+
155+
return [pointData];
156+
}
157+
}
69158
};

src/traces/scatter/plot.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ var Lib = require('../../lib');
1515
var Drawing = require('../../components/drawing');
1616
var ErrorBars = require('../../components/errorbars');
1717

18+
var polygonTester = require('../../lib/polygon').tester;
19+
1820
var subTypes = require('./subtypes');
1921
var arraysToCalcdata = require('./arrays_to_calcdata');
2022
var linePoints = require('./line_points');
@@ -41,6 +43,7 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
4143

4244
// BUILD LINES AND FILLS
4345
var prevpath = '',
46+
prevPolygons = [],
4447
ownFillEl3, ownFillDir, tonext, nexttonext;
4548

4649
scattertraces.each(function(d) {
@@ -125,12 +128,23 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
125128
linear: line.shape === 'linear'
126129
});
127130

131+
// since we already have the pixel segments here, use them to make
132+
// polygons for hover on fill
133+
// TODO: can we skip this if hoveron!=fills? That would mean we
134+
// need to redraw when you change hoveron...
135+
var thisPolygons = trace._polygons = new Array(segments.length),
136+
i;
137+
138+
for(i = 0; i < segments.length; i++) {
139+
trace._polygons[i] = polygonTester(segments[i]);
140+
}
141+
128142
if(segments.length) {
129143
var pt0 = segments[0][0],
130144
lastSegment = segments[segments.length - 1],
131145
pt1 = lastSegment[lastSegment.length - 1];
132146

133-
for(var i = 0; i < segments.length; i++) {
147+
for(i = 0; i < segments.length; i++) {
134148
var pts = segments[i];
135149
thispath = pathfn(pts);
136150
thisrevpath = revpathfn(pts);
@@ -185,8 +199,10 @@ module.exports = function plot(gd, plotinfo, cdscatter) {
185199
// existing curve or off the end of it
186200
tonext.attr('d', fullpath + 'L' + prevpath.substr(1) + 'Z');
187201
}
202+
trace._polygons = trace._polygons.concat(prevPolygons);
188203
}
189204
prevpath = revpath;
205+
prevPolygons = thisPolygons;
190206
}
191207
});
192208

src/traces/scatterternary/attributes.js

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ module.exports = {
117117
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
118118
flags: ['a', 'b', 'c', 'text', 'name']
119119
}),
120+
hoveron: scatterAttrs.hoveron,
120121
_nestedModules: {
121122
'marker.colorbar': 'Colorbar'
122123
}

src/traces/scatterternary/defaults.js

+8
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
8181
handleTextDefaults(traceIn, traceOut, layout, coerce);
8282
}
8383

84+
var dfltHoverOn = [];
85+
8486
if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) {
8587
coerce('marker.maxdisplayed');
88+
dfltHoverOn.push('points');
8689
}
8790

8891
coerce('fill');
@@ -92,4 +95,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
9295
}
9396

9497
coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined);
98+
99+
if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') {
100+
dfltHoverOn.push('fills');
101+
}
102+
coerce('hoveron', dfltHoverOn.join('+') || 'points');
95103
};

src/traces/scatterternary/hover.js

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
1717
var scatterPointData = scatterHover(pointData, xval, yval, hovermode);
1818
if(!scatterPointData || scatterPointData[0].index === false) return;
1919

20+
// if hovering on a fill, we don't show any point data so the label is
21+
// unchanged from what scatter gives us.
22+
if(scatterPointData[0].index === undefined) return scatterPointData;
23+
2024
var newPointData = scatterPointData[0],
2125
cdi = newPointData.cd[newPointData.index];
2226

test/jasmine/assets/create_graph_div.js

+6
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@ module.exports = function createGraphDiv() {
44
var gd = document.createElement('div');
55
gd.id = 'graph';
66
document.body.appendChild(gd);
7+
8+
// force the graph to be at position 0,0 no matter what
9+
gd.style.position = 'fixed';
10+
gd.style.left = 0;
11+
gd.style.top = 0;
12+
713
return gd;
814
};

0 commit comments

Comments
 (0)