Skip to content

Commit e7c7149

Browse files
authored
Merge pull request #2040 from plotly/throttle-select
Throttle selectPoints
2 parents 2ba7bdf + 811073e commit e7c7149

23 files changed

+582
-483
lines changed

src/components/dragelement/unhover.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@
1111

1212

1313
var Events = require('../../lib/events');
14+
var throttle = require('../../lib/throttle');
15+
var getGraphDiv = require('../../lib/get_graph_div');
1416

17+
var hoverConstants = require('../fx/constants');
1518

1619
var unhover = module.exports = {};
1720

1821

1922
unhover.wrapped = function(gd, evt, subplot) {
20-
if(typeof gd === 'string') gd = document.getElementById(gd);
23+
gd = getGraphDiv(gd);
2124

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

2828
unhover.raw(gd, evt, subplot);
2929
};

src/components/fx/constants.js

+4-1
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

+6-20
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,13 @@ var HOVERTEXTPAD = constants.HOVERTEXTPAD;
6767
// We wrap the hovers in a timer, to limit their frequency.
6868
// The actual rendering is done by private function _hover.
6969
exports.hover = function hover(gd, evt, subplot, noHoverEvent) {
70-
if(typeof gd === 'string') gd = document.getElementById(gd);
71-
if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0;
70+
gd = Lib.getGraphDiv(gd);
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+
gd._fullLayout._uid + constants.HOVERID,
74+
constants.HOVERMINTIME,
75+
function() { _hover(gd, evt, subplot, noHoverEvent); }
76+
);
9177
};
9278

9379
/*

src/lib/get_graph_div.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 by 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+
module.exports = function(gd) {
20+
var gdElement;
21+
22+
if(typeof gd === 'string') {
23+
gdElement = document.getElementById(gd);
24+
25+
if(gdElement === null) {
26+
throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
27+
}
28+
29+
return gdElement;
30+
}
31+
else if(gd === null || gd === undefined) {
32+
throw new Error('DOM element provided is null or undefined');
33+
}
34+
35+
return gd; // otherwise assume that gd is a DOM element
36+
};

src/lib/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ 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.throttleDone = throttleModule.done;
103+
lib.clearThrottle = throttleModule.clear;
104+
105+
lib.getGraphDiv = require('./get_graph_div');
106+
100107
lib.notifier = require('./notifier');
101108

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

src/lib/throttle.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 {string} id: an identifier to mark events to throttle together
22+
* @param {number} minInterval: minimum time, in milliseconds, between
23+
* invocations of `callback`
24+
* @param {function} callback: the function to throttle. `callback` itself
25+
* should be a purely synchronous function.
26+
*/
27+
exports.throttle = function throttle(id, minInterval, callback) {
28+
var cache = timerCache[id];
29+
var now = Date.now();
30+
31+
if(!cache) {
32+
/*
33+
* Throw out old items before making a new one, to prevent the cache
34+
* getting overgrown, for example from old plots that have been replaced.
35+
* 1 minute age is arbitrary.
36+
*/
37+
for(var idi in timerCache) {
38+
if(timerCache[idi].ts < now - 60000) {
39+
delete timerCache[idi];
40+
}
41+
}
42+
cache = timerCache[id] = {ts: 0, timer: null};
43+
}
44+
45+
_clearTimeout(cache);
46+
47+
function exec() {
48+
callback();
49+
cache.ts = Date.now();
50+
if(cache.onDone) {
51+
cache.onDone();
52+
cache.onDone = null;
53+
}
54+
}
55+
56+
if(now > cache.ts + minInterval) {
57+
exec();
58+
return;
59+
}
60+
61+
cache.timer = setTimeout(function() {
62+
exec();
63+
cache.timer = null;
64+
}, minInterval);
65+
};
66+
67+
exports.done = function(id) {
68+
var cache = timerCache[id];
69+
if(!cache || !cache.timer) return Promise.resolve();
70+
71+
return new Promise(function(resolve) {
72+
var previousOnDone = cache.onDone;
73+
cache.onDone = function onDone() {
74+
if(previousOnDone) previousOnDone();
75+
resolve();
76+
cache.onDone = null;
77+
};
78+
});
79+
};
80+
81+
/**
82+
* Clear the throttle cache for one or all timers
83+
* @param {optional string} id:
84+
* if provided, clear just this timer
85+
* if omitted, clear all timers (mainly useful for testing)
86+
*/
87+
exports.clear = function(id) {
88+
if(id) {
89+
_clearTimeout(timerCache[id]);
90+
delete timerCache[id];
91+
}
92+
else {
93+
for(var idi in timerCache) exports.clear(idi);
94+
}
95+
};
96+
97+
function _clearTimeout(cache) {
98+
if(cache && cache.timer !== null) {
99+
clearTimeout(cache.timer);
100+
cache.timer = null;
101+
}
102+
}

