Skip to content
10 changes: 5 additions & 5 deletions src/components/dragelement/unhover.js
Original file line number Diff line number Diff line change
@@ -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);
};
5 changes: 4 additions & 1 deletion src/components/fx/constants.js
Original file line number Diff line number Diff line change
@@ -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
@@ -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(
gd._fullLayout._uid + constants.HOVERID,
constants.HOVERMINTIME,
function() { _hover(gd, evt, subplot, noHoverEvent); }
);
};

/*
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) {
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
@@ -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');
102 changes: 102 additions & 0 deletions src/lib/throttle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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 {string} id: an identifier to mark events to throttle together
* @param {number} minInterval: minimum time, in milliseconds, between
* invocations of `callback`
* @param {function} callback: the function to throttle. `callback` itself
* should be a purely synchronous function.
*/
exports.throttle = function throttle(id, minInterval, callback) {
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
@@ -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) {
30 changes: 15 additions & 15 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
@@ -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);
@@ -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);
@@ -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 || {});
@@ -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,

@@ -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,

@@ -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,
@@ -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,
@@ -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 = [],
@@ -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 = {};
@@ -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) {
@@ -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) {
@@ -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(
@@ -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;

@@ -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);
@@ -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 || [];
Loading