Skip to content

Commit a6df912

Browse files
committed
generalized event throttling routine
1 parent 549ee38 commit a6df912

15 files changed

+147
-39
lines changed

src/components/dragelement/unhover.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212

1313
var Events = require('../../lib/events');
14+
var throttle = require('../../lib/throttle');
1415

16+
var hoverConstants = require('../fx/constants');
1517

1618
var unhover = module.exports = {};
1719

@@ -20,10 +22,7 @@ unhover.wrapped = function(gd, evt, subplot) {
2022
if(typeof gd === 'string') gd = document.getElementById(gd);
2123

2224
// Important, clear any queued hovers
23-
if(gd._hoverTimer) {
24-
clearTimeout(gd._hoverTimer);
25-
gd._hoverTimer = undefined;
26-
}
25+
throttle.clear(gd._fullLayout._uid + hoverConstants.HOVERID);
2726

2827
unhover.raw(gd, evt, subplot);
2928
};

src/components/fx/constants.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ module.exports = {
2626
HOVERFONT: 'Arial, sans-serif',
2727

2828
// minimum time (msec) between hover calls
29-
HOVERMINTIME: 50
29+
HOVERMINTIME: 50,
30+
31+
// ID suffix (with fullLayout._uid) for hover events in the throttle cache
32+
HOVERID: '-hover'
3033
};

src/components/fx/hover.js

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,12 @@ var HOVERTEXTPAD = constants.HOVERTEXTPAD;
6868
// The actual rendering is done by private function _hover.
6969
exports.hover = function hover(gd, evt, subplot, noHoverEvent) {
7070
if(typeof gd === 'string') gd = document.getElementById(gd);
71-
if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0;
7271

73-
// If we have an update queued, discard it now
74-
if(gd._hoverTimer !== undefined) {
75-
clearTimeout(gd._hoverTimer);
76-
gd._hoverTimer = undefined;
77-
}
78-
// Is it more than 100ms since the last update? If so, force
79-
// an update now (synchronously) and exit
80-
if(Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) {
81-
_hover(gd, evt, subplot, noHoverEvent);
82-
gd._lastHoverTime = Date.now();
83-
return;
84-
}
85-
// Queue up the next hover for 100ms from now (if no further events)
86-
gd._hoverTimer = setTimeout(function() {
87-
_hover(gd, evt, subplot, noHoverEvent);
88-
gd._lastHoverTime = Date.now();
89-
gd._hoverTimer = undefined;
90-
}, constants.HOVERMINTIME);
72+
Lib.throttle(
73+
function() { _hover(gd, evt, subplot, noHoverEvent); },
74+
constants.HOVERMINTIME,
75+
gd._fullLayout._uid + constants.HOVERID
76+
);
9177
};
9278

9379
/*

src/lib/get_graph_div.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
'use strict';
10+
11+
/**
12+
* Allow referencing a graph DOM element either directly
13+
* or its id string
14+
*
15+
* @param {HTMLDivElement|string} gd: a graph element or its id
16+
*
17+
* @returns {HTMLDivElement} the DOM element of the graph
18+
*/
19+
// Get the container div: we store all variables for this plot as
20+
// properties of this div
21+
// some callers send this in by DOM element, others by id (string)
22+
module.exports = function(gd) {
23+
var gdElement;
24+
25+
if(typeof gd === 'string') {
26+
gdElement = document.getElementById(gd);
27+
28+
if(gdElement === null) {
29+
throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
30+
}
31+
32+
return gdElement;
33+
}
34+
else if(gd === null || gd === undefined) {
35+
throw new Error('DOM element provided is null or undefined');
36+
}
37+
38+
return gd; // otherwise assume that gd is a DOM element
39+
};

src/lib/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ lib.error = loggersModule.error;
9797
var regexModule = require('./regex');
9898
lib.counterRegex = regexModule.counter;
9999

100+
var throttleModule = require('./throttle');
101+
lib.throttle = throttleModule.throttle;
102+
lib.clearThrottle = throttleModule.clear;
103+
100104
lib.notifier = require('./notifier');
101105

102106
lib.filterUnique = require('./filter_unique');

src/lib/throttle.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
'use strict';
10+
11+
var timerCache = {};
12+
13+
/**
14+
* Throttle a callback. `callback` executes synchronously only if
15+
* more than `minInterval` milliseconds have already elapsed since the latest
16+
* call (if any). Otherwise we wait until `minInterval` is over and execute the
17+
* last callback received while waiting.
18+
* So the first and last events in a train are always executed (eventually)
19+
* but some of the events in the middle can be dropped.
20+
*
21+
* @param {function} callback: the function to throttle
22+
* @param {string} id: an identifier to mark events to throttle together
23+
* @param {number} minInterval: minimum time, in milliseconds, between
24+
* invocations of `callback`
25+
*/
26+
exports.throttle = function throttle(callback, minInterval, id) {
27+
var cache = timerCache[id];
28+
var now = Date.now();
29+
30+
if(!cache) {
31+
/*
32+
* Throw out old items before making a new one, to prevent the cache
33+
* getting overgrown, for example from old plots that have been replaced.
34+
* 1 minute age is arbitrary.
35+
*/
36+
for(var idi in timerCache) {
37+
if(timerCache[idi].ts < now - 60000) {
38+
delete timerCache[idi];
39+
}
40+
}
41+
cache = timerCache[id] = {ts: 0, timer: null};
42+
}
43+
44+
_clearTimeout(cache);
45+
46+
if(now > cache.ts + minInterval) {
47+
callback();
48+
cache.ts = now;
49+
return;
50+
}
51+
52+
cache.timer = setTimeout(function() {
53+
callback();
54+
cache.ts = Date.now();
55+
cache.timer = null;
56+
}, minInterval);
57+
};
58+
59+
/**
60+
* Clear the throttle cache for one or all timers
61+
* @param {optional string} id:
62+
* if provided, clear just this timer
63+
* if omitted, clear all timers (mainly useful for testing)
64+
*/
65+
exports.clear = function(id) {
66+
if(id) {
67+
_clearTimeout(timerCache[id]);
68+
delete timerCache[id];
69+
}
70+
else {
71+
for(var idi in timerCache) exports.clear(idi);
72+
}
73+
};
74+
75+
function _clearTimeout(cache) {
76+
if(cache && cache.timer !== null) {
77+
clearTimeout(cache.timer);
78+
cache.timer = null;
79+
}
80+
}