src/plot_api/helpers.js

-22
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,6 @@ var Axes = require('../plots/cartesian/axes');
1919
var Color = require('../components/color');
2020

2121

22-
// Get the container div: we store all variables for this plot as
23-
// properties of this div
24-
// some callers send this in by DOM element, others by id (string)
25-
exports.getGraphDiv = function(gd) {
26-
var gdElement;
27-
28-
if(typeof gd === 'string') {
29-
gdElement = document.getElementById(gd);
30-
31-
if(gdElement === null) {
32-
throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
33-
}
34-
35-
return gdElement;
36-
}
37-
else if(gd === null || gd === undefined) {
38-
throw new Error('DOM element provided is null or undefined');
39-
}
40-
41-
return gd; // otherwise assume that gd is a DOM element
42-
};
43-
4422
// clear the promise queue if one of them got rejected
4523
exports.clearPromiseQueue = function(gd) {
4624
if(Array.isArray(gd._promises) && gd._promises.length > 0) {

src/plot_api/plot_api.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ var axisIds = require('../plots/cartesian/axis_ids');
6060
Plotly.plot = function(gd, data, layout, config) {
6161
var frames;
6262

63-
gd = helpers.getGraphDiv(gd);
63+
gd = Lib.getGraphDiv(gd);
6464

6565
// Events.init is idempotent and bails early if gd has already been init'd
6666
Events.init(gd);
@@ -581,7 +581,7 @@ function plotPolar(gd, data, layout) {
581581

582582
// convenience function to force a full redraw, mostly for use by plotly.js
583583
Plotly.redraw = function(gd) {
584-
gd = helpers.getGraphDiv(gd);
584+
gd = Lib.getGraphDiv(gd);
585585

586586
if(!Lib.isPlotDiv(gd)) {
587587
throw new Error('This element is not a Plotly plot: ' + gd);
@@ -606,7 +606,7 @@ Plotly.redraw = function(gd) {
606606
* @param {Object} config
607607
*/
608608
Plotly.newPlot = function(gd, data, layout, config) {
609-
gd = helpers.getGraphDiv(gd);
609+
gd = Lib.getGraphDiv(gd);
610610

611611
// remove gl contexts
612612
Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});
@@ -959,7 +959,7 @@ function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray
959959
*
960960
*/
961961
Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) {
962-
gd = helpers.getGraphDiv(gd);
962+
gd = Lib.getGraphDiv(gd);
963963

964964
var undo = spliceTraces(gd, update, indices, maxPoints,
965965

@@ -986,7 +986,7 @@ Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) {
986986
};
987987

988988
Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
989-
gd = helpers.getGraphDiv(gd);
989+
gd = Lib.getGraphDiv(gd);
990990

991991
var undo = spliceTraces(gd, update, indices, maxPoints,
992992

@@ -1022,7 +1022,7 @@ Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
10221022
*
10231023
*/
10241024
Plotly.addTraces = function addTraces(gd, traces, newIndices) {
1025-
gd = helpers.getGraphDiv(gd);
1025+
gd = Lib.getGraphDiv(gd);
10261026

10271027
var currentIndices = [],
10281028
undoFunc = Plotly.deleteTraces,
@@ -1099,7 +1099,7 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) {
10991099
* @param {Number|Number[]} indices The indices
11001100
*/
11011101
Plotly.deleteTraces = function deleteTraces(gd, indices) {
1102-
gd = helpers.getGraphDiv(gd);
1102+
gd = Lib.getGraphDiv(gd);
11031103

11041104
var traces = [],
11051105
undoFunc = Plotly.addTraces,
@@ -1165,7 +1165,7 @@ Plotly.deleteTraces = function deleteTraces(gd, indices) {
11651165
* Plotly.moveTraces(gd, [b, d, e, a, c]) // same as 'move to end'
11661166
*/
11671167
Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
1168-
gd = helpers.getGraphDiv(gd);
1168+
gd = Lib.getGraphDiv(gd);
11691169

11701170
var newData = [],
11711171
movingTraceMap = [],
@@ -1262,7 +1262,7 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
12621262
* style files that want to specify cyclical default values).
12631263
*/
12641264
Plotly.restyle = function restyle(gd, astr, val, _traces) {
1265-
gd = helpers.getGraphDiv(gd);
1265+
gd = Lib.getGraphDiv(gd);
12661266
helpers.clearPromiseQueue(gd);
12671267

12681268
var aobj = {};
@@ -1649,7 +1649,7 @@ function _restyle(gd, aobj, traces) {
16491649
* allows setting multiple attributes simultaneously
16501650
*/
16511651
Plotly.relayout = function relayout(gd, astr, val) {
1652-
gd = helpers.getGraphDiv(gd);
1652+
gd = Lib.getGraphDiv(gd);
16531653
helpers.clearPromiseQueue(gd);
16541654

16551655
if(gd.framework && gd.framework.isPolar) {
@@ -2079,7 +2079,7 @@ function _relayout(gd, aobj) {
20792079
*
20802080
*/
20812081
Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
2082-
gd = helpers.getGraphDiv(gd);
2082+
gd = Lib.getGraphDiv(gd);
20832083
helpers.clearPromiseQueue(gd);
20842084

20852085
if(gd.framework && gd.framework.isPolar) {
@@ -2185,7 +2185,7 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
21852185
* configuration for the animation
21862186
*/
21872187
Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
2188-
gd = helpers.getGraphDiv(gd);
2188+
gd = Lib.getGraphDiv(gd);
21892189

21902190
if(!Lib.isPlotDiv(gd)) {
21912191
throw new Error(
@@ -2549,7 +2549,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
25492549
* will be overwritten.
25502550
*/
25512551
Plotly.addFrames = function(gd, frameList, indices) {
2552-
gd = helpers.getGraphDiv(gd);
2552+
gd = Lib.getGraphDiv(gd);
25532553

25542554
var numericNameWarningCount = 0;
25552555

@@ -2673,7 +2673,7 @@ Plotly.addFrames = function(gd, frameList, indices) {
26732673
* list of integer indices of frames to be deleted
26742674
*/
26752675
Plotly.deleteFrames = function(gd, frameList) {
2676-
gd = helpers.getGraphDiv(gd);
2676+
gd = Lib.getGraphDiv(gd);
26772677

26782678
if(!Lib.isPlotDiv(gd)) {
26792679
throw new Error('This element is not a Plotly plot: ' + gd);
@@ -2717,7 +2717,7 @@ Plotly.deleteFrames = function(gd, frameList) {
27172717
* the id or DOM element of the graph container div
27182718
*/
27192719
Plotly.purge = function purge(gd) {
2720-
gd = helpers.getGraphDiv(gd);
2720+
gd = Lib.getGraphDiv(gd);
27212721

27222722
var fullLayout = gd._fullLayout || {},
27232723
fullData = gd._fullData || [];

0 commit comments

Comments
 (0)