Skip to content

Throttle selectPoints #2040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 28, 2017
10 changes: 5 additions & 5 deletions src/components/dragelement/unhover.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@


var Events = require('../../lib/events');
var throttle = require('../../lib/throttle');
var getGraphDiv = require('../../lib/get_graph_div');

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

var unhover = module.exports = {};


unhover.wrapped = function(gd, evt, subplot) {
if(typeof gd === 'string') gd = document.getElementById(gd);
gd = getGraphDiv(gd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIce 🌴


// Important, clear any queued hovers
if(gd._hoverTimer) {
clearTimeout(gd._hoverTimer);
gd._hoverTimer = undefined;
}
throttle.clear(gd._fullLayout._uid + hoverConstants.HOVERID);

unhover.raw(gd, evt, subplot);
};
Expand Down
5 changes: 4 additions & 1 deletion src/components/fx/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ module.exports = {
HOVERFONT: 'Arial, sans-serif',

// minimum time (msec) between hover calls
HOVERMINTIME: 50
HOVERMINTIME: 50,

// ID suffix (with fullLayout._uid) for hover events in the throttle cache
HOVERID: '-hover'
};
26 changes: 6 additions & 20 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,13 @@ var HOVERTEXTPAD = constants.HOVERTEXTPAD;
// We wrap the hovers in a timer, to limit their frequency.
// The actual rendering is done by private function _hover.
exports.hover = function hover(gd, evt, subplot, noHoverEvent) {
if(typeof gd === 'string') gd = document.getElementById(gd);
if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0;
gd = Lib.getGraphDiv(gd);

// If we have an update queued, discard it now
if(gd._hoverTimer !== undefined) {
clearTimeout(gd._hoverTimer);
gd._hoverTimer = undefined;
}
// Is it more than 100ms since the last update? If so, force
// an update now (synchronously) and exit
if(Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) {
_hover(gd, evt, subplot, noHoverEvent);
gd._lastHoverTime = Date.now();
return;
}
// Queue up the next hover for 100ms from now (if no further events)
gd._hoverTimer = setTimeout(function() {
_hover(gd, evt, subplot, noHoverEvent);
gd._lastHoverTime = Date.now();
gd._hoverTimer = undefined;
}, constants.HOVERMINTIME);
Lib.throttle(
function() { _hover(gd, evt, subplot, noHoverEvent); },
constants.HOVERMINTIME,
gd._fullLayout._uid + constants.HOVERID
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using fullLayout._uid to construct the key to what we're throttling means we can 🔪 gd._lastHoverTime and gd._hoverTimer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loving this ❤️

);
};

/*
Expand Down
36 changes: 36 additions & 0 deletions src/lib/get_graph_div.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

/**
* Allow referencing a graph DOM element either directly
* or by its id string
*
* @param {HTMLDivElement|string} gd: a graph element or its id
*
* @returns {HTMLDivElement} the DOM element of the graph
*/
module.exports = function(gd) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, this file was supposed to go in the next commit aa434dd

docs cleaned up in 0861736

var gdElement;

if(typeof gd === 'string') {
gdElement = document.getElementById(gd);

if(gdElement === null) {
throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
}

return gdElement;
}
else if(gd === null || gd === undefined) {
throw new Error('DOM element provided is null or undefined');
}

return gd; // otherwise assume that gd is a DOM element
};
7 changes: 7 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ lib.error = loggersModule.error;
var regexModule = require('./regex');
lib.counterRegex = regexModule.counter;

var throttleModule = require('./throttle');
lib.throttle = throttleModule.throttle;
lib.throttleDone = throttleModule.done;
lib.clearThrottle = throttleModule.clear;

lib.getGraphDiv = require('./get_graph_div');

lib.notifier = require('./notifier');

lib.filterUnique = require('./filter_unique');
Expand Down
101 changes: 101 additions & 0 deletions src/lib/throttle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var timerCache = {};

/**
* Throttle a callback. `callback` executes synchronously only if
* more than `minInterval` milliseconds have already elapsed since the latest
* call (if any). Otherwise we wait until `minInterval` is over and execute the
* last callback received while waiting.
* So the first and last events in a train are always executed (eventually)
* but some of the events in the middle can be dropped.
*
* @param {function} callback: the function to throttle
* @param {string} id: an identifier to mark events to throttle together
* @param {number} minInterval: minimum time, in milliseconds, between
* invocations of `callback`
*/
exports.throttle = function throttle(callback, minInterval, id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think throttle(id, minInterval, callback) would a call signature more familiar to most (node.)js devs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> b6d64be

var cache = timerCache[id];
var now = Date.now();

if(!cache) {
/*
* Throw out old items before making a new one, to prevent the cache
* getting overgrown, for example from old plots that have been replaced.
* 1 minute age is arbitrary.
*/
for(var idi in timerCache) {
if(timerCache[idi].ts < now - 60000) {
delete timerCache[idi];
}
}
cache = timerCache[id] = {ts: 0, timer: null};
}

_clearTimeout(cache);

function exec() {
callback();
Copy link
Contributor

@etpinard etpinard Sep 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So callback can't itself be async - which is fine but deserves a mention in the docstring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> b6d64be

cache.ts = Date.now();
if(cache.onDone) {
cache.onDone();
cache.onDone = null;
}
}

if(now > cache.ts + minInterval) {
exec();
return;
}

cache.timer = setTimeout(function() {
exec();
cache.timer = null;
}, minInterval);
};