src/plots/plots.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,8 +1327,6 @@ plots.purge = function(gd) {
13271327
delete gd.hmlumcount;
13281328
delete gd.hmpixcount;
13291329
delete gd.numboxes;
1330-
delete gd._hoverTimer;
1331-
delete gd._lastHoverTime;
13321330
delete gd._transitionData;
13331331
delete gd._transitioning;
13341332
delete gd._initialAutoSize;

test/jasmine/tests/cartesian_interact_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ describe('Event data:', function() {
446446
function _hover(px, py) {
447447
return new Promise(function(resolve, reject) {
448448
gd.once('plotly_hover', function(d) {
449-
delete gd._lastHoverTime;
449+
Lib.clearThrottle();
450450
resolve(d);
451451
});
452452

test/jasmine/tests/hover_label_test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ describe('hover info', function() {
470470
describe('\'hover info for x/y/z traces', function() {
471471
function _hover(gd, xpx, ypx) {
472472
Fx.hover(gd, { xpx: xpx, ypx: ypx }, 'xy');
473-
delete gd._lastHoverTime;
473+
Lib.clearThrottle();
474474
}
475475

476476
function _assert(nameLabel, lines) {
@@ -811,7 +811,7 @@ describe('hover after resizing', function() {
811811
}
812812

813813
function assertLabelCount(pos, cnt, msg) {
814-
delete gd._lastHoverTime;
814+
Lib.clearThrottle();
815815
mouseEvent('mousemove', pos[0], pos[1]);
816816

817817
var hoverText = d3.selectAll('g.hovertext');
@@ -1097,7 +1097,7 @@ describe('Test hover label custom styling:', function() {
10971097

10981098
function _hover(gd, opts) {
10991099
Fx.hover(gd, opts);
1100-
delete gd._lastHoverTime;
1100+
Lib.clearThrottle();
11011101
}
11021102

11031103
it('should work for x/y cartesian traces', function(done) {

test/jasmine/tests/hover_spikeline_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('spikeline', function() {
3030

3131
function _hover(evt, subplot) {
3232
Fx.hover(gd, evt, subplot);
33-
delete gd._lastHoverTime;
33+
Lib.clearThrottle();
3434
}
3535

3636
function _assert(lineExpect, circleExpect) {

test/jasmine/tests/pie_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ describe('pie hovering', function() {
241241

242242
function _hover() {
243243
mouseEvent('mouseover', 223, 143);
244-
delete gd._lastHoverTime;
244+
Lib.clearThrottle();
245245
}
246246

247247
function assertLabel(content, style) {

test/jasmine/tests/plots_test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,7 @@ describe('Test Plots', function() {
424424
'data', 'layout', '_fullData', '_fullLayout', 'calcdata', 'framework',
425425
'empty', 'fid', 'undoqueue', 'undonum', 'autoplay', 'changed',
426426
'_promises', '_redrawTimer', 'firstscatter', 'hmlumcount', 'hmpixcount',
427-
'numboxes', '_hoverTimer', '_lastHoverTime', '_transitionData',
428-
'_transitioning'
427+
'numboxes', '_transitionData', '_transitioning'
429428
];
430429

431430
Plots.purge(gd);

test/jasmine/tests/sankey_test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ describe('sankey tests', function() {
382382
function _hover(px, py) {
383383
mouseEvent('mousemove', px, py);
384384
mouseEvent('mouseover', px, py);
385-
delete gd._lastHoverTime;
385+
Lib.clearThrottle();
386386
}
387387

388388
Plotly.plot(gd, mockCopy).then(function() {
@@ -454,7 +454,7 @@ describe('sankey tests', function() {
454454
function _hover(px, py) {
455455
mouseEvent('mousemove', px, py);
456456
mouseEvent('mouseover', px, py);
457-
delete gd._lastHoverTime;
457+
Lib.clearThrottle();
458458
}
459459

460460
Plotly.plot(gd, mockCopy)
@@ -489,7 +489,7 @@ describe('sankey tests', function() {
489489
return function(elType) {
490490
return new Promise(function(resolve, reject) {
491491
gd.once(eventType, function(d) {
492-
delete gd._lastHoverTime;
492+
Lib.clearThrottle();
493493
resolve(d);
494494
});
495495

test/jasmine/tests/ternary_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('ternary plots', function() {
122122
'hoverlabel.font.family': [['Gravitas', 'Arial', 'Roboto']]
123123
})
124124
.then(function() {
125-
delete gd._lastHoverTime;
125+
Lib.clearThrottle();
126126
mouseEvent('mousemove', pointPos[0], pointPos[1]);
127127

128128
var path = d3.select('g.hovertext').select('path');

test/jasmine/tests/transform_sort_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ describe('Test sort transform interactions:', function() {
275275
function hover(gd, id) {
276276
return new Promise(function(resolve, reject) {
277277
gd.once('plotly_hover', function(eventData) {
278-
delete gd._lastHoverTime;
278+
Lib.clearThrottle();
279279
resolve(eventData);
280280
});
281281

0 commit comments

Comments
 (0)