Skip to content

Commit 9bfbabf

Browse files
authored
Merge pull request #2113 from plotly/histogram-events
Histogram events & bin hover label improvements
2 parents af9f9e7 + 0331a16 commit 9bfbabf

File tree

16 files changed

+1005
-229
lines changed

16 files changed

+1005
-229
lines changed

devtools/test_dashboard/perf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ window.timeit = function(f, n, nchunk, arg) {
4242
times.sort();
4343
var min = (times[0]).toFixed(4);
4444
var max = (times[n - 1]).toFixed(4);
45-
var median = (times[Math.ceil(n / 2)]).toFixed(4);
45+
var median = (times[Math.min(Math.ceil(n / 2), n - 1)]).toFixed(4);
4646
var mean = (totalTime / n).toFixed(4);
4747
console.log((f.name || 'function') + ' timing (ms) - min: ' + min +
4848
' max: ' + max +

src/components/fx/hover.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1063,8 +1063,7 @@ function cleanPoint(d, hovermode) {
10631063
d.y0 = Lib.constrain(d.y0, 0, d.ya._length);
10641064
d.y1 = Lib.constrain(d.y1, 0, d.ya._length);
10651065

1066-
// and convert the x and y label values into objects
1067-
// formatted as text, with font info
1066+
// and convert the x and y label values into formatted text
10681067
if(d.xLabelVal !== undefined) {
10691068
d.xLabel = ('xLabel' in d) ? d.xLabel : Axes.hoverLabelText(d.xa, d.xLabelVal);
10701069
d.xVal = d.xa.c2d(d.xLabelVal);

src/lib/search.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
var isNumeric = require('fast-isnumeric');
1313
var loggers = require('./loggers');
1414

15+
// don't trust floating point equality - fraction of bin size to call
16+
// "on the line" and ensure that they go the right way specified by
17+
// linelow
18+
var roundingError = 1e-9;
19+
1520

1621
/**
1722
* findBin - find the bin for val - note that it can return outside the
@@ -26,20 +31,21 @@ var loggers = require('./loggers');
2631
exports.findBin = function(val, bins, linelow) {
2732
if(isNumeric(bins.start)) {
2833
return linelow ?
29-
Math.ceil((val - bins.start) / bins.size) - 1 :
30-
Math.floor((val - bins.start) / bins.size);
34+
Math.ceil((val - bins.start) / bins.size - roundingError) - 1 :
35+
Math.floor((val - bins.start) / bins.size + roundingError);
3136
}
3237
else {
33-
var n1 = 0,
34-
n2 = bins.length,
35-
c = 0,
36-
n,
37-
test;
38-
if(bins[bins.length - 1] >= bins[0]) {
38+
var n1 = 0;
39+
var n2 = bins.length;
40+
var c = 0;
41+
var binSize = (n2 > 1) ? (bins[n2 - 1] - bins[0]) / (n2 - 1) : 1;
42+
var n, test;
43+
if(binSize >= 0) {
3944
test = linelow ? lessThan : lessOrEqual;
4045
} else {
4146
test = linelow ? greaterOrEqual : greaterThan;
4247
}
48+
val += binSize * roundingError * (linelow ? -1 : 1) * (binSize >= 0 ? 1 : -1);
4349
// c is just to avoid infinite loops if there's an error
4450
while(n1 < n2 && c++ < 100) {
4551
n = Math.floor((n1 + n2) / 2);

src/plots/cartesian/axes.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var ONEHOUR = constants.ONEHOUR;
2828
var ONEMIN = constants.ONEMIN;
2929
var ONESEC = constants.ONESEC;
3030
var MINUS_SIGN = constants.MINUS_SIGN;
31+
var BADNUM = constants.BADNUM;
3132

3233
var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
3334

@@ -1216,12 +1217,28 @@ axes.tickText = function(ax, x, hover) {
12161217
return out;
12171218
};
12181219

1219-
axes.hoverLabelText = function(ax, val) {
1220+
/**
1221+
* create text for a hover label on this axis, with special handling of
1222+
* log axes (where negative values can't be displayed but can appear in hover text)
1223+
*
1224+
* @param {object} ax: the axis to format text for
1225+
* @param {number} val: calcdata value to format
1226+
* @param {Optional(number)} val2: a second value to display
1227+
*
1228+
* @returns {string} `val` formatted as a string appropriate to this axis, or
1229+
* `val` and `val2` as a range (ie '<val> - <val2>') if `val2` is provided and
1230+
* it's different from `val`.
1231+
*/
1232+
axes.hoverLabelText = function(ax, val, val2) {
1233+
if(val2 !== BADNUM && val2 !== val) {
1234+
return axes.hoverLabelText(ax, val) + ' - ' + axes.hoverLabelText(ax, val2);
1235+
}
1236+
12201237
var logOffScale = (ax.type === 'log' && val <= 0);
12211238
var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), 'hover').text;
12221239

12231240
if(logOffScale) {
1224-
return val === 0 ? '0' : '-' + tx;
1241+
return val === 0 ? '0' : MINUS_SIGN + tx;
12251242
}
12261243

12271244
// TODO: should we do something special if the axis calendar and

src/traces/bar/hover.js

+47-49
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,13 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
1818
var cd = pointData.cd;
1919
var trace = cd[0].trace;
2020
var t = cd[0].t;
21-
var xa = pointData.xa;
22-
var ya = pointData.ya;
2321

24-
var posVal, thisBarMinPos, thisBarMaxPos, minPos, maxPos, dx, dy;
22+
var posVal, sizeVal, posLetter, sizeLetter, dx, dy;
2523

26-
var positionFn = function(di) {
27-
return Fx.inbox(minPos(di) - posVal, maxPos(di) - posVal);
28-
};
24+
function thisBarMinPos(di) { return di[posLetter] - di.w / 2; }
25+
function thisBarMaxPos(di) { return di[posLetter] + di.w / 2; }
2926

30-
if(trace.orientation === 'h') {
31-
posVal = yval;
32-
thisBarMinPos = function(di) { return di.y - di.w / 2; };
33-
thisBarMaxPos = function(di) { return di.y + di.w / 2; };
34-
dx = function(di) {
35-
// add a gradient so hovering near the end of a
36-
// bar makes it a little closer match
37-
return Fx.inbox(di.b - xval, di.x - xval) + (di.x - xval) / (di.x - di.b);
38-
};
39-
dy = positionFn;
40-
}
41-
else {
42-
posVal = xval;
43-
thisBarMinPos = function(di) { return di.x - di.w / 2; };
44-
thisBarMaxPos = function(di) { return di.x + di.w / 2; };
45-
dy = function(di) {
46-
return Fx.inbox(di.b - yval, di.y - yval) + (di.y - yval) / (di.y - di.b);
47-
};
48-
dx = positionFn;
49-
}
50-
51-
minPos = (hovermode === 'closest') ?
27+
var minPos = (hovermode === 'closest') ?
5228
thisBarMinPos :
5329
function(di) {
5430
/*
@@ -60,44 +36,66 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
6036
return Math.min(thisBarMinPos(di), di.p - t.bargroupwidth / 2);
6137
};
6238

63-
maxPos = (hovermode === 'closest') ?
39+
var maxPos = (hovermode === 'closest') ?
6440
thisBarMaxPos :
6541
function(di) {
6642
return Math.max(thisBarMaxPos(di), di.p + t.bargroupwidth / 2);
6743
};
6844

45+
function positionFn(di) {
46+
return Fx.inbox(minPos(di) - posVal, maxPos(di) - posVal);
47+
}
48+
49+
function sizeFn(di) {
50+
// add a gradient so hovering near the end of a
51+
// bar makes it a little closer match
52+
return Fx.inbox(di.b - sizeVal, di[sizeLetter] - sizeVal) +
53+
(di[sizeLetter] - sizeVal) / (di[sizeLetter] - di.b);
54+
}
55+
56+
if(trace.orientation === 'h') {
57+
posVal = yval;
58+
sizeVal = xval;
59+
posLetter = 'y';
60+
sizeLetter = 'x';
61+
dx = sizeFn;
62+
dy = positionFn;
63+
}
64+
else {
65+
posVal = xval;
66+
sizeVal = yval;
67+
posLetter = 'x';
68+
sizeLetter = 'y';
69+
dy = sizeFn;
70+
dx = positionFn;
71+
}
72+
73+
var pa = pointData[posLetter + 'a'];
74+
var sa = pointData[sizeLetter + 'a'];
75+
6976
var distfn = Fx.getDistanceFunction(hovermode, dx, dy);
7077
Fx.getClosest(cd, distfn, pointData);
7178

7279
// skip the rest (for this trace) if we didn't find a close point
7380
if(pointData.index === false) return;
7481

7582
// the closest data point
76-
var index = pointData.index,
77-
di = cd[index],
78-
mc = di.mcc || trace.marker.color,
79-
mlc = di.mlcc || trace.marker.line.color,
80-
mlw = di.mlw || trace.marker.line.width;
83+
var index = pointData.index;
84+
var di = cd[index];
85+
var mc = di.mcc || trace.marker.color;
86+
var mlc = di.mlcc || trace.marker.line.color;
87+
var mlw = di.mlw || trace.marker.line.width;
88+
8189
if(Color.opacity(mc)) pointData.color = mc;
8290
else if(Color.opacity(mlc) && mlw) pointData.color = mlc;
8391

8492
var size = (trace.base) ? di.b + di.s : di.s;
85-
if(trace.orientation === 'h') {
86-
pointData.x0 = pointData.x1 = xa.c2p(di.x, true);
87-
pointData.xLabelVal = size;
93+
pointData[sizeLetter + '0'] = pointData[sizeLetter + '1'] = sa.c2p(di[sizeLetter], true);
94+
pointData[sizeLetter + 'LabelVal'] = size;
8895

89-
pointData.y0 = ya.c2p(minPos(di), true);
90-
pointData.y1 = ya.c2p(maxPos(di), true);
91-
pointData.yLabelVal = di.p;
92-
}
93-
else {
94-
pointData.y0 = pointData.y1 = ya.c2p(di.y, true);
95-
pointData.yLabelVal = size;
96-
97-
pointData.x0 = xa.c2p(minPos(di), true);
98-
pointData.x1 = xa.c2p(maxPos(di), true);
99-
pointData.xLabelVal = di.p;
100-
}
96+
pointData[posLetter + '0'] = pa.c2p(minPos(di), true);
97+
pointData[posLetter + '1'] = pa.c2p(maxPos(di), true);
98+
pointData[posLetter + 'LabelVal'] = di.p;
10199

102100
fillHoverText(di, trace, pointData);
103101
ErrorBars.hoverInfo(di, trace, pointData);

src/traces/heatmap/calc.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ module.exports = function calc(gd, trace) {
4040
y0,
4141
dy,
4242
z,
43-
i;
43+
i,
44+
binned;
4445

4546
// cancel minimum tick spacings (only applies to bars and boxes)
4647
xa._minDtick = 0;
4748
ya._minDtick = 0;
4849

4950
if(isHist) {
50-
var binned = histogram2dCalc(gd, trace);
51+
binned = histogram2dCalc(gd, trace);
5152
x = binned.x;
5253
x0 = binned.x0;
5354
dx = binned.dx;
@@ -128,6 +129,12 @@ module.exports = function calc(gd, trace) {
128129

129130
var cd0 = {x: xArray, y: yArray, z: z, text: trace.text};
130131

132+
if(isHist) {
133+
cd0.xRanges = binned.xRanges;
134+
cd0.yRanges = binned.yRanges;
135+
cd0.pts = binned.pts;
136+
}
137+
131138
// auto-z and autocolorscale if applicable
132139
colorscaleCalc(trace, z, '', 'z');
133140

0 commit comments

Comments
 (0)