exports.done = function(id) {
var cache = timerCache[id];
if(!cache || !cache.timer) return Promise.resolve();

return new Promise(function(resolve) {
var previousOnDone = cache.onDone;
cache.onDone = function onDone() {
if(previousOnDone) previousOnDone();
resolve();
cache.onDone = null;
};
});
};

/**
* Clear the throttle cache for one or all timers
* @param {optional string} id:
* if provided, clear just this timer
* if omitted, clear all timers (mainly useful for testing)
*/
exports.clear = function(id) {
if(id) {
_clearTimeout(timerCache[id]);
delete timerCache[id];
}
else {
for(var idi in timerCache) exports.clear(idi);
}
};

function _clearTimeout(cache) {
if(cache && cache.timer !== null) {
clearTimeout(cache.timer);
cache.timer = null;
}
}
22 changes: 0 additions & 22 deletions src/plot_api/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,6 @@ var Axes = require('../plots/cartesian/axes');
var Color = require('../components/color');


// Get the container div: we store all variables for this plot as
// properties of this div
// some callers send this in by DOM element, others by id (string)
exports.getGraphDiv = function(gd) {
var gdElement;

if(typeof gd === 'string') {
gdElement = document.getElementById(gd);

if(gdElement === null) {
throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
}

return gdElement;
}
else if(gd === null || gd === undefined) {
throw new Error('DOM element provided is null or undefined');
}

return gd; // otherwise assume that gd is a DOM element
};

// clear the promise queue if one of them got rejected
exports.clearPromiseQueue = function(gd) {
if(Array.isArray(gd._promises) && gd._promises.length > 0) {
Expand Down
30 changes: 15 additions & 15 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ var axisIds = require('../plots/cartesian/axis_ids');
Plotly.plot = function(gd, data, layout, config) {
var frames;

gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

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

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

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

// remove gl contexts
Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});
Expand Down Expand Up @@ -959,7 +959,7 @@ function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray
*
*/
Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

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

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

Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

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

Expand Down Expand Up @@ -1022,7 +1022,7 @@ Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
*
*/
Plotly.addTraces = function addTraces(gd, traces, newIndices) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

var currentIndices = [],
undoFunc = Plotly.deleteTraces,
Expand Down Expand Up @@ -1099,7 +1099,7 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) {
* @param {Number|Number[]} indices The indices
*/
Plotly.deleteTraces = function deleteTraces(gd, indices) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

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

var newData = [],
movingTraceMap = [],
Expand Down Expand Up @@ -1262,7 +1262,7 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
* style files that want to specify cyclical default values).
*/
Plotly.restyle = function restyle(gd, astr, val, _traces) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);
helpers.clearPromiseQueue(gd);

var aobj = {};
Expand Down Expand Up @@ -1649,7 +1649,7 @@ function _restyle(gd, aobj, traces) {
* allows setting multiple attributes simultaneously
*/
Plotly.relayout = function relayout(gd, astr, val) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);
helpers.clearPromiseQueue(gd);

if(gd.framework && gd.framework.isPolar) {
Expand Down Expand Up @@ -2079,7 +2079,7 @@ function _relayout(gd, aobj) {
*
*/
Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);
helpers.clearPromiseQueue(gd);

if(gd.framework && gd.framework.isPolar) {
Expand Down Expand Up @@ -2185,7 +2185,7 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
* configuration for the animation
*/
Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

if(!Lib.isPlotDiv(gd)) {
throw new Error(
Expand Down Expand Up @@ -2549,7 +2549,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
* will be overwritten.
*/
Plotly.addFrames = function(gd, frameList, indices) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

var numericNameWarningCount = 0;

Expand Down Expand Up @@ -2673,7 +2673,7 @@ Plotly.addFrames = function(gd, frameList, indices) {
* list of integer indices of frames to be deleted
*/
Plotly.deleteFrames = function(gd, frameList) {
gd = helpers.getGraphDiv(gd);
gd = Lib.getGraphDiv(gd);

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

var fullLayout = gd._fullLayout || {},
fullData = gd._fullData || [];
Expand Down
Loading