From 83cd8c84b51e9ad18fce034df78d50188490842a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 11 Apr 2017 14:10:19 -0400 Subject: [PATCH 01/38] add hoverlabel attribute container for all traces - allow arrayOk for all hover label attribute - don't set dflt for bgcolor, bordercolor and font.color as their value depends and the trace color and plot bgcolor --- src/plots/attributes.js | 34 ++++++++++++++++++++++++++++++++++ src/plots/plots.js | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 594fba13a6d..57eda293a07 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -8,6 +8,9 @@ 'use strict'; +var constants = require('./cartesian/constants'); +var fontAttrs = require('./font_attributes'); +var extendFlat = require('../lib/extend').extendFlat; module.exports = { type: { @@ -80,6 +83,37 @@ module.exports = { 'But, if `none` is set, click and hover events are still fired.' ].join(' ') }, + hoverlabel: { + bgcolor: { + valType: 'color', + role: 'style', + arrayOk: true, + description: [ + 'Sets the background color of the hover label.' + ].join(' ') + }, + bordercolor: { + valType: 'color', + role: 'style', + arrayOk: true, + description: [ + 'Sets the border color of the hover label.' + ].join(' ') + }, + font: { + family: extendFlat({}, fontAttrs.family, { + arrayOk: true, + dflt: constants.HOVERFONT + }), + size: extendFlat({}, fontAttrs.size, { + arrayOk: true, + dflt: constants.HOVERFONTSIZE + }), + color: extendFlat({}, fontAttrs.color, { + arrayOk: true + }) + } + }, stream: { token: { valType: 'string', diff --git a/src/plots/plots.js b/src/plots/plots.js index 5c853459536..966d1fd9cab 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -811,6 +811,10 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde // gets overwritten in pie, geo and ternary modules coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); + coerce('hoverlabel.bgcolor'); + coerce('hoverlabel.bordercolor'); + Lib.coerceFont(coerce, 'hoverlabel.font'); + // TODO add per-base-plot-module trace defaults step if(_module) _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); From 258f1c7a7dc6144359d1d748f1fe481ad9a8dd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 11 Apr 2017 14:11:21 -0400 Subject: [PATCH 02/38] merge hoverlabel items in scatter and bar calcdata --- src/traces/bar/arrays_to_calcdata.js | 8 ++++++++ src/traces/scatter/arrays_to_calcdata.js | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/traces/bar/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js index 675364e9920..f034117dfe5 100644 --- a/src/traces/bar/arrays_to_calcdata.js +++ b/src/traces/bar/arrays_to_calcdata.js @@ -28,4 +28,12 @@ module.exports = function arraysToCalcdata(cd, trace) { mergeArray(markerLine.width, cd, 'mlw'); } } + + if(trace.hoverlabel) { + mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); + mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); + mergeArray(trace.hoverlabel.font.size, cd, 'hts'); + mergeArray(trace.hoverlabel.font.color, cd, 'htc'); + mergeArray(trace.hoverlabel.font.family, cd, 'htf'); + } }; diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index 378fc7613f0..b2172eb3ed3 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -17,8 +17,10 @@ module.exports = function arraysToCalcdata(cd, trace) { Lib.mergeArray(trace.text, cd, 'tx'); Lib.mergeArray(trace.hovertext, cd, 'htx'); + Lib.mergeArray(trace.customdata, cd, 'data'); Lib.mergeArray(trace.textposition, cd, 'tp'); + if(trace.textfont) { Lib.mergeArray(trace.textfont.size, cd, 'ts'); Lib.mergeArray(trace.textfont.color, cd, 'tc'); @@ -38,4 +40,12 @@ module.exports = function arraysToCalcdata(cd, trace) { Lib.mergeArray(markerLine.width, cd, 'mlw'); } } + + if(trace.hoverlabel) { + Lib.mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); + Lib.mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); + Lib.mergeArray(trace.hoverlabel.font.size, cd, 'hts'); + Lib.mergeArray(trace.hoverlabel.font.color, cd, 'htc'); + Lib.mergeArray(trace.hoverlabel.font.family, cd, 'htf'); + } }; From fe50b6fa9b786a10253d4b2a7184a839e47bc889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 11 Apr 2017 14:13:31 -0400 Subject: [PATCH 03/38] add hover label setting to hoverData during `cleanPoint` - to that there (as opposed to in trace module hoverPoints) to have to repeat this logic across all trace modules - note that PR https://github.com/plotly/plotly.js/pull/1573/ already added the relevant `createHoverText` logic --- src/plots/cartesian/graph_interact.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index d1307a8cbb5..7b00d8506b0 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -672,6 +672,9 @@ fx.getClosest = function(cd, distfn, pointData) { }; function cleanPoint(d, hovermode) { + var trace = d.trace || {}; + var cd = d.cd[d.index] || {}; + d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; // then constrain all the positions to be on the plot @@ -749,6 +752,19 @@ function cleanPoint(d, hovermode) { if(infomode.indexOf('name') === -1) d.name = undefined; } + function fill(key, calcKey, traceKey) { + if(cd[calcKey]) return d[key] = cd[calcKey]; + + var traceVal = Lib.nestedProperty(trace, traceKey).get(); + if(traceVal) return d[key] = traceVal; + } + + fill('color', 'hbg', 'hoverlabel.bgcolor'); + fill('borderColor', 'hbc', 'hoverlabel.bordercolor'); + fill('fontFamily', 'htf', 'hoverlabel.font.family'); + fill('fontSize', 'hts', 'hoverlabel.font.size'); + fill('fontColor', 'htc', 'hoverlabel.font.color'); + return d; } From ffde987f6a49b5962284bb13f1c784f6c890bec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 13:54:17 -0400 Subject: [PATCH 04/38] break up cartesian graph_interact.js: - into component/fx/ - keep old 'init' routine in graph_interact.js - mv hover constants out of cartesian constants.js and into fx/ --- src/components/fx/click.js | 27 + src/components/fx/constants.js | 26 + src/components/fx/helpers.js | 81 ++ src/components/fx/hover.js | 1287 ++++++++++++++++++++++ src/components/fx/index.js | 46 + src/components/fx/layout_defaults.js | 46 + src/plotly.js | 4 +- src/plots/cartesian/constants.js | 15 - src/plots/cartesian/graph_interact.js | 1432 +------------------------ 9 files changed, 1524 insertions(+), 1440 deletions(-) create mode 100644 src/components/fx/click.js create mode 100644 src/components/fx/constants.js create mode 100644 src/components/fx/helpers.js create mode 100644 src/components/fx/hover.js create mode 100644 src/components/fx/index.js create mode 100644 src/components/fx/layout_defaults.js diff --git a/src/components/fx/click.js b/src/components/fx/click.js new file mode 100644 index 00000000000..0fb3ed79828 --- /dev/null +++ b/src/components/fx/click.js @@ -0,0 +1,27 @@ +/** +* 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 Registry = require('../../registry'); + +module.exports = function click(gd, evt) { + var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata); + + function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, event: evt}); } + + if(gd._hoverdata && evt && evt.target) { + if(annotationsDone && annotationsDone.then) { + annotationsDone.then(emitClick); + } + else emitClick(); + + // why do we get a double event without this??? + if(evt.stopImmediatePropagation) evt.stopImmediatePropagation(); + } +}; diff --git a/src/components/fx/constants.js b/src/components/fx/constants.js new file mode 100644 index 00000000000..03d726e42da --- /dev/null +++ b/src/components/fx/constants.js @@ -0,0 +1,26 @@ +/** +* 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'; + +module.exports = { + // max pixels away from mouse to allow a point to highlight + MAXDIST: 20, + + // hover labels for multiple horizontal bars get tilted by this angle + YANGLE: 60, + + // size and display constants for hover text + HOVERARROWSIZE: 6, // pixel size of hover arrows + HOVERTEXTPAD: 3, // pixels padding around text + HOVERFONTSIZE: 13, + HOVERFONT: 'Arial, sans-serif', + + // minimum time (msec) between hover calls + HOVERMINTIME: 50, +}; diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js new file mode 100644 index 00000000000..683009efe30 --- /dev/null +++ b/src/components/fx/helpers.js @@ -0,0 +1,81 @@ +/** +* 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 constants = require('./constants'); + +// look for either subplot or xaxis and yaxis attributes +exports.getSubplot = function getSubplot(trace) { + return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo; +}; + +// convenience functions for mapping all relevant axes +exports.flat = function flat(subplots, v) { + var out = []; + for(var i = subplots.length; i > 0; i--) out.push(v); + return out; +}; + +exports.p2c = function p2c(axArray, v) { + var out = []; + for(var i = 0; i < axArray.length; i++) out.push(axArray[i].p2c(v)); + return out; +}; + +exports.getDistanceFunction = function getDistanceFunction(mode, dx, dy, dxy) { + if(mode === 'closest') return dxy || quadrature(dx, dy); + return mode === 'x' ? dx : dy; +}; + +exports.getClosest = function getClosest(cd, distfn, pointData) { + // do we already have a point number? (array mode only) + if(pointData.index !== false) { + if(pointData.index >= 0 && pointData.index < cd.length) { + pointData.distance = 0; + } + else pointData.index = false; + } + else { + // apply the distance function to each data point + // this is the longest loop... if this bogs down, we may need + // to create pre-sorted data (by x or y), not sure how to + // do this for 'closest' + for(var i = 0; i < cd.length; i++) { + var newDistance = distfn(cd[i]); + if(newDistance <= pointData.distance) { + pointData.index = i; + pointData.distance = newDistance; + } + } + } + return pointData; +}; + +// for bar charts and others with finite-size objects: you must be inside +// it to see its hover info, so distance is infinite outside. +// But make distance inside be at least 1/4 MAXDIST, and a little bigger +// for bigger bars, to prioritize scatter and smaller bars over big bars +// +// note that for closest mode, two inbox's will get added in quadrature +// args are (signed) difference from the two opposite edges +// count one edge as in, so that over continuous ranges you never get a gap +exports.inbox = function inbox(v0, v1) { + if(v0 * v1 < 0 || v0 === 0) { + return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); + } + return Infinity; +}; + +function quadrature(dx, dy) { + return function(di) { + var x = dx(di), + y = dy(di); + return Math.sqrt(x * x + y * y); + }; +} diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js new file mode 100644 index 00000000000..803b9e33e89 --- /dev/null +++ b/src/components/fx/hover.js @@ -0,0 +1,1287 @@ +/** +* 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 d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); + +var Lib = require('../../lib'); +var Events = require('../../lib/events'); +var svgTextUtils = require('../../lib/svg_text_utils'); +var overrideCursor = require('../../lib/override_cursor'); +var Drawing = require('../drawing'); +var Color = require('../color'); +var dragElement = require('../dragelement'); +var Axes = require('../../plots/cartesian/axes'); +var Registry = require('../../registry'); + +var helpers = require('./helpers'); +var constants = require('./constants'); + +// hover labels for multiple horizontal bars get tilted by some angle, +// then need to be offset differently if they overlap +var YANGLE = constants.YANGLE; +var YA_RADIANS = Math.PI * YANGLE / 180; + +// expansion of projected height +var YFACTOR = 1 / Math.sin(YA_RADIANS); + +// to make the appropriate post-rotation x offset, +// you need both x and y offsets +var YSHIFTX = Math.cos(YA_RADIANS); +var YSHIFTY = Math.sin(YA_RADIANS); + +// size and display constants for hover text +var HOVERARROWSIZE = constants.HOVERARROWSIZE; +var HOVERTEXTPAD = constants.HOVERTEXTPAD; + +// fx.hover: highlight data on hover +// evt can be a mousemove event, or an object with data about what points +// to hover on +// {xpx,ypx[,hovermode]} - pixel locations from top left +// (with optional overriding hovermode) +// {xval,yval[,hovermode]} - data values +// [{curveNumber,(pointNumber|xval and/or yval)}] - +// array of specific points to highlight +// pointNumber is a single integer if gd.data[curveNumber] is 1D, +// or a two-element array if it's 2D +// xval and yval are data values, +// 1D data may specify either or both, +// 2D data must specify both +// subplot is an id string (default "xy") +// makes use of gl.hovermode, which can be: +// x (find the points with the closest x values, ie a column), +// closest (find the single closest point) +// internally there are two more that occasionally get used: +// y (pick out a row - only used for multiple horizontal bar charts) +// array (used when the user specifies an explicit +// array of points to hover on) +// +// 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) { + if(typeof gd === 'string') gd = document.getElementById(gd); + if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0; + + // 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); + 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); + gd._lastHoverTime = Date.now(); + gd._hoverTimer = undefined; + }, constants.HOVERMINTIME); +}; + +/* + * Draw a single hover item in a pre-existing svg container somewhere + * hoverItem should have keys: + * - x and y (or x0, x1, y0, and y1): + * the pixel position to mark, relative to opts.container + * - xLabel, yLabel, zLabel, text, and name: + * info to go in the label + * - color: + * the background color for the label. + * - idealAlign (optional): + * 'left' or 'right' for which side of the x/y box to try to put this on first + * - borderColor (optional): + * color for the border, defaults to strongest contrast with color + * - fontFamily (optional): + * string, the font for this label, defaults to constants.HOVERFONT + * - fontSize (optional): + * the label font size, defaults to constants.HOVERFONTSIZE + * - fontColor (optional): + * defaults to borderColor + * opts should have keys: + * - bgColor: + * the background color this is against, used if the trace is + * non-opaque, and for the name, which goes outside the box + * - container: + * a or element to add the hover label to + * - outerContainer: + * normally a parent of `container`, sets the bounding box to use to + * constrain the hover label and determine whether to show it on the left or right + */ +exports.loneHover = function loneHover(hoverItem, opts) { + var pointData = { + color: hoverItem.color || Color.defaultLine, + x0: hoverItem.x0 || hoverItem.x || 0, + x1: hoverItem.x1 || hoverItem.x || 0, + y0: hoverItem.y0 || hoverItem.y || 0, + y1: hoverItem.y1 || hoverItem.y || 0, + xLabel: hoverItem.xLabel, + yLabel: hoverItem.yLabel, + zLabel: hoverItem.zLabel, + text: hoverItem.text, + name: hoverItem.name, + idealAlign: hoverItem.idealAlign, + + // optional extra bits of styling + borderColor: hoverItem.borderColor, + fontFamily: hoverItem.fontFamily, + fontSize: hoverItem.fontSize, + fontColor: hoverItem.fontColor, + + // filler to make createHoverText happy + trace: { + index: 0, + hoverinfo: '' + }, + xa: {_offset: 0}, + ya: {_offset: 0}, + index: 0 + }; + + var container3 = d3.select(opts.container), + outerContainer3 = opts.outerContainer ? + d3.select(opts.outerContainer) : container3; + + var fullOpts = { + hovermode: 'closest', + rotateLabels: false, + bgColor: opts.bgColor || Color.background, + container: container3, + outerContainer: outerContainer3 + }; + + var hoverLabel = createHoverText([pointData], fullOpts); + alignHoverText(hoverLabel, fullOpts.rotateLabels); + + return hoverLabel.node(); +}; + +// The actual implementation is here: +function _hover(gd, evt, subplot) { + if(subplot === 'pie') { + gd.emit('plotly_hover', { + event: evt.originalEvent, + points: [evt] + }); + return; + } + + if(!subplot) subplot = 'xy'; + + // if the user passed in an array of subplots, + // use those instead of finding overlayed plots + var subplots = Array.isArray(subplot) ? subplot : [subplot]; + + var fullLayout = gd._fullLayout, + plots = fullLayout._plots || [], + plotinfo = plots[subplot]; + + // list of all overlaid subplots to look at + if(plotinfo) { + var overlayedSubplots = plotinfo.overlays.map(function(pi) { + return pi.id; + }); + + subplots = subplots.concat(overlayedSubplots); + } + + var len = subplots.length, + xaArray = new Array(len), + yaArray = new Array(len); + + for(var i = 0; i < len; i++) { + var spId = subplots[i]; + + // 'cartesian' case + var plotObj = plots[spId]; + if(plotObj) { + + // TODO make sure that fullLayout_plots axis refs + // get updated properly so that we don't have + // to use Axes.getFromId in general. + + xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id); + yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id); + continue; + } + + // other subplot types + var _subplot = fullLayout[spId]._subplot; + xaArray[i] = _subplot.xaxis; + yaArray[i] = _subplot.yaxis; + } + + var hovermode = evt.hovermode || fullLayout.hovermode; + + if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata || + gd.querySelector('.zoombox') || gd._dragging) { + return dragElement.unhoverRaw(gd, evt); + } + + // hoverData: the set of candidate points we've found to highlight + var hoverData = [], + + // searchData: the data to search in. Mostly this is just a copy of + // gd.calcdata, filtered to the subplot and overlays we're on + // but if a point array is supplied it will be a mapping + // of indicated curves + searchData = [], + + // [x|y]valArray: the axis values of the hover event + // mapped onto each of the currently selected overlaid subplots + xvalArray, + yvalArray, + + // used in loops + itemnum, + curvenum, + cd, + trace, + subplotId, + subploti, + mode, + xval, + yval, + pointData, + closedataPreviousLength; + + // Figure out what we're hovering on: + // mouse location or user-supplied data + + if(Array.isArray(evt)) { + // user specified an array of points to highlight + hovermode = 'array'; + for(itemnum = 0; itemnum < evt.length; itemnum++) { + cd = gd.calcdata[evt[itemnum].curveNumber||0]; + if(cd[0].trace.hoverinfo !== 'skip') { + searchData.push(cd); + } + } + } + else { + for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) { + cd = gd.calcdata[curvenum]; + trace = cd[0].trace; + if(trace.hoverinfo !== 'skip' && subplots.indexOf(helpers.getSubplot(trace)) !== -1) { + searchData.push(cd); + } + } + + // [x|y]px: the pixels (from top left) of the mouse location + // on the currently selected plot area + var hasUserCalledHover = !evt.target, + xpx, ypx; + + if(hasUserCalledHover) { + if('xpx' in evt) xpx = evt.xpx; + else xpx = xaArray[0]._length / 2; + + if('ypx' in evt) ypx = evt.ypx; + else ypx = yaArray[0]._length / 2; + } + else { + // fire the beforehover event and quit if it returns false + // note that we're only calling this on real mouse events, so + // manual calls to fx.hover will always run. + if(Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { + return; + } + + var dbb = evt.target.getBoundingClientRect(); + + xpx = evt.clientX - dbb.left; + ypx = evt.clientY - dbb.top; + + // in case hover was called from mouseout into hovertext, + // it's possible you're not actually over the plot anymore + if(xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) { + return dragElement.unhoverRaw(gd, evt); + } + } + + if('xval' in evt) xvalArray = helpers.flat(subplots, evt.xval); + else xvalArray = helpers.p2c(xaArray, xpx); + + if('yval' in evt) yvalArray = helpers.flat(subplots, evt.yval); + else yvalArray = helpers.p2c(yaArray, ypx); + + if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { + Lib.warn('Fx.hover failed', evt, gd); + return dragElement.unhoverRaw(gd, evt); + } + } + + // the pixel distance to beat as a matching point + // in 'x' or 'y' mode this resets for each trace + var distance = Infinity; + + // find the closest point in each trace + // this is minimum dx and/or dy, depending on mode + // and the pixel position for the label (labelXpx, labelYpx) + for(curvenum = 0; curvenum < searchData.length; curvenum++) { + cd = searchData[curvenum]; + + // filter out invisible or broken data + if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; + + trace = cd[0].trace; + + // Explicitly bail out for these two. I don't know how to otherwise prevent + // the rest of this function from running and failing + if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; + + subplotId = helpers.getSubplot(trace); + subploti = subplots.indexOf(subplotId); + + // within one trace mode can sometimes be overridden + mode = hovermode; + + // container for new point, also used to pass info into module.hoverPoints + pointData = { + // trace properties + cd: cd, + trace: trace, + xa: xaArray[subploti], + ya: yaArray[subploti], + name: (gd.data.length > 1 || trace.hoverinfo.indexOf('name') !== -1) ? trace.name : undefined, + // point properties - override all of these + index: false, // point index in trace - only used by plotly.js hoverdata consumers + distance: Math.min(distance, constants.MAXDIST), // pixel distance or pseudo-distance + color: Color.defaultLine, // trace color + x0: undefined, + x1: undefined, + y0: undefined, + y1: undefined, + xLabelVal: undefined, + yLabelVal: undefined, + zLabelVal: undefined, + text: undefined + }; + + // add ref to subplot object (non-cartesian case) + if(fullLayout[subplotId]) { + pointData.subplot = fullLayout[subplotId]._subplot; + } + + closedataPreviousLength = hoverData.length; + + // for a highlighting array, figure out what + // we're searching for with this element + if(mode === 'array') { + var selection = evt[curvenum]; + if('pointNumber' in selection) { + pointData.index = selection.pointNumber; + mode = 'closest'; + } + else { + mode = ''; + if('xval' in selection) { + xval = selection.xval; + mode = 'x'; + } + if('yval' in selection) { + yval = selection.yval; + mode = mode ? 'closest' : 'y'; + } + } + } + else { + xval = xvalArray[subploti]; + yval = yvalArray[subploti]; + } + + // Now find the points. + if(trace._module && trace._module.hoverPoints) { + var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode); + if(newPoints) { + var newPoint; + for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) { + newPoint = newPoints[newPointNum]; + if(isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) { + hoverData.push(cleanPoint(newPoint, hovermode)); + } + } + } + } + else { + Lib.log('Unrecognized trace type in hover:', trace); + } + + // in closest mode, remove any existing (farther) points + // and don't look any farther than this latest point (or points, if boxes) + if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) { + hoverData.splice(0, closedataPreviousLength); + distance = hoverData[0].distance; + } + } + + // nothing left: remove all labels and quit + if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); + + hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); + + // lastly, emit custom hover/unhover events + var oldhoverdata = gd._hoverdata, + newhoverdata = []; + + // pull out just the data that's useful to + // other people and send it to the event + for(itemnum = 0; itemnum < hoverData.length; itemnum++) { + var pt = hoverData[itemnum]; + + var out = { + data: pt.trace._input, + fullData: pt.trace, + curveNumber: pt.trace.index, + pointNumber: pt.index + }; + + if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt); + else { + out.x = pt.xVal; + out.y = pt.yVal; + out.xaxis = pt.xa; + out.yaxis = pt.ya; + + if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal; + } + + newhoverdata.push(out); + } + + gd._hoverdata = newhoverdata; + + if(hoverChanged(gd, evt, oldhoverdata) && fullLayout._hasCartesian) { + var spikelineOpts = { + hovermode: hovermode, + fullLayout: fullLayout, + container: fullLayout._hoverlayer, + outerContainer: fullLayout._paperdiv + }; + createSpikelines(hoverData, spikelineOpts); + } + + // if there's more than one horz bar trace, + // rotate the labels so they don't overlap + var rotateLabels = hovermode === 'y' && searchData.length > 1; + + var bgColor = Color.combine( + fullLayout.plot_bgcolor || Color.background, + fullLayout.paper_bgcolor + ); + + var labelOpts = { + hovermode: hovermode, + rotateLabels: rotateLabels, + bgColor: bgColor, + container: fullLayout._hoverlayer, + outerContainer: fullLayout._paperdiv + }; + + var hoverLabels = createHoverText(hoverData, labelOpts); + + hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); + + alignHoverText(hoverLabels, rotateLabels); + + // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true + // we should improve the "fx" API so other plots can use it without these hack. + if(evt.target && evt.target.tagName) { + var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata); + overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : ''); + } + + // don't emit events if called manually + if(!evt.target || !hoverChanged(gd, evt, oldhoverdata)) return; + + if(oldhoverdata) { + gd.emit('plotly_unhover', { + event: evt, + points: oldhoverdata + }); + } + + gd.emit('plotly_hover', { + event: evt, + points: gd._hoverdata, + xaxes: xaArray, + yaxes: yaArray, + xvals: xvalArray, + yvals: yvalArray + }); +} + +function createHoverText(hoverData, opts) { + var hovermode = opts.hovermode, + rotateLabels = opts.rotateLabels, + bgColor = opts.bgColor, + container = opts.container, + outerContainer = opts.outerContainer, + + // opts.fontFamily/Size are used for the common label + // and as defaults for each hover label, though the individual labels + // can override this. + fontFamily = opts.fontFamily || constants.HOVERFONT, + fontSize = opts.fontSize || constants.HOVERFONTSIZE, + + c0 = hoverData[0], + xa = c0.xa, + ya = c0.ya, + commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel', + t0 = c0[commonAttr], + t00 = (String(t0) || '').split(' ')[0], + outerContainerBB = outerContainer.node().getBoundingClientRect(), + outerTop = outerContainerBB.top, + outerWidth = outerContainerBB.width, + outerHeight = outerContainerBB.height; + + // show the common label, if any, on the axis + // never show a common label in array mode, + // even if sometimes there could be one + var showCommonLabel = c0.distance <= constants.MAXDIST && + (hovermode === 'x' || hovermode === 'y'); + + // all hover traces hoverinfo must contain the hovermode + // to have common labels + var i, traceHoverinfo; + for(i = 0; i < hoverData.length; i++) { + traceHoverinfo = hoverData[i].trace.hoverinfo; + var parts = traceHoverinfo.split('+'); + if(parts.indexOf('all') === -1 && + parts.indexOf(hovermode) === -1) { + showCommonLabel = false; + break; + } + } + + var commonLabel = container.selectAll('g.axistext') + .data(showCommonLabel ? [0] : []); + commonLabel.enter().append('g') + .classed('axistext', true); + commonLabel.exit().remove(); + + commonLabel.each(function() { + var label = d3.select(this), + lpath = label.selectAll('path').data([0]), + ltext = label.selectAll('text').data([0]); + + lpath.enter().append('path') + .style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background}); + ltext.enter().append('text') + .call(Drawing.font, fontFamily, fontSize, Color.background) + // prohibit tex interpretation until we can handle + // tex and regular text together + .attr('data-notex', 1); + + ltext.text(t0) + .call(svgTextUtils.convertToTspans) + .call(Drawing.setPosition, 0, 0) + .selectAll('tspan.line') + .call(Drawing.setPosition, 0, 0); + label.attr('transform', ''); + + var tbb = ltext.node().getBoundingClientRect(); + if(hovermode === 'x') { + ltext.attr('text-anchor', 'middle') + .call(Drawing.setPosition, 0, (xa.side === 'top' ? + (outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD) : + (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))) + .selectAll('tspan.line') + .attr({ + x: ltext.attr('x'), + y: ltext.attr('y') + }); + + var topsign = xa.side === 'top' ? '-' : ''; + lpath.attr('d', 'M0,0' + + 'L' + HOVERARROWSIZE + ',' + topsign + HOVERARROWSIZE + + 'H' + (HOVERTEXTPAD + tbb.width / 2) + + 'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) + + 'H-' + (HOVERTEXTPAD + tbb.width / 2) + + 'V' + topsign + HOVERARROWSIZE + 'H-' + HOVERARROWSIZE + 'Z'); + + label.attr('transform', 'translate(' + + (xa._offset + (c0.x0 + c0.x1) / 2) + ',' + + (ya._offset + (xa.side === 'top' ? 0 : ya._length)) + ')'); + } + else { + ltext.attr('text-anchor', ya.side === 'right' ? 'start' : 'end') + .call(Drawing.setPosition, + (ya.side === 'right' ? 1 : -1) * (HOVERTEXTPAD + HOVERARROWSIZE), + outerTop - tbb.top - tbb.height / 2) + .selectAll('tspan.line') + .attr({ + x: ltext.attr('x'), + y: ltext.attr('y') + }); + + var leftsign = ya.side === 'right' ? '' : '-'; + lpath.attr('d', 'M0,0' + + 'L' + leftsign + HOVERARROWSIZE + ',' + HOVERARROWSIZE + + 'V' + (HOVERTEXTPAD + tbb.height / 2) + + 'h' + leftsign + (HOVERTEXTPAD * 2 + tbb.width) + + 'V-' + (HOVERTEXTPAD + tbb.height / 2) + + 'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z'); + + label.attr('transform', 'translate(' + + (xa._offset + (ya.side === 'right' ? xa._length : 0)) + ',' + + (ya._offset + (c0.y0 + c0.y1) / 2) + ')'); + } + // remove the "close but not quite" points + // because of error bars, only take up to a space + hoverData = hoverData.filter(function(d) { + return (d.zLabelVal !== undefined) || + (d[commonAttr] || '').split(' ')[0] === t00; + }); + }); + + // show all the individual labels + + // first create the objects + var hoverLabels = container.selectAll('g.hovertext') + .data(hoverData, function(d) { + return [d.trace.index, d.index, d.x0, d.y0, d.name, d.attr, d.xa, d.ya || ''].join(','); + }); + hoverLabels.enter().append('g') + .classed('hovertext', true) + .each(function() { + var g = d3.select(this); + // trace name label (rect and text.name) + g.append('rect') + .call(Color.fill, Color.addOpacity(bgColor, 0.8)); + g.append('text').classed('name', true); + // trace data label (path and text.nums) + g.append('path') + .style('stroke-width', '1px'); + g.append('text').classed('nums', true) + .call(Drawing.font, fontFamily, fontSize); + }); + hoverLabels.exit().remove(); + + // then put the text in, position the pointer to the data, + // and figure out sizes + hoverLabels.each(function(d) { + var g = d3.select(this).attr('transform', ''), + name = '', + text = '', + // combine possible non-opaque trace color with bgColor + baseColor = Color.opacity(d.color) ? + d.color : Color.defaultLine, + traceColor = Color.combine(baseColor, bgColor), + + // find a contrasting color for border and text + contrastColor = d.borderColor || Color.contrast(traceColor); + + // to get custom 'name' labels pass cleanPoint + if(d.nameOverride !== undefined) d.name = d.nameOverride; + + if(d.name && d.zLabelVal === undefined) { + // strip out our pseudo-html elements from d.name (if it exists at all) + name = svgTextUtils.plainText(d.name || ''); + + if(name.length > 15) name = name.substr(0, 12) + '...'; + } + + // used by other modules (initially just ternary) that + // manage their own hoverinfo independent of cleanPoint + // the rest of this will still apply, so such modules + // can still put things in (x|y|z)Label, text, and name + // and hoverinfo will still determine their visibility + if(d.extraText !== undefined) text += d.extraText; + + if(d.zLabel !== undefined) { + if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; + if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; + text += (text ? 'z: ' : '') + d.zLabel; + } + else if(showCommonLabel && d[hovermode + 'Label'] === t0) { + text = d[(hovermode === 'x' ? 'y' : 'x') + 'Label'] || ''; + } + else if(d.xLabel === undefined) { + if(d.yLabel !== undefined) text = d.yLabel; + } + else if(d.yLabel === undefined) text = d.xLabel; + else text = '(' + d.xLabel + ', ' + d.yLabel + ')'; + + if(d.text && !Array.isArray(d.text)) text += (text ? '
' : '') + d.text; + + // if 'text' is empty at this point, + // put 'name' in main label and don't show secondary label + if(text === '') { + // if 'name' is also empty, remove entire label + if(name === '') g.remove(); + text = name; + } + + // main label + var tx = g.select('text.nums') + .call(Drawing.font, + d.fontFamily || fontFamily, + d.fontSize || fontSize, + d.fontColor || contrastColor) + .call(Drawing.setPosition, 0, 0) + .text(text) + .attr('data-notex', 1) + .call(svgTextUtils.convertToTspans); + tx.selectAll('tspan.line') + .call(Drawing.setPosition, 0, 0); + + var tx2 = g.select('text.name'), + tx2width = 0; + + // secondary label for non-empty 'name' + if(name && name !== text) { + tx2.call(Drawing.font, + d.fontFamily || fontFamily, + d.fontSize || fontSize, + traceColor) + .text(name) + .call(Drawing.setPosition, 0, 0) + .attr('data-notex', 1) + .call(svgTextUtils.convertToTspans); + tx2.selectAll('tspan.line') + .call(Drawing.setPosition, 0, 0); + tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; + } + else { + tx2.remove(); + g.select('rect').remove(); + } + + g.select('path') + .style({ + fill: traceColor, + stroke: contrastColor + }); + var tbb = tx.node().getBoundingClientRect(), + htx = d.xa._offset + (d.x0 + d.x1) / 2, + hty = d.ya._offset + (d.y0 + d.y1) / 2, + dx = Math.abs(d.x1 - d.x0), + dy = Math.abs(d.y1 - d.y0), + txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width, + anchorStartOK, + anchorEndOK; + + d.ty0 = outerTop - tbb.top; + d.bx = tbb.width + 2 * HOVERTEXTPAD; + d.by = tbb.height + 2 * HOVERTEXTPAD; + d.anchor = 'start'; + d.txwidth = tbb.width; + d.tx2width = tx2width; + d.offset = 0; + + if(rotateLabels) { + d.pos = htx; + anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; + anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; + if((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { + hty -= dy / 2; + d.anchor = 'end'; + } else if(anchorStartOK) { + hty += dy / 2; + d.anchor = 'start'; + } else d.anchor = 'middle'; + } + else { + d.pos = hty; + anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; + anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; + if((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { + htx -= dx / 2; + d.anchor = 'end'; + } else if(anchorStartOK) { + htx += dx / 2; + d.anchor = 'start'; + } else d.anchor = 'middle'; + } + + tx.attr('text-anchor', d.anchor); + if(tx2width) tx2.attr('text-anchor', d.anchor); + g.attr('transform', 'translate(' + htx + ',' + hty + ')' + + (rotateLabels ? 'rotate(' + YANGLE + ')' : '')); + }); + + return hoverLabels; +} + +// Make groups of touching points, and within each group +// move each point so that no labels overlap, but the average +// label position is the same as it was before moving. Indicentally, +// this is equivalent to saying all the labels are on equal linear +// springs about their initial position. Initially, each point is +// its own group, but as we find overlaps we will clump the points. +// +// Also, there are hard constraints at the edges of the graphs, +// that push all groups to the middle so they are visible. I don't +// know what happens if the group spans all the way from one edge to +// the other, though it hardly matters - there's just too much +// information then. +function hoverAvoidOverlaps(hoverData, ax) { + var nummoves = 0, + + // make groups of touching points + pointgroups = hoverData + .map(function(d, i) { + var axis = d[ax]; + return [{ + i: i, + dp: 0, + pos: d.pos, + posref: d.posref, + size: d.by * (axis._id.charAt(0) === 'x' ? YFACTOR : 1) / 2, + pmin: axis._offset, + pmax: axis._offset + axis._length + }]; + }) + .sort(function(a, b) { return a[0].posref - b[0].posref; }), + donepositioning, + topOverlap, + bottomOverlap, + i, j, + pti, + sumdp; + + function constrainGroup(grp) { + var minPt = grp[0], + maxPt = grp[grp.length - 1]; + + // overlap with the top - positive vals are overlaps + topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size; + + // overlap with the bottom - positive vals are overlaps + bottomOverlap = maxPt.pos + maxPt.dp + maxPt.size - minPt.pmax; + + // check for min overlap first, so that we always + // see the largest labels + // allow for .01px overlap, so we don't get an + // infinite loop from rounding errors + if(topOverlap > 0.01) { + for(j = grp.length - 1; j >= 0; j--) grp[j].dp += topOverlap; + donepositioning = false; + } + if(bottomOverlap < 0.01) return; + if(topOverlap < -0.01) { + // make sure we're not pushing back and forth + for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; + donepositioning = false; + } + if(!donepositioning) return; + + // no room to fix positioning, delete off-screen points + + // first see how many points we need to delete + var deleteCount = 0; + for(i = 0; i < grp.length; i++) { + pti = grp[i]; + if(pti.pos + pti.dp + pti.size > minPt.pmax) deleteCount++; + } + + // start by deleting points whose data is off screen + for(i = grp.length - 1; i >= 0; i--) { + if(deleteCount <= 0) break; + pti = grp[i]; + + // pos has already been constrained to [pmin,pmax] + // so look for points close to that to delete + if(pti.pos > minPt.pmax - 1) { + pti.del = true; + deleteCount--; + } + } + for(i = 0; i < grp.length; i++) { + if(deleteCount <= 0) break; + pti = grp[i]; + + // pos has already been constrained to [pmin,pmax] + // so look for points close to that to delete + if(pti.pos < minPt.pmin + 1) { + pti.del = true; + deleteCount--; + + // shift the whole group minus into this new space + bottomOverlap = pti.size * 2; + for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; + } + } + // then delete points that go off the bottom + for(i = grp.length - 1; i >= 0; i--) { + if(deleteCount <= 0) break; + pti = grp[i]; + if(pti.pos + pti.dp + pti.size > minPt.pmax) { + pti.del = true; + deleteCount--; + } + } + } + + // loop through groups, combining them if they overlap, + // until nothing moves + while(!donepositioning && nummoves <= hoverData.length) { + // to avoid infinite loops, don't move more times + // than there are traces + nummoves++; + + // assume nothing will move in this iteration, + // reverse this if it does + donepositioning = true; + i = 0; + while(i < pointgroups.length - 1) { + // the higher (g0) and lower (g1) point group + var g0 = pointgroups[i], + g1 = pointgroups[i + 1], + + // the lowest point in the higher group (p0) + // the highest point in the lower group (p1) + p0 = g0[g0.length - 1], + p1 = g1[0]; + topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size; + + // Only group points that lie on the same axes + if(topOverlap > 0.01 && (p0.pmin === p1.pmin) && (p0.pmax === p1.pmax)) { + // push the new point(s) added to this group out of the way + for(j = g1.length - 1; j >= 0; j--) g1[j].dp += topOverlap; + + // add them to the group + g0.push.apply(g0, g1); + pointgroups.splice(i + 1, 1); + + // adjust for minimum average movement + sumdp = 0; + for(j = g0.length - 1; j >= 0; j--) sumdp += g0[j].dp; + bottomOverlap = sumdp / g0.length; + for(j = g0.length - 1; j >= 0; j--) g0[j].dp -= bottomOverlap; + donepositioning = false; + } + else i++; + } + + // check if we're going off the plot on either side and fix + pointgroups.forEach(constrainGroup); + } + + // now put these offsets into hoverData + for(i = pointgroups.length - 1; i >= 0; i--) { + var grp = pointgroups[i]; + for(j = grp.length - 1; j >= 0; j--) { + var pt = grp[j], + hoverPt = hoverData[pt.i]; + hoverPt.offset = pt.dp; + hoverPt.del = pt.del; + } + } +} + +function alignHoverText(hoverLabels, rotateLabels) { + // finally set the text positioning relative to the data and draw the + // box around it + hoverLabels.each(function(d) { + var g = d3.select(this); + if(d.del) { + g.remove(); + return; + } + var horzSign = d.anchor === 'end' ? -1 : 1, + tx = g.select('text.nums'), + alignShift = {start: 1, end: -1, middle: 0}[d.anchor], + txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD), + tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD), + offsetX = 0, + offsetY = d.offset; + if(d.anchor === 'middle') { + txx -= d.tx2width / 2; + tx2x -= d.tx2width / 2; + } + if(rotateLabels) { + offsetY *= -YSHIFTY; + offsetX = d.offset * YSHIFTX; + } + + g.select('path').attr('d', d.anchor === 'middle' ? + // middle aligned: rect centered on data + ('M-' + (d.bx / 2) + ',-' + (d.by / 2) + 'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') : + // left or right aligned: side rect with arrow to data + ('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) + + 'v' + (d.by / 2 - HOVERARROWSIZE) + + 'h' + (horzSign * d.bx) + + 'v-' + d.by + + 'H' + (horzSign * HOVERARROWSIZE + offsetX) + + 'V' + (offsetY - HOVERARROWSIZE) + + 'Z')); + + tx.call(Drawing.setPosition, + txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD) + .selectAll('tspan.line') + .attr({ + x: tx.attr('x'), + y: tx.attr('y') + }); + + if(d.tx2width) { + g.select('text.name, text.name tspan.line') + .call(Drawing.setPosition, + tx2x + alignShift * HOVERTEXTPAD + offsetX, + offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); + g.select('rect') + .call(Drawing.setRect, + tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX, + offsetY - d.by / 2 - 1, + d.tx2width, d.by + 2); + } + }); +} + +function cleanPoint(d, hovermode) { + var trace = d.trace || {}; + var cd = d.cd[d.index] || {}; + + d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; + + // then constrain all the positions to be on the plot + d.x0 = Lib.constrain(d.x0, 0, d.xa._length); + d.x1 = Lib.constrain(d.x1, 0, d.xa._length); + d.y0 = Lib.constrain(d.y0, 0, d.ya._length); + d.y1 = Lib.constrain(d.y1, 0, d.ya._length); + + // and convert the x and y label values into objects + // formatted as text, with font info + var logOffScale; + if(d.xLabelVal !== undefined) { + logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0); + var xLabelObj = Axes.tickText(d.xa, + d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover'); + if(logOffScale) { + if(d.xLabelVal === 0) d.xLabel = '0'; + else d.xLabel = '-' + xLabelObj.text; + } + // TODO: should we do something special if the axis calendar and + // the data calendar are different? Somehow display both dates with + // their system names? Right now it will just display in the axis calendar + // but users could add the other one as text. + else d.xLabel = xLabelObj.text; + d.xVal = d.xa.c2d(d.xLabelVal); + } + + if(d.yLabelVal !== undefined) { + logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0); + var yLabelObj = Axes.tickText(d.ya, + d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover'); + if(logOffScale) { + if(d.yLabelVal === 0) d.yLabel = '0'; + else d.yLabel = '-' + yLabelObj.text; + } + // TODO: see above TODO + else d.yLabel = yLabelObj.text; + d.yVal = d.ya.c2d(d.yLabelVal); + } + + if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal); + + // for box means and error bars, add the range to the label + if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) { + var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text; + if(d.xerrneg !== undefined) { + d.xLabel += ' +' + xeText + ' / -' + + Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text; + } + else d.xLabel += ' ± ' + xeText; + + // small distance penalty for error bars, so that if there are + // traces with errors and some without, the error bar label will + // hoist up to the point + if(hovermode === 'x') d.distance += 1; + } + if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) { + var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text; + if(d.yerrneg !== undefined) { + d.yLabel += ' +' + yeText + ' / -' + + Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text; + } + else d.yLabel += ' ± ' + yeText; + + if(hovermode === 'y') d.distance += 1; + } + + var infomode = d.trace.hoverinfo; + if(infomode !== 'all') { + infomode = infomode.split('+'); + if(infomode.indexOf('x') === -1) d.xLabel = undefined; + if(infomode.indexOf('y') === -1) d.yLabel = undefined; + if(infomode.indexOf('z') === -1) d.zLabel = undefined; + if(infomode.indexOf('text') === -1) d.text = undefined; + if(infomode.indexOf('name') === -1) d.name = undefined; + } + + function fill(key, calcKey, traceKey) { + if(cd[calcKey]) return d[key] = cd[calcKey]; + + var traceVal = Lib.nestedProperty(trace, traceKey).get(); + if(traceVal) return d[key] = traceVal; + } + + fill('color', 'hbg', 'hoverlabel.bgcolor'); + fill('borderColor', 'hbc', 'hoverlabel.bordercolor'); + fill('fontFamily', 'htf', 'hoverlabel.font.family'); + fill('fontSize', 'hts', 'hoverlabel.font.size'); + fill('fontColor', 'htc', 'hoverlabel.font.color'); + + return d; +} + +function createSpikelines(hoverData, opts) { + var hovermode = opts.hovermode; + var container = opts.container; + var c0 = hoverData[0]; + var xa = c0.xa; + var ya = c0.ya; + var showX = xa.showspikes; + var showY = ya.showspikes; + + // Remove old spikeline items + container.selectAll('.spikeline').remove(); + + if(hovermode !== 'closest' || !(showX || showY)) return; + + var fullLayout = opts.fullLayout; + var xPoint = xa._offset + (c0.x0 + c0.x1) / 2; + var yPoint = ya._offset + (c0.y0 + c0.y1) / 2; + var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor); + var dfltDashColor = tinycolor.readability(c0.color, contrastColor) < 1.5 ? + Color.contrast(contrastColor) : c0.color; + + if(showY) { + var yMode = ya.spikemode; + var yThickness = ya.spikethickness; + var yColor = ya.spikecolor || dfltDashColor; + var yBB = ya._boundingBox; + var xEdge = ((yBB.left + yBB.right) / 2) < xPoint ? yBB.right : yBB.left; + + if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) { + var xBase = xEdge; + var xEndSpike = xPoint; + if(yMode.indexOf('across') !== -1) { + xBase = ya._counterSpan[0]; + xEndSpike = ya._counterSpan[1]; + } + + // Background horizontal Line (to y-axis) + container.append('line') + .attr({ + 'x1': xBase, + 'x2': xEndSpike, + 'y1': yPoint, + 'y2': yPoint, + 'stroke-width': yThickness + 2, + 'stroke': contrastColor + }) + .classed('spikeline', true) + .classed('crisp', true); + + // Foreground horizontal line (to y-axis) + container.append('line') + .attr({ + 'x1': xBase, + 'x2': xEndSpike, + 'y1': yPoint, + 'y2': yPoint, + 'stroke-width': yThickness, + 'stroke': yColor, + 'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness) + }) + .classed('spikeline', true) + .classed('crisp', true); + } + // Y axis marker + if(yMode.indexOf('marker') !== -1) { + container.append('circle') + .attr({ + 'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness), + 'cy': yPoint, + 'r': yThickness, + 'fill': yColor + }) + .classed('spikeline', true); + } + } + + if(showX) { + var xMode = xa.spikemode; + var xThickness = xa.spikethickness; + var xColor = xa.spikecolor || dfltDashColor; + var xBB = xa._boundingBox; + var yEdge = ((xBB.top + xBB.bottom) / 2) < yPoint ? xBB.bottom : xBB.top; + + if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) { + var yBase = yEdge; + var yEndSpike = yPoint; + if(xMode.indexOf('across') !== -1) { + yBase = xa._counterSpan[0]; + yEndSpike = xa._counterSpan[1]; + } + + // Background vertical line (to x-axis) + container.append('line') + .attr({ + 'x1': xPoint, + 'x2': xPoint, + 'y1': yBase, + 'y2': yEndSpike, + 'stroke-width': xThickness + 2, + 'stroke': contrastColor + }) + .classed('spikeline', true) + .classed('crisp', true); + + // Foreground vertical line (to x-axis) + container.append('line') + .attr({ + 'x1': xPoint, + 'x2': xPoint, + 'y1': yBase, + 'y2': yEndSpike, + 'stroke-width': xThickness, + 'stroke': xColor, + 'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness) + }) + .classed('spikeline', true) + .classed('crisp', true); + } + + // X axis marker + if(xMode.indexOf('marker') !== -1) { + container.append('circle') + .attr({ + 'cx': xPoint, + 'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness), + 'r': xThickness, + 'fill': xColor + }) + .classed('spikeline', true); + } + } +} + +function hoverChanged(gd, evt, oldhoverdata) { + // don't emit any events if nothing changed + if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true; + + for(var i = oldhoverdata.length - 1; i >= 0; i--) { + var oldPt = oldhoverdata[i], + newPt = gd._hoverdata[i]; + if(oldPt.curveNumber !== newPt.curveNumber || + String(oldPt.pointNumber) !== String(newPt.pointNumber)) { + return true; + } + } + return false; +} diff --git a/src/components/fx/index.js b/src/components/fx/index.js new file mode 100644 index 00000000000..0a999a656e2 --- /dev/null +++ b/src/components/fx/index.js @@ -0,0 +1,46 @@ +/** +* 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 d3 = require('d3'); +var Lib = require('../../lib'); +var dragElement = require('../dragelement'); +var helpers = require('./helpers'); + +module.exports = { + moduleType: 'component', + name: 'fx', + + constants: require('./constants'), + + supplyLayoutDefaults: require('./layout_defaults'), + + getDistanceFunction: helpers.getDistanceFunction, + getClosest: helpers.getClosest, + inbox: helpers.inbox, + + hover: require('./hover').hover, + unhover: dragElement.unhover, + + loneHover: require('./hover').loneHover, + loneUnhover: loneUnhover, + + click: require('./click') +}; + +function loneUnhover(containerOrSelection) { + // duck type whether the arg is a d3 selection because ie9 doesn't + // handle instanceof like modern browsers do. + var selection = Lib.isD3Selection(containerOrSelection) ? + containerOrSelection : + d3.select(containerOrSelection); + + selection.selectAll('g.hovertext').remove(); + selection.selectAll('.spikeline').remove(); +} diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js new file mode 100644 index 00000000000..3304f695516 --- /dev/null +++ b/src/components/fx/layout_defaults.js @@ -0,0 +1,46 @@ +/** +* 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 Lib = require('../../lib'); +var layoutAttributes = require('../../plots/layout_attributes'); + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + + coerce('dragmode'); + + var hovermodeDflt; + if(layoutOut._has('cartesian')) { + // flag for 'horizontal' plots: + // determines the state of the mode bar 'compare' hovermode button + layoutOut._isHoriz = isHoriz(fullData); + hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x'; + } + else hovermodeDflt = 'closest'; + + coerce('hovermode', hovermodeDflt); +}; + +function isHoriz(fullData) { + var out = true; + + for(var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if(trace.orientation !== 'h') { + out = false; + break; + } + } + + return out; +} diff --git a/src/plotly.js b/src/plotly.js index 042166b05bc..c8947f2dca4 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -24,8 +24,10 @@ exports.defaultConfig = require('./plot_api/plot_config'); // plots exports.Plots = require('./plots/plots'); exports.Axes = require('./plots/cartesian/axes'); -exports.Fx = require('./plots/cartesian/graph_interact'); + +// components exports.ModeBar = require('./components/modebar'); +exports.Fx = require('./components/fx'); // plot api require('./plot_api/plot_api'); diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 2934d4ecd0f..d72f7c2fd7b 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -41,21 +41,6 @@ module.exports = { // width of axis drag regions DRAGGERSIZE: 20, - // max pixels away from mouse to allow a point to highlight - MAXDIST: 20, - - // hover labels for multiple horizontal bars get tilted by this angle - YANGLE: 60, - - // size and display constants for hover text - HOVERARROWSIZE: 6, // pixel size of hover arrows - HOVERTEXTPAD: 3, // pixels padding around text - HOVERFONTSIZE: 13, - HOVERFONT: 'Arial, sans-serif', - - // minimum time (msec) between hover calls - HOVERMINTIME: 50, - // max pixels off straight before a lasso select line counts as bent BENDPX: 1.5, diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 3e6a9c77eb3..410e693fe09 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -9,67 +9,15 @@ 'use strict'; -var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); -var Lib = require('../../lib'); -var Events = require('../../lib/events'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); +var Fx = require('../../components/fx'); var dragElement = require('../../components/dragelement'); -var overrideCursor = require('../../lib/override_cursor'); -var Registry = require('../../registry'); -var Axes = require('./axes'); var constants = require('./constants'); var dragBox = require('./dragbox'); -var layoutAttributes = require('../layout_attributes'); - -var fx = module.exports = {}; - -// TODO remove this in version 2.0 -// copy on Fx for backward compatible -fx.unhover = dragElement.unhover; - -fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); - } - - coerce('dragmode'); - - var hovermodeDflt; - if(layoutOut._has('cartesian')) { - // flag for 'horizontal' plots: - // determines the state of the mode bar 'compare' hovermode button - var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); - hovermodeDflt = isHoriz ? 'y' : 'x'; - } - else hovermodeDflt = 'closest'; - - coerce('hovermode', hovermodeDflt); -}; - -fx.isHoriz = function(fullData) { - var isHoriz = true; - - for(var i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - - if(trace.orientation !== 'h') { - isHoriz = false; - break; - } - } - - return isHoriz; -}; - -fx.init = function(gd) { +module.exports = function initInteractions(gd) { var fullLayout = gd._fullLayout; if(!fullLayout._has('cartesian') || gd._context.staticPlot) return; @@ -118,11 +66,11 @@ fx.init = function(gd) { // changes by the time this is called again. gd._fullLayout._rehover = function() { if(gd._fullLayout._hoversubplot === subplot) { - fx.hover(gd, evt, subplot); + Fx.hover(gd, evt, subplot); } }; - fx.hover(gd, evt, subplot); + Fx.hover(gd, evt, subplot); // Note that we have *not* used the cached fullLayout variable here // since that may be outdated when this is called as a callback later on @@ -149,7 +97,7 @@ fx.init = function(gd) { }; maindrag.onclick = function(evt) { - fx.click(gd, evt); + Fx.click(gd, evt); }; // corner draggers @@ -189,19 +137,19 @@ fx.init = function(gd) { } }); - // In case you mousemove over some hovertext, send it to fx.hover too + // In case you mousemove over some hovertext, send it to Fx.hover too // we do this so that we can put the hover text in front of everything, // but still be able to interact with everything as if it isn't there var hoverLayer = fullLayout._hoverlayer.node(); hoverLayer.onmousemove = function(evt) { evt.target = fullLayout._lasthover; - fx.hover(gd, evt, fullLayout._hoversubplot); + Fx.hover(gd, evt, fullLayout._hoversubplot); }; hoverLayer.onclick = function(evt) { evt.target = fullLayout._lasthover; - fx.click(gd, evt); + Fx.click(gd, evt); }; // also delegate mousedowns... TODO: does this actually work? @@ -209,1367 +157,3 @@ fx.init = function(gd) { fullLayout._lasthover.onmousedown(evt); }; }; - -// hover labels for multiple horizontal bars get tilted by some angle, -// then need to be offset differently if they overlap -var YANGLE = constants.YANGLE, - YA_RADIANS = Math.PI * YANGLE / 180, - - // expansion of projected height - YFACTOR = 1 / Math.sin(YA_RADIANS), - - // to make the appropriate post-rotation x offset, - // you need both x and y offsets - YSHIFTX = Math.cos(YA_RADIANS), - YSHIFTY = Math.sin(YA_RADIANS); - -// convenience functions for mapping all relevant axes -function flat(subplots, v) { - var out = []; - for(var i = subplots.length; i > 0; i--) out.push(v); - return out; -} - -function p2c(axArray, v) { - var out = []; - for(var i = 0; i < axArray.length; i++) out.push(axArray[i].p2c(v)); - return out; -} - -function quadrature(dx, dy) { - return function(di) { - var x = dx(di), - y = dy(di); - return Math.sqrt(x * x + y * y); - }; -} - -// size and display constants for hover text -var HOVERARROWSIZE = constants.HOVERARROWSIZE, - HOVERTEXTPAD = constants.HOVERTEXTPAD; - -// fx.hover: highlight data on hover -// evt can be a mousemove event, or an object with data about what points -// to hover on -// {xpx,ypx[,hovermode]} - pixel locations from top left -// (with optional overriding hovermode) -// {xval,yval[,hovermode]} - data values -// [{curveNumber,(pointNumber|xval and/or yval)}] - -// array of specific points to highlight -// pointNumber is a single integer if gd.data[curveNumber] is 1D, -// or a two-element array if it's 2D -// xval and yval are data values, -// 1D data may specify either or both, -// 2D data must specify both -// subplot is an id string (default "xy") -// makes use of gl.hovermode, which can be: -// x (find the points with the closest x values, ie a column), -// closest (find the single closest point) -// internally there are two more that occasionally get used: -// y (pick out a row - only used for multiple horizontal bar charts) -// array (used when the user specifies an explicit -// array of points to hover on) -// -// We wrap the hovers in a timer, to limit their frequency. -// The actual rendering is done by private functions -// hover() and unhover(). - -fx.hover = function(gd, evt, subplot) { - if(typeof gd === 'string') gd = document.getElementById(gd); - if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0; - - // 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); - 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); - gd._lastHoverTime = Date.now(); - gd._hoverTimer = undefined; - }, constants.HOVERMINTIME); -}; - -// The actual implementation is here: - -function hover(gd, evt, subplot) { - if(subplot === 'pie') { - gd.emit('plotly_hover', { - event: evt.originalEvent, - points: [evt] - }); - return; - } - - if(!subplot) subplot = 'xy'; - - // if the user passed in an array of subplots, - // use those instead of finding overlayed plots - var subplots = Array.isArray(subplot) ? subplot : [subplot]; - - var fullLayout = gd._fullLayout, - plots = fullLayout._plots || [], - plotinfo = plots[subplot]; - - // list of all overlaid subplots to look at - if(plotinfo) { - var overlayedSubplots = plotinfo.overlays.map(function(pi) { - return pi.id; - }); - - subplots = subplots.concat(overlayedSubplots); - } - - var len = subplots.length, - xaArray = new Array(len), - yaArray = new Array(len); - - for(var i = 0; i < len; i++) { - var spId = subplots[i]; - - // 'cartesian' case - var plotObj = plots[spId]; - if(plotObj) { - - // TODO make sure that fullLayout_plots axis refs - // get updated properly so that we don't have - // to use Axes.getFromId in general. - - xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id); - yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id); - continue; - } - - // other subplot types - var _subplot = fullLayout[spId]._subplot; - xaArray[i] = _subplot.xaxis; - yaArray[i] = _subplot.yaxis; - } - - var hovermode = evt.hovermode || fullLayout.hovermode; - - if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata || - gd.querySelector('.zoombox') || gd._dragging) { - return dragElement.unhoverRaw(gd, evt); - } - - // hoverData: the set of candidate points we've found to highlight - var hoverData = [], - - // searchData: the data to search in. Mostly this is just a copy of - // gd.calcdata, filtered to the subplot and overlays we're on - // but if a point array is supplied it will be a mapping - // of indicated curves - searchData = [], - - // [x|y]valArray: the axis values of the hover event - // mapped onto each of the currently selected overlaid subplots - xvalArray, - yvalArray, - - // used in loops - itemnum, - curvenum, - cd, - trace, - subplotId, - subploti, - mode, - xval, - yval, - pointData, - closedataPreviousLength; - - // Figure out what we're hovering on: - // mouse location or user-supplied data - - if(Array.isArray(evt)) { - // user specified an array of points to highlight - hovermode = 'array'; - for(itemnum = 0; itemnum < evt.length; itemnum++) { - cd = gd.calcdata[evt[itemnum].curveNumber||0]; - if(cd[0].trace.hoverinfo !== 'skip') { - searchData.push(cd); - } - } - } - else { - for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) { - cd = gd.calcdata[curvenum]; - trace = cd[0].trace; - if(trace.hoverinfo !== 'skip' && subplots.indexOf(getSubplot(trace)) !== -1) { - searchData.push(cd); - } - } - - // [x|y]px: the pixels (from top left) of the mouse location - // on the currently selected plot area - var hasUserCalledHover = !evt.target, - xpx, ypx; - - if(hasUserCalledHover) { - if('xpx' in evt) xpx = evt.xpx; - else xpx = xaArray[0]._length / 2; - - if('ypx' in evt) ypx = evt.ypx; - else ypx = yaArray[0]._length / 2; - } - else { - // fire the beforehover event and quit if it returns false - // note that we're only calling this on real mouse events, so - // manual calls to fx.hover will always run. - if(Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { - return; - } - - var dbb = evt.target.getBoundingClientRect(); - - xpx = evt.clientX - dbb.left; - ypx = evt.clientY - dbb.top; - - // in case hover was called from mouseout into hovertext, - // it's possible you're not actually over the plot anymore - if(xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) { - return dragElement.unhoverRaw(gd, evt); - } - } - - if('xval' in evt) xvalArray = flat(subplots, evt.xval); - else xvalArray = p2c(xaArray, xpx); - - if('yval' in evt) yvalArray = flat(subplots, evt.yval); - else yvalArray = p2c(yaArray, ypx); - - if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { - Lib.warn('Fx.hover failed', evt, gd); - return dragElement.unhoverRaw(gd, evt); - } - } - - // the pixel distance to beat as a matching point - // in 'x' or 'y' mode this resets for each trace - var distance = Infinity; - - // find the closest point in each trace - // this is minimum dx and/or dy, depending on mode - // and the pixel position for the label (labelXpx, labelYpx) - for(curvenum = 0; curvenum < searchData.length; curvenum++) { - cd = searchData[curvenum]; - - // filter out invisible or broken data - if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; - - trace = cd[0].trace; - - // Explicitly bail out for these two. I don't know how to otherwise prevent - // the rest of this function from running and failing - if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; - - subplotId = getSubplot(trace); - subploti = subplots.indexOf(subplotId); - - // within one trace mode can sometimes be overridden - mode = hovermode; - - // container for new point, also used to pass info into module.hoverPoints - pointData = { - // trace properties - cd: cd, - trace: trace, - xa: xaArray[subploti], - ya: yaArray[subploti], - name: (gd.data.length > 1 || trace.hoverinfo.indexOf('name') !== -1) ? trace.name : undefined, - // point properties - override all of these - index: false, // point index in trace - only used by plotly.js hoverdata consumers - distance: Math.min(distance, constants.MAXDIST), // pixel distance or pseudo-distance - color: Color.defaultLine, // trace color - x0: undefined, - x1: undefined, - y0: undefined, - y1: undefined, - xLabelVal: undefined, - yLabelVal: undefined, - zLabelVal: undefined, - text: undefined - }; - - // add ref to subplot object (non-cartesian case) - if(fullLayout[subplotId]) { - pointData.subplot = fullLayout[subplotId]._subplot; - } - - closedataPreviousLength = hoverData.length; - - // for a highlighting array, figure out what - // we're searching for with this element - if(mode === 'array') { - var selection = evt[curvenum]; - if('pointNumber' in selection) { - pointData.index = selection.pointNumber; - mode = 'closest'; - } - else { - mode = ''; - if('xval' in selection) { - xval = selection.xval; - mode = 'x'; - } - if('yval' in selection) { - yval = selection.yval; - mode = mode ? 'closest' : 'y'; - } - } - } - else { - xval = xvalArray[subploti]; - yval = yvalArray[subploti]; - } - - // Now find the points. - if(trace._module && trace._module.hoverPoints) { - var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode); - if(newPoints) { - var newPoint; - for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) { - newPoint = newPoints[newPointNum]; - if(isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) { - hoverData.push(cleanPoint(newPoint, hovermode)); - } - } - } - } - else { - Lib.log('Unrecognized trace type in hover:', trace); - } - - // in closest mode, remove any existing (farther) points - // and don't look any farther than this latest point (or points, if boxes) - if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) { - hoverData.splice(0, closedataPreviousLength); - distance = hoverData[0].distance; - } - } - - // nothing left: remove all labels and quit - if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); - - hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); - - // lastly, emit custom hover/unhover events - var oldhoverdata = gd._hoverdata, - newhoverdata = []; - - // pull out just the data that's useful to - // other people and send it to the event - for(itemnum = 0; itemnum < hoverData.length; itemnum++) { - var pt = hoverData[itemnum]; - - var out = { - data: pt.trace._input, - fullData: pt.trace, - curveNumber: pt.trace.index, - pointNumber: pt.index - }; - - if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt); - else { - out.x = pt.xVal; - out.y = pt.yVal; - out.xaxis = pt.xa; - out.yaxis = pt.ya; - - if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal; - } - - newhoverdata.push(out); - } - - gd._hoverdata = newhoverdata; - - if(hoverChanged(gd, evt, oldhoverdata) && fullLayout._hasCartesian) { - var spikelineOpts = { - hovermode: hovermode, - fullLayout: fullLayout, - container: fullLayout._hoverlayer, - outerContainer: fullLayout._paperdiv - }; - createSpikelines(hoverData, spikelineOpts); - } - - // if there's more than one horz bar trace, - // rotate the labels so they don't overlap - var rotateLabels = hovermode === 'y' && searchData.length > 1; - - var bgColor = Color.combine( - fullLayout.plot_bgcolor || Color.background, - fullLayout.paper_bgcolor - ); - - var labelOpts = { - hovermode: hovermode, - rotateLabels: rotateLabels, - bgColor: bgColor, - container: fullLayout._hoverlayer, - outerContainer: fullLayout._paperdiv - }; - - var hoverLabels = createHoverText(hoverData, labelOpts); - - hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); - - alignHoverText(hoverLabels, rotateLabels); - - // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true - // we should improve the "fx" API so other plots can use it without these hack. - if(evt.target && evt.target.tagName) { - var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata); - overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : ''); - } - - // don't emit events if called manually - if(!evt.target || !hoverChanged(gd, evt, oldhoverdata)) return; - - if(oldhoverdata) { - gd.emit('plotly_unhover', { - event: evt, - points: oldhoverdata - }); - } - - gd.emit('plotly_hover', { - event: evt, - points: gd._hoverdata, - xaxes: xaArray, - yaxes: yaArray, - xvals: xvalArray, - yvals: yvalArray - }); -} - -// look for either .subplot (currently just ternary) -// or xaxis and yaxis attributes -function getSubplot(trace) { - return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo; -} - -fx.getDistanceFunction = function(mode, dx, dy, dxy) { - if(mode === 'closest') return dxy || quadrature(dx, dy); - return mode === 'x' ? dx : dy; -}; - -fx.getClosest = function(cd, distfn, pointData) { - // do we already have a point number? (array mode only) - if(pointData.index !== false) { - if(pointData.index >= 0 && pointData.index < cd.length) { - pointData.distance = 0; - } - else pointData.index = false; - } - else { - // apply the distance function to each data point - // this is the longest loop... if this bogs down, we may need - // to create pre-sorted data (by x or y), not sure how to - // do this for 'closest' - for(var i = 0; i < cd.length; i++) { - var newDistance = distfn(cd[i]); - if(newDistance <= pointData.distance) { - pointData.index = i; - pointData.distance = newDistance; - } - } - } - return pointData; -}; - -function cleanPoint(d, hovermode) { - var trace = d.trace || {}; - var cd = d.cd[d.index] || {}; - - d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; - - // then constrain all the positions to be on the plot - d.x0 = Lib.constrain(d.x0, 0, d.xa._length); - d.x1 = Lib.constrain(d.x1, 0, d.xa._length); - d.y0 = Lib.constrain(d.y0, 0, d.ya._length); - d.y1 = Lib.constrain(d.y1, 0, d.ya._length); - - // and convert the x and y label values into objects - // formatted as text, with font info - var logOffScale; - if(d.xLabelVal !== undefined) { - logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0); - var xLabelObj = Axes.tickText(d.xa, - d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover'); - if(logOffScale) { - if(d.xLabelVal === 0) d.xLabel = '0'; - else d.xLabel = '-' + xLabelObj.text; - } - // TODO: should we do something special if the axis calendar and - // the data calendar are different? Somehow display both dates with - // their system names? Right now it will just display in the axis calendar - // but users could add the other one as text. - else d.xLabel = xLabelObj.text; - d.xVal = d.xa.c2d(d.xLabelVal); - } - - if(d.yLabelVal !== undefined) { - logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0); - var yLabelObj = Axes.tickText(d.ya, - d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover'); - if(logOffScale) { - if(d.yLabelVal === 0) d.yLabel = '0'; - else d.yLabel = '-' + yLabelObj.text; - } - // TODO: see above TODO - else d.yLabel = yLabelObj.text; - d.yVal = d.ya.c2d(d.yLabelVal); - } - - if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal); - - // for box means and error bars, add the range to the label - if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) { - var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text; - if(d.xerrneg !== undefined) { - d.xLabel += ' +' + xeText + ' / -' + - Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text; - } - else d.xLabel += ' ± ' + xeText; - - // small distance penalty for error bars, so that if there are - // traces with errors and some without, the error bar label will - // hoist up to the point - if(hovermode === 'x') d.distance += 1; - } - if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) { - var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text; - if(d.yerrneg !== undefined) { - d.yLabel += ' +' + yeText + ' / -' + - Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text; - } - else d.yLabel += ' ± ' + yeText; - - if(hovermode === 'y') d.distance += 1; - } - - var infomode = d.trace.hoverinfo; - if(infomode !== 'all') { - infomode = infomode.split('+'); - if(infomode.indexOf('x') === -1) d.xLabel = undefined; - if(infomode.indexOf('y') === -1) d.yLabel = undefined; - if(infomode.indexOf('z') === -1) d.zLabel = undefined; - if(infomode.indexOf('text') === -1) d.text = undefined; - if(infomode.indexOf('name') === -1) d.name = undefined; - } - - function fill(key, calcKey, traceKey) { - if(cd[calcKey]) return d[key] = cd[calcKey]; - - var traceVal = Lib.nestedProperty(trace, traceKey).get(); - if(traceVal) return d[key] = traceVal; - } - - fill('color', 'hbg', 'hoverlabel.bgcolor'); - fill('borderColor', 'hbc', 'hoverlabel.bordercolor'); - fill('fontFamily', 'htf', 'hoverlabel.font.family'); - fill('fontSize', 'hts', 'hoverlabel.font.size'); - fill('fontColor', 'htc', 'hoverlabel.font.color'); - - return d; -} - -/* - * Draw a single hover item in a pre-existing svg container somewhere - * hoverItem should have keys: - * - x and y (or x0, x1, y0, and y1): - * the pixel position to mark, relative to opts.container - * - xLabel, yLabel, zLabel, text, and name: - * info to go in the label - * - color: - * the background color for the label. - * - idealAlign (optional): - * 'left' or 'right' for which side of the x/y box to try to put this on first - * - borderColor (optional): - * color for the border, defaults to strongest contrast with color - * - fontFamily (optional): - * string, the font for this label, defaults to constants.HOVERFONT - * - fontSize (optional): - * the label font size, defaults to constants.HOVERFONTSIZE - * - fontColor (optional): - * defaults to borderColor - * opts should have keys: - * - bgColor: - * the background color this is against, used if the trace is - * non-opaque, and for the name, which goes outside the box - * - container: - * a or element to add the hover label to - * - outerContainer: - * normally a parent of `container`, sets the bounding box to use to - * constrain the hover label and determine whether to show it on the left or right - */ -fx.loneHover = function(hoverItem, opts) { - var pointData = { - color: hoverItem.color || Color.defaultLine, - x0: hoverItem.x0 || hoverItem.x || 0, - x1: hoverItem.x1 || hoverItem.x || 0, - y0: hoverItem.y0 || hoverItem.y || 0, - y1: hoverItem.y1 || hoverItem.y || 0, - xLabel: hoverItem.xLabel, - yLabel: hoverItem.yLabel, - zLabel: hoverItem.zLabel, - text: hoverItem.text, - name: hoverItem.name, - idealAlign: hoverItem.idealAlign, - - // optional extra bits of styling - borderColor: hoverItem.borderColor, - fontFamily: hoverItem.fontFamily, - fontSize: hoverItem.fontSize, - fontColor: hoverItem.fontColor, - - // filler to make createHoverText happy - trace: { - index: 0, - hoverinfo: '' - }, - xa: {_offset: 0}, - ya: {_offset: 0}, - index: 0 - }; - - var container3 = d3.select(opts.container), - outerContainer3 = opts.outerContainer ? - d3.select(opts.outerContainer) : container3; - - var fullOpts = { - hovermode: 'closest', - rotateLabels: false, - bgColor: opts.bgColor || Color.background, - container: container3, - outerContainer: outerContainer3 - }; - - var hoverLabel = createHoverText([pointData], fullOpts); - alignHoverText(hoverLabel, fullOpts.rotateLabels); - - return hoverLabel.node(); -}; - -fx.loneUnhover = function(containerOrSelection) { - // duck type whether the arg is a d3 selection because ie9 doesn't - // handle instanceof like modern browsers do. - var selection = Lib.isD3Selection(containerOrSelection) ? - containerOrSelection : - d3.select(containerOrSelection); - - selection.selectAll('g.hovertext').remove(); - selection.selectAll('.spikeline').remove(); -}; - -function createSpikelines(hoverData, opts) { - var hovermode = opts.hovermode; - var container = opts.container; - var c0 = hoverData[0]; - var xa = c0.xa; - var ya = c0.ya; - var showX = xa.showspikes; - var showY = ya.showspikes; - - // Remove old spikeline items - container.selectAll('.spikeline').remove(); - - if(hovermode !== 'closest' || !(showX || showY)) return; - - var fullLayout = opts.fullLayout; - var xPoint = xa._offset + (c0.x0 + c0.x1) / 2; - var yPoint = ya._offset + (c0.y0 + c0.y1) / 2; - var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor); - var dfltDashColor = tinycolor.readability(c0.color, contrastColor) < 1.5 ? - Color.contrast(contrastColor) : c0.color; - - if(showY) { - var yMode = ya.spikemode; - var yThickness = ya.spikethickness; - var yColor = ya.spikecolor || dfltDashColor; - var yBB = ya._boundingBox; - var xEdge = ((yBB.left + yBB.right) / 2) < xPoint ? yBB.right : yBB.left; - - if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) { - var xBase = xEdge; - var xEndSpike = xPoint; - if(yMode.indexOf('across') !== -1) { - xBase = ya._counterSpan[0]; - xEndSpike = ya._counterSpan[1]; - } - - // Background horizontal Line (to y-axis) - container.append('line') - .attr({ - 'x1': xBase, - 'x2': xEndSpike, - 'y1': yPoint, - 'y2': yPoint, - 'stroke-width': yThickness + 2, - 'stroke': contrastColor - }) - .classed('spikeline', true) - .classed('crisp', true); - - // Foreground horizontal line (to y-axis) - container.append('line') - .attr({ - 'x1': xBase, - 'x2': xEndSpike, - 'y1': yPoint, - 'y2': yPoint, - 'stroke-width': yThickness, - 'stroke': yColor, - 'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness) - }) - .classed('spikeline', true) - .classed('crisp', true); - } - // Y axis marker - if(yMode.indexOf('marker') !== -1) { - container.append('circle') - .attr({ - 'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness), - 'cy': yPoint, - 'r': yThickness, - 'fill': yColor - }) - .classed('spikeline', true); - } - } - - if(showX) { - var xMode = xa.spikemode; - var xThickness = xa.spikethickness; - var xColor = xa.spikecolor || dfltDashColor; - var xBB = xa._boundingBox; - var yEdge = ((xBB.top + xBB.bottom) / 2) < yPoint ? xBB.bottom : xBB.top; - - if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) { - var yBase = yEdge; - var yEndSpike = yPoint; - if(xMode.indexOf('across') !== -1) { - yBase = xa._counterSpan[0]; - yEndSpike = xa._counterSpan[1]; - } - - // Background vertical line (to x-axis) - container.append('line') - .attr({ - 'x1': xPoint, - 'x2': xPoint, - 'y1': yBase, - 'y2': yEndSpike, - 'stroke-width': xThickness + 2, - 'stroke': contrastColor - }) - .classed('spikeline', true) - .classed('crisp', true); - - // Foreground vertical line (to x-axis) - container.append('line') - .attr({ - 'x1': xPoint, - 'x2': xPoint, - 'y1': yBase, - 'y2': yEndSpike, - 'stroke-width': xThickness, - 'stroke': xColor, - 'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness) - }) - .classed('spikeline', true) - .classed('crisp', true); - } - - // X axis marker - if(xMode.indexOf('marker') !== -1) { - container.append('circle') - .attr({ - 'cx': xPoint, - 'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness), - 'r': xThickness, - 'fill': xColor - }) - .classed('spikeline', true); - } - } -} - -function createHoverText(hoverData, opts) { - var hovermode = opts.hovermode, - rotateLabels = opts.rotateLabels, - bgColor = opts.bgColor, - container = opts.container, - outerContainer = opts.outerContainer, - - // opts.fontFamily/Size are used for the common label - // and as defaults for each hover label, though the individual labels - // can override this. - fontFamily = opts.fontFamily || constants.HOVERFONT, - fontSize = opts.fontSize || constants.HOVERFONTSIZE, - - c0 = hoverData[0], - xa = c0.xa, - ya = c0.ya, - commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel', - t0 = c0[commonAttr], - t00 = (String(t0) || '').split(' ')[0], - outerContainerBB = outerContainer.node().getBoundingClientRect(), - outerTop = outerContainerBB.top, - outerWidth = outerContainerBB.width, - outerHeight = outerContainerBB.height; - - // show the common label, if any, on the axis - // never show a common label in array mode, - // even if sometimes there could be one - var showCommonLabel = c0.distance <= constants.MAXDIST && - (hovermode === 'x' || hovermode === 'y'); - - // all hover traces hoverinfo must contain the hovermode - // to have common labels - var i, traceHoverinfo; - for(i = 0; i < hoverData.length; i++) { - traceHoverinfo = hoverData[i].trace.hoverinfo; - var parts = traceHoverinfo.split('+'); - if(parts.indexOf('all') === -1 && - parts.indexOf(hovermode) === -1) { - showCommonLabel = false; - break; - } - } - - var commonLabel = container.selectAll('g.axistext') - .data(showCommonLabel ? [0] : []); - commonLabel.enter().append('g') - .classed('axistext', true); - commonLabel.exit().remove(); - - commonLabel.each(function() { - var label = d3.select(this), - lpath = label.selectAll('path').data([0]), - ltext = label.selectAll('text').data([0]); - - lpath.enter().append('path') - .style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background}); - ltext.enter().append('text') - .call(Drawing.font, fontFamily, fontSize, Color.background) - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1); - - ltext.text(t0) - .call(svgTextUtils.convertToTspans) - .call(Drawing.setPosition, 0, 0) - .selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - label.attr('transform', ''); - - var tbb = ltext.node().getBoundingClientRect(); - if(hovermode === 'x') { - ltext.attr('text-anchor', 'middle') - .call(Drawing.setPosition, 0, (xa.side === 'top' ? - (outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD) : - (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); - - var topsign = xa.side === 'top' ? '-' : ''; - lpath.attr('d', 'M0,0' + - 'L' + HOVERARROWSIZE + ',' + topsign + HOVERARROWSIZE + - 'H' + (HOVERTEXTPAD + tbb.width / 2) + - 'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) + - 'H-' + (HOVERTEXTPAD + tbb.width / 2) + - 'V' + topsign + HOVERARROWSIZE + 'H-' + HOVERARROWSIZE + 'Z'); - - label.attr('transform', 'translate(' + - (xa._offset + (c0.x0 + c0.x1) / 2) + ',' + - (ya._offset + (xa.side === 'top' ? 0 : ya._length)) + ')'); - } - else { - ltext.attr('text-anchor', ya.side === 'right' ? 'start' : 'end') - .call(Drawing.setPosition, - (ya.side === 'right' ? 1 : -1) * (HOVERTEXTPAD + HOVERARROWSIZE), - outerTop - tbb.top - tbb.height / 2) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); - - var leftsign = ya.side === 'right' ? '' : '-'; - lpath.attr('d', 'M0,0' + - 'L' + leftsign + HOVERARROWSIZE + ',' + HOVERARROWSIZE + - 'V' + (HOVERTEXTPAD + tbb.height / 2) + - 'h' + leftsign + (HOVERTEXTPAD * 2 + tbb.width) + - 'V-' + (HOVERTEXTPAD + tbb.height / 2) + - 'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z'); - - label.attr('transform', 'translate(' + - (xa._offset + (ya.side === 'right' ? xa._length : 0)) + ',' + - (ya._offset + (c0.y0 + c0.y1) / 2) + ')'); - } - // remove the "close but not quite" points - // because of error bars, only take up to a space - hoverData = hoverData.filter(function(d) { - return (d.zLabelVal !== undefined) || - (d[commonAttr] || '').split(' ')[0] === t00; - }); - }); - - // show all the individual labels - - // first create the objects - var hoverLabels = container.selectAll('g.hovertext') - .data(hoverData, function(d) { - return [d.trace.index, d.index, d.x0, d.y0, d.name, d.attr, d.xa, d.ya || ''].join(','); - }); - hoverLabels.enter().append('g') - .classed('hovertext', true) - .each(function() { - var g = d3.select(this); - // trace name label (rect and text.name) - g.append('rect') - .call(Color.fill, Color.addOpacity(bgColor, 0.8)); - g.append('text').classed('name', true); - // trace data label (path and text.nums) - g.append('path') - .style('stroke-width', '1px'); - g.append('text').classed('nums', true) - .call(Drawing.font, fontFamily, fontSize); - }); - hoverLabels.exit().remove(); - - // then put the text in, position the pointer to the data, - // and figure out sizes - hoverLabels.each(function(d) { - var g = d3.select(this).attr('transform', ''), - name = '', - text = '', - // combine possible non-opaque trace color with bgColor - baseColor = Color.opacity(d.color) ? - d.color : Color.defaultLine, - traceColor = Color.combine(baseColor, bgColor), - - // find a contrasting color for border and text - contrastColor = d.borderColor || Color.contrast(traceColor); - - // to get custom 'name' labels pass cleanPoint - if(d.nameOverride !== undefined) d.name = d.nameOverride; - - if(d.name && d.zLabelVal === undefined) { - // strip out our pseudo-html elements from d.name (if it exists at all) - name = svgTextUtils.plainText(d.name || ''); - - if(name.length > 15) name = name.substr(0, 12) + '...'; - } - - // used by other modules (initially just ternary) that - // manage their own hoverinfo independent of cleanPoint - // the rest of this will still apply, so such modules - // can still put things in (x|y|z)Label, text, and name - // and hoverinfo will still determine their visibility - if(d.extraText !== undefined) text += d.extraText; - - if(d.zLabel !== undefined) { - if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; - if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; - text += (text ? 'z: ' : '') + d.zLabel; - } - else if(showCommonLabel && d[hovermode + 'Label'] === t0) { - text = d[(hovermode === 'x' ? 'y' : 'x') + 'Label'] || ''; - } - else if(d.xLabel === undefined) { - if(d.yLabel !== undefined) text = d.yLabel; - } - else if(d.yLabel === undefined) text = d.xLabel; - else text = '(' + d.xLabel + ', ' + d.yLabel + ')'; - - if(d.text && !Array.isArray(d.text)) text += (text ? '
' : '') + d.text; - - // if 'text' is empty at this point, - // put 'name' in main label and don't show secondary label - if(text === '') { - // if 'name' is also empty, remove entire label - if(name === '') g.remove(); - text = name; - } - - // main label - var tx = g.select('text.nums') - .call(Drawing.font, - d.fontFamily || fontFamily, - d.fontSize || fontSize, - d.fontColor || contrastColor) - .call(Drawing.setPosition, 0, 0) - .text(text) - .attr('data-notex', 1) - .call(svgTextUtils.convertToTspans); - tx.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - - var tx2 = g.select('text.name'), - tx2width = 0; - - // secondary label for non-empty 'name' - if(name && name !== text) { - tx2.call(Drawing.font, - d.fontFamily || fontFamily, - d.fontSize || fontSize, - traceColor) - .text(name) - .call(Drawing.setPosition, 0, 0) - .attr('data-notex', 1) - .call(svgTextUtils.convertToTspans); - tx2.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; - } - else { - tx2.remove(); - g.select('rect').remove(); - } - - g.select('path') - .style({ - fill: traceColor, - stroke: contrastColor - }); - var tbb = tx.node().getBoundingClientRect(), - htx = d.xa._offset + (d.x0 + d.x1) / 2, - hty = d.ya._offset + (d.y0 + d.y1) / 2, - dx = Math.abs(d.x1 - d.x0), - dy = Math.abs(d.y1 - d.y0), - txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width, - anchorStartOK, - anchorEndOK; - - d.ty0 = outerTop - tbb.top; - d.bx = tbb.width + 2 * HOVERTEXTPAD; - d.by = tbb.height + 2 * HOVERTEXTPAD; - d.anchor = 'start'; - d.txwidth = tbb.width; - d.tx2width = tx2width; - d.offset = 0; - - if(rotateLabels) { - d.pos = htx; - anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; - anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; - if((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { - hty -= dy / 2; - d.anchor = 'end'; - } else if(anchorStartOK) { - hty += dy / 2; - d.anchor = 'start'; - } else d.anchor = 'middle'; - } - else { - d.pos = hty; - anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; - anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; - if((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { - htx -= dx / 2; - d.anchor = 'end'; - } else if(anchorStartOK) { - htx += dx / 2; - d.anchor = 'start'; - } else d.anchor = 'middle'; - } - - tx.attr('text-anchor', d.anchor); - if(tx2width) tx2.attr('text-anchor', d.anchor); - g.attr('transform', 'translate(' + htx + ',' + hty + ')' + - (rotateLabels ? 'rotate(' + YANGLE + ')' : '')); - }); - - return hoverLabels; -} - -// Make groups of touching points, and within each group -// move each point so that no labels overlap, but the average -// label position is the same as it was before moving. Indicentally, -// this is equivalent to saying all the labels are on equal linear -// springs about their initial position. Initially, each point is -// its own group, but as we find overlaps we will clump the points. -// -// Also, there are hard constraints at the edges of the graphs, -// that push all groups to the middle so they are visible. I don't -// know what happens if the group spans all the way from one edge to -// the other, though it hardly matters - there's just too much -// information then. -function hoverAvoidOverlaps(hoverData, ax) { - var nummoves = 0, - - // make groups of touching points - pointgroups = hoverData - .map(function(d, i) { - var axis = d[ax]; - return [{ - i: i, - dp: 0, - pos: d.pos, - posref: d.posref, - size: d.by * (axis._id.charAt(0) === 'x' ? YFACTOR : 1) / 2, - pmin: axis._offset, - pmax: axis._offset + axis._length - }]; - }) - .sort(function(a, b) { return a[0].posref - b[0].posref; }), - donepositioning, - topOverlap, - bottomOverlap, - i, j, - pti, - sumdp; - - function constrainGroup(grp) { - var minPt = grp[0], - maxPt = grp[grp.length - 1]; - - // overlap with the top - positive vals are overlaps - topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size; - - // overlap with the bottom - positive vals are overlaps - bottomOverlap = maxPt.pos + maxPt.dp + maxPt.size - minPt.pmax; - - // check for min overlap first, so that we always - // see the largest labels - // allow for .01px overlap, so we don't get an - // infinite loop from rounding errors - if(topOverlap > 0.01) { - for(j = grp.length - 1; j >= 0; j--) grp[j].dp += topOverlap; - donepositioning = false; - } - if(bottomOverlap < 0.01) return; - if(topOverlap < -0.01) { - // make sure we're not pushing back and forth - for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; - donepositioning = false; - } - if(!donepositioning) return; - - // no room to fix positioning, delete off-screen points - - // first see how many points we need to delete - var deleteCount = 0; - for(i = 0; i < grp.length; i++) { - pti = grp[i]; - if(pti.pos + pti.dp + pti.size > minPt.pmax) deleteCount++; - } - - // start by deleting points whose data is off screen - for(i = grp.length - 1; i >= 0; i--) { - if(deleteCount <= 0) break; - pti = grp[i]; - - // pos has already been constrained to [pmin,pmax] - // so look for points close to that to delete - if(pti.pos > minPt.pmax - 1) { - pti.del = true; - deleteCount--; - } - } - for(i = 0; i < grp.length; i++) { - if(deleteCount <= 0) break; - pti = grp[i]; - - // pos has already been constrained to [pmin,pmax] - // so look for points close to that to delete - if(pti.pos < minPt.pmin + 1) { - pti.del = true; - deleteCount--; - - // shift the whole group minus into this new space - bottomOverlap = pti.size * 2; - for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; - } - } - // then delete points that go off the bottom - for(i = grp.length - 1; i >= 0; i--) { - if(deleteCount <= 0) break; - pti = grp[i]; - if(pti.pos + pti.dp + pti.size > minPt.pmax) { - pti.del = true; - deleteCount--; - } - } - } - - // loop through groups, combining them if they overlap, - // until nothing moves - while(!donepositioning && nummoves <= hoverData.length) { - // to avoid infinite loops, don't move more times - // than there are traces - nummoves++; - - // assume nothing will move in this iteration, - // reverse this if it does - donepositioning = true; - i = 0; - while(i < pointgroups.length - 1) { - // the higher (g0) and lower (g1) point group - var g0 = pointgroups[i], - g1 = pointgroups[i + 1], - - // the lowest point in the higher group (p0) - // the highest point in the lower group (p1) - p0 = g0[g0.length - 1], - p1 = g1[0]; - topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size; - - // Only group points that lie on the same axes - if(topOverlap > 0.01 && (p0.pmin === p1.pmin) && (p0.pmax === p1.pmax)) { - // push the new point(s) added to this group out of the way - for(j = g1.length - 1; j >= 0; j--) g1[j].dp += topOverlap; - - // add them to the group - g0.push.apply(g0, g1); - pointgroups.splice(i + 1, 1); - - // adjust for minimum average movement - sumdp = 0; - for(j = g0.length - 1; j >= 0; j--) sumdp += g0[j].dp; - bottomOverlap = sumdp / g0.length; - for(j = g0.length - 1; j >= 0; j--) g0[j].dp -= bottomOverlap; - donepositioning = false; - } - else i++; - } - - // check if we're going off the plot on either side and fix - pointgroups.forEach(constrainGroup); - } - - // now put these offsets into hoverData - for(i = pointgroups.length - 1; i >= 0; i--) { - var grp = pointgroups[i]; - for(j = grp.length - 1; j >= 0; j--) { - var pt = grp[j], - hoverPt = hoverData[pt.i]; - hoverPt.offset = pt.dp; - hoverPt.del = pt.del; - } - } -} - -function alignHoverText(hoverLabels, rotateLabels) { - // finally set the text positioning relative to the data and draw the - // box around it - hoverLabels.each(function(d) { - var g = d3.select(this); - if(d.del) { - g.remove(); - return; - } - var horzSign = d.anchor === 'end' ? -1 : 1, - tx = g.select('text.nums'), - alignShift = {start: 1, end: -1, middle: 0}[d.anchor], - txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD), - tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD), - offsetX = 0, - offsetY = d.offset; - if(d.anchor === 'middle') { - txx -= d.tx2width / 2; - tx2x -= d.tx2width / 2; - } - if(rotateLabels) { - offsetY *= -YSHIFTY; - offsetX = d.offset * YSHIFTX; - } - - g.select('path').attr('d', d.anchor === 'middle' ? - // middle aligned: rect centered on data - ('M-' + (d.bx / 2) + ',-' + (d.by / 2) + 'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') : - // left or right aligned: side rect with arrow to data - ('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) + - 'v' + (d.by / 2 - HOVERARROWSIZE) + - 'h' + (horzSign * d.bx) + - 'v-' + d.by + - 'H' + (horzSign * HOVERARROWSIZE + offsetX) + - 'V' + (offsetY - HOVERARROWSIZE) + - 'Z')); - - tx.call(Drawing.setPosition, - txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD) - .selectAll('tspan.line') - .attr({ - x: tx.attr('x'), - y: tx.attr('y') - }); - - if(d.tx2width) { - g.select('text.name, text.name tspan.line') - .call(Drawing.setPosition, - tx2x + alignShift * HOVERTEXTPAD + offsetX, - offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); - g.select('rect') - .call(Drawing.setRect, - tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX, - offsetY - d.by / 2 - 1, - d.tx2width, d.by + 2); - } - }); -} - -function hoverChanged(gd, evt, oldhoverdata) { - // don't emit any events if nothing changed - if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true; - - for(var i = oldhoverdata.length - 1; i >= 0; i--) { - var oldPt = oldhoverdata[i], - newPt = gd._hoverdata[i]; - if(oldPt.curveNumber !== newPt.curveNumber || - String(oldPt.pointNumber) !== String(newPt.pointNumber)) { - return true; - } - } - return false; -} - -// on click -fx.click = function(gd, evt) { - var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata); - - function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, event: evt}); } - - if(gd._hoverdata && evt && evt.target) { - if(annotationsDone && annotationsDone.then) { - annotationsDone.then(emitClick); - } - else emitClick(); - - // why do we get a double event without this??? - if(evt.stopImmediatePropagation) evt.stopImmediatePropagation(); - } -}; - - -// for bar charts and others with finite-size objects: you must be inside -// it to see its hover info, so distance is infinite outside. -// But make distance inside be at least 1/4 MAXDIST, and a little bigger -// for bigger bars, to prioritize scatter and smaller bars over big bars - -// note that for closest mode, two inbox's will get added in quadrature -// args are (signed) difference from the two opposite edges -// count one edge as in, so that over continuous ranges you never get a gap -fx.inbox = function(v0, v1) { - if(v0 * v1 < 0 || v0 === 0) { - return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); - } - return Infinity; -}; From 1d4930c2dd75a7844024b1bd789c8a2dbcf3c08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 13:56:21 -0400 Subject: [PATCH 05/38] update Fx require statements --- src/components/annotations/draw.js | 2 +- src/plots/geo/geo.js | 2 +- src/plots/gl2d/scene2d.js | 2 +- src/plots/gl3d/scene.js | 2 +- src/plots/mapbox/mapbox.js | 2 +- src/plots/ternary/ternary.js | 6 +++--- src/traces/bar/hover.js | 2 +- src/traces/box/hover.js | 2 +- src/traces/heatmap/hover.js | 2 +- src/traces/pie/plot.js | 2 +- src/traces/scatter/hover.js | 2 +- src/traces/scattergeo/hover.js | 2 +- src/traces/scattermapbox/hover.js | 2 +- test/jasmine/tests/fx_test.js | 2 +- test/jasmine/tests/hover_label_test.js | 2 +- test/jasmine/tests/hover_spikeline_test.js | 2 +- 16 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 371af5ef0da..6123d9fc0b5 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -15,9 +15,9 @@ var Plotly = require('../../plotly'); var Plots = require('../../plots/plots'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); var Color = require('../color'); var Drawing = require('../drawing'); +var Fx = require('../fx'); var svgTextUtils = require('../../lib/svg_text_utils'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../dragelement'); diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 16d760872ae..2909a3cc6da 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -15,9 +15,9 @@ var d3 = require('d3'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); +var Fx = require('../../components/fx'); var Plots = require('../plots'); var Axes = require('../cartesian/axes'); -var Fx = require('../cartesian/graph_interact'); var addProjectionsToD3 = require('./projections'); var createGeoScale = require('./set_scale'); diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 01b1fdb2b56..abd054cf892 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -11,7 +11,7 @@ var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var createPlot2D = require('gl-plot2d'); var createSpikes = require('gl-spikes2d'); diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index bf35acc3902..e4c44895671 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -15,7 +15,7 @@ var getContext = require('webgl-context'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var str2RGBAarray = require('../../lib/str2rgbarray'); var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 6062376404b..30e28eace91 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -11,7 +11,7 @@ var mapboxgl = require('mapbox-gl'); -var Fx = require('../cartesian/graph_interact'); +var Fx = require('../../components/fx'); var Lib = require('../../lib'); var constants = require('./constants'); var layoutAttributes = require('./layout_attributes'); diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 3e4cf0a3215..4c1e6a8c6a3 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -21,10 +21,10 @@ var extendFlat = require('../../lib/extend').extendFlat; var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); +var Fx = require('../../components/fx'); var Titles = require('../../components/titles'); var prepSelect = require('../cartesian/select'); var constants = require('../cartesian/constants'); -var fx = require('../cartesian/graph_interact'); function Ternary(options, fullLayout) { @@ -619,7 +619,7 @@ proto.initInteractions = function() { // these event handlers must already be set before dragElement.init // so it can stash them and override them. dragger.onmousemove = function(evt) { - fx.hover(gd, evt, _this.id); + Fx.hover(gd, evt, _this.id); gd._fullLayout._lasthover = dragger; gd._fullLayout._hoversubplot = _this.id; }; @@ -631,7 +631,7 @@ proto.initInteractions = function() { }; dragger.onclick = function(evt) { - fx.click(gd, evt); + Fx.click(gd, evt); }; dragElement.init(dragOptions); diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index 377cf4fa0a0..b718dc4b139 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -9,7 +9,7 @@ 'use strict'; -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var ErrorBars = require('../../components/errorbars'); var Color = require('../../components/color'); diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 76e65c5104f..c565e9d0d47 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -9,8 +9,8 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); var Lib = require('../../lib'); +var Fx = require('../../components/fx'); var Color = require('../../components/color'); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index d9ea27f4340..c54e6c20356 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -9,7 +9,7 @@ 'use strict'; -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var Lib = require('../../lib'); var MAXDIST = require('../../plots/cartesian/constants').MAXDIST; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 124e96368e3..6ef897e1748 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -10,7 +10,7 @@ var d3 = require('d3'); -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var svgTextUtils = require('../../lib/svg_text_utils'); diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index caba649e550..902f50cd3a6 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -10,7 +10,7 @@ 'use strict'; var Lib = require('../../lib'); -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var constants = require('../../plots/cartesian/constants'); var ErrorBars = require('../../components/errorbars'); var getTraceColor = require('./get_trace_color'); diff --git a/src/traces/scattergeo/hover.js b/src/traces/scattergeo/hover.js index 44928b875e8..6607a3dbcee 100644 --- a/src/traces/scattergeo/hover.js +++ b/src/traces/scattergeo/hover.js @@ -9,7 +9,7 @@ 'use strict'; -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var Axes = require('../../plots/cartesian/axes'); var BADNUM = require('../../constants/numerical').BADNUM; diff --git a/src/traces/scattermapbox/hover.js b/src/traces/scattermapbox/hover.js index 011425a3ebc..3b3f5434634 100644 --- a/src/traces/scattermapbox/hover.js +++ b/src/traces/scattermapbox/hover.js @@ -9,7 +9,7 @@ 'use strict'; -var Fx = require('../../plots/cartesian/graph_interact'); +var Fx = require('../../components/fx'); var getTraceColor = require('../scatter/get_trace_color'); var BADNUM = require('../../constants/numerical').BADNUM; diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 4a89b547472..123f84ec38f 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -1,7 +1,7 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); -var Fx = require('@src/plots/cartesian/graph_interact'); +var Fx = require('@src/components/fx'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index eeb0a665743..fdb921c46c4 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1,7 +1,7 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); -var Fx = require('@src/plots/cartesian/graph_interact'); +var Fx = require('@src/components/fx'); var constants = require('@src/plots/cartesian/constants'); var Lib = require('@src/lib'); diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js index f8651bbc7ed..0b1a1cc1ea8 100644 --- a/test/jasmine/tests/hover_spikeline_test.js +++ b/test/jasmine/tests/hover_spikeline_test.js @@ -1,7 +1,7 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); -var Fx = require('@src/plots/cartesian/graph_interact'); +var Fx = require('@src/components/fx'); var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); From e6099558fbaaceea7b86de8bd41a40ecdae55b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 13:56:48 -0400 Subject: [PATCH 06/38] update hover constant require statements --- src/components/annotations/annotation_defaults.js | 6 +++--- src/plots/attributes.js | 2 +- src/traces/heatmap/hover.js | 3 +-- src/traces/scatter/hover.js | 4 ++-- test/jasmine/tests/annotations_test.js | 2 +- test/jasmine/tests/geo_test.js | 3 +-- test/jasmine/tests/hover_label_test.js | 14 +++++++------- test/jasmine/tests/scattermapbox_test.js | 2 +- 8 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index f81436159da..cf11a1a7f90 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -11,8 +11,8 @@ var Lib = require('../../lib'); var Color = require('../color'); +var Fx = require('../fx'); var Axes = require('../../plots/cartesian/axes'); -var constants = require('../../plots/cartesian/constants'); var attributes = require('./attributes'); @@ -118,8 +118,8 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op Color.opacity(bgColor) ? Color.rgb(bgColor) : Color.defaultLine); var hoverBorder = coerce('hoverlabel.bordercolor', Color.contrast(hoverBG)); Lib.coerceFont(coerce, 'hoverlabel.font', { - family: constants.HOVERFONT, - size: constants.HOVERFONTSIZE, + family: Fx.constants.HOVERFONT, + size: Fx.constants.HOVERFONTSIZE, color: hoverBorder }); } diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 57eda293a07..d8ec6780af5 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -8,7 +8,7 @@ 'use strict'; -var constants = require('./cartesian/constants'); +var constants = require('../components/fx/constants'); var fontAttrs = require('./font_attributes'); var extendFlat = require('../lib/extend').extendFlat; diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index c54e6c20356..e3a870f3cb7 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -12,8 +12,7 @@ var Fx = require('../../components/fx'); var Lib = require('../../lib'); -var MAXDIST = require('../../plots/cartesian/constants').MAXDIST; - +var MAXDIST = Fx.constants.MAXDIST; module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) { // never let a heatmap override another type as closest point diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index 902f50cd3a6..7ceac0db070 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -11,11 +11,11 @@ var Lib = require('../../lib'); var Fx = require('../../components/fx'); -var constants = require('../../plots/cartesian/constants'); var ErrorBars = require('../../components/errorbars'); var getTraceColor = require('./get_trace_color'); var Color = require('../../components/color'); +var MAXDIST = Fx.constants.MAXDIST; module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var cd = pointData.cd, @@ -148,7 +148,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { Lib.extendFlat(pointData, { // never let a 2D override 1D type as closest point - distance: constants.MAXDIST + 10, + distance: MAXDIST + 10, x0: xmin, x1: xmax, y0: yAvg, diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index b6811cade12..bf0aeb220a5 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -5,7 +5,7 @@ var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var Loggers = require('@src/lib/loggers'); var Axes = require('@src/plots/cartesian/axes'); -var HOVERMINTIME = require('@src/plots/cartesian/constants').HOVERMINTIME; +var HOVERMINTIME = require('@src/components/fx').constants.HOVERMINTIME; var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; var d3 = require('d3'); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index b783fda1c45..8841292a16c 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -17,8 +17,7 @@ var mouseEvent = require('../assets/mouse_event'); var click = require('../assets/click'); var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; -var HOVERMINTIME = require('@src/plots/cartesian/constants').HOVERMINTIME; - +var HOVERMINTIME = require('@src/components/fx').constants.HOVERMINTIME; function move(fromX, fromY, toX, toY, delay) { return new Promise(function(resolve) { diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index fdb921c46c4..ddb987e32bd 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -2,8 +2,8 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); var Fx = require('@src/components/fx'); -var constants = require('@src/plots/cartesian/constants'); var Lib = require('@src/lib'); +var HOVERMINTIME = require('@src/components/fx').constants.HOVERMINTIME; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -554,7 +554,7 @@ describe('hover info', function() { Promise.resolve().then(function() { Fx.hover(gd, event, 'xy'); }) - .then(delay(constants.HOVERMINTIME * 1.1)) + .then(delay(HOVERMINTIME * 1.1)) .then(function() { Fx.unhover(gd); }) @@ -719,7 +719,7 @@ describe('hover after resizing', function() { setTimeout(function() { resolve(); - }, constants.HOVERMINTIME); + }, HOVERMINTIME); }); } @@ -732,7 +732,7 @@ describe('hover after resizing', function() { expect(hoverText.size()).toEqual(cnt, msg); resolve(); - }, constants.HOVERMINTIME); + }, HOVERMINTIME); }); } @@ -799,7 +799,7 @@ describe('hover on fill', function() { expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1.2, labelText + ':y'); resolve(); - }, constants.HOVERMINTIME); + }, HOVERMINTIME); }); } @@ -879,7 +879,7 @@ describe('hover updates', function() { } resolve(); - }, constants.HOVERMINTIME); + }, HOVERMINTIME); }); } @@ -935,7 +935,7 @@ describe('hover updates', function() { mouseEvent('mousemove', 394, 285); setTimeout(function() { resolve(); - }, constants.HOVERMINTIME); + }, HOVERMINTIME); }); } diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 7670ca1d8f2..180bf93618d 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -11,7 +11,7 @@ var customMatchers = require('../assets/custom_matchers'); var mouseEvent = require('../assets/mouse_event'); var click = require('../assets/click'); -var HOVERMINTIME = require('@src/plots/cartesian/constants').HOVERMINTIME; +var HOVERMINTIME = require('@src/components/fx').constants.HOVERMINTIME; function move(fromX, fromY, toX, toY, delay) { return new Promise(function(resolve) { From 12954aafd6d302c3cc9e5a650e66cd1137fdf6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 13:57:08 -0400 Subject: [PATCH 07/38] sub Fx.init by (cartesian) initIterations --- src/plot_api/plot_api.js | 6 +++--- src/plot_api/subroutines.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f25b231cd7f..48bea3d137a 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -20,8 +20,8 @@ var Queue = require('../lib/queue'); var Registry = require('../registry'); var Plots = require('../plots/plots'); -var Fx = require('../plots/cartesian/graph_interact'); var Polar = require('../plots/polar'); +var initInteractions = require('../plots/cartesian/graph_interact'); var Drawing = require('../components/drawing'); var ErrorBars = require('../components/errorbars'); @@ -188,7 +188,7 @@ Plotly.plot = function(gd, data, layout, config) { return Lib.syncOrAsync([ subroutines.layoutStyles, drawAxes, - Fx.init + initInteractions ], gd); } @@ -223,7 +223,7 @@ Plotly.plot = function(gd, data, layout, config) { // re-initialize cartesian interaction, // which are sometimes cleared during marginPushers - seq = seq.concat(Fx.init); + seq = seq.concat(initInteractions); return Lib.syncOrAsync(seq, gd); } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index f0a81f76af8..45238db4497 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -19,7 +19,7 @@ var Color = require('../components/color'); var Drawing = require('../components/drawing'); var Titles = require('../components/titles'); var ModeBar = require('../components/modebar'); - +var initInteractions = require('../plots/cartesian/graph_interact'); exports.layoutStyles = function(gd) { return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); @@ -378,7 +378,7 @@ exports.doModeBar = function(gd) { var subplotIds, i; ModeBar.manage(gd); - Plotly.Fx.init(gd); + initInteractions(gd); subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { From 27fb2d01743b2ca37070b79e502abc4cfeba2784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 13:57:29 -0400 Subject: [PATCH 08/38] decrease max allowed circular deps to 13 :tada: --- tasks/test_syntax.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 150cac24f4c..a8a5fefa77d 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -176,10 +176,8 @@ function assertCircularDeps() { var circularDeps = res.circular(); var logs = []; - // as of v1.17.0 - 2016/09/08 // see https://github.com/plotly/plotly.js/milestone/9 - // for more details - var MAX_ALLOWED_CIRCULAR_DEPS = 17; + var MAX_ALLOWED_CIRCULAR_DEPS = 13; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { console.log(circularDeps.join('\n')); From a96af57c50a09fdcd9db3682d5e3b1aceedbd4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 14:52:31 -0400 Subject: [PATCH 09/38] mv fx attribute out of plots/ and into components/fx/ --- src/components/fx/index.js | 5 +++++ src/components/fx/layout_attributes.js | 30 ++++++++++++++++++++++++++ src/components/fx/layout_defaults.js | 2 +- src/plots/layout_attributes.js | 18 ---------------- 4 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 src/components/fx/layout_attributes.js diff --git a/src/components/fx/index.js b/src/components/fx/index.js index 0a999a656e2..dd7a79e9ccc 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -12,13 +12,18 @@ var d3 = require('d3'); var Lib = require('../../lib'); var dragElement = require('../dragelement'); var helpers = require('./helpers'); +var layoutAttributes = require('./layout_attributes'); module.exports = { moduleType: 'component', name: 'fx', constants: require('./constants'), + schema: { + layout: layoutAttributes + }, + layoutAttributes: layoutAttributes, supplyLayoutDefaults: require('./layout_defaults'), getDistanceFunction: helpers.getDistanceFunction, diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js new file mode 100644 index 00000000000..37da49ebfc0 --- /dev/null +++ b/src/components/fx/layout_attributes.js @@ -0,0 +1,30 @@ +/** +* 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'; + +module.exports = { + dragmode: { + valType: 'enumerated', + role: 'info', + values: ['zoom', 'pan', 'select', 'lasso', 'orbit', 'turntable'], + dflt: 'zoom', + description: [ + 'Determines the mode of drag interactions.', + '*select* and *lasso* apply only to scatter traces with', + 'markers or text. *orbit* and *turntable* apply only to', + '3D scenes.' + ].join(' ') + }, + hovermode: { + valType: 'enumerated', + role: 'info', + values: ['x', 'y', 'closest', false], + description: 'Determines the mode of hover interactions.' + } +}; diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js index 3304f695516..13d5d631919 100644 --- a/src/components/fx/layout_defaults.js +++ b/src/components/fx/layout_defaults.js @@ -9,7 +9,7 @@ 'use strict'; var Lib = require('../../lib'); -var layoutAttributes = require('../../plots/layout_attributes'); +var layoutAttributes = require('./layout_attributes'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { function coerce(attr, dflt) { diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index c2c033d8b0d..4043ac267f9 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -169,23 +169,5 @@ module.exports = { valType: 'boolean', role: 'info', description: 'Determines whether or not a legend is drawn.' - }, - dragmode: { - valType: 'enumerated', - role: 'info', - values: ['zoom', 'pan', 'select', 'lasso', 'orbit', 'turntable'], - dflt: 'zoom', - description: [ - 'Determines the mode of drag interactions.', - '*select* and *lasso* apply only to scatter traces with', - 'markers or text. *orbit* and *turntable* apply only to', - '3D scenes.' - ].join(' ') - }, - hovermode: { - valType: 'enumerated', - role: 'info', - values: ['x', 'y', 'closest', false], - description: 'Determines the mode of hover interactions.' } }; From abcf1b034c637aa2a56203bdd7724fa870ec9cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 14:53:13 -0400 Subject: [PATCH 10/38] register fx in core.js - this removes one more circular dep! --- src/core.js | 3 ++- src/plotly.js | 1 - src/plots/plots.js | 3 --- tasks/test_syntax.js | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/core.js b/src/core.js index 3040b7cd4db..4c848e1a37b 100644 --- a/src/core.js +++ b/src/core.js @@ -53,6 +53,7 @@ exports.register(require('./traces/scatter')); // register all registrable components modules exports.register([ + require('./components/fx'), require('./components/legend'), require('./components/annotations'), require('./components/shapes'), @@ -68,7 +69,7 @@ exports.Icons = require('../build/ploticon'); // unofficial 'beta' plot methods, use at your own risk exports.Plots = Plotly.Plots; -exports.Fx = Plotly.Fx; +exports.Fx = require('./components/fx'); exports.Snapshot = require('./snapshot'); exports.PlotSchema = require('./plot_api/plot_schema'); exports.Queue = require('./lib/queue'); diff --git a/src/plotly.js b/src/plotly.js index c8947f2dca4..1eae9f219a6 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -27,7 +27,6 @@ exports.Axes = require('./plots/cartesian/axes'); // components exports.ModeBar = require('./components/modebar'); -exports.Fx = require('./components/fx'); // plot api require('./plot_api/plot_api'); diff --git a/src/plots/plots.js b/src/plots/plots.js index b70569ff39c..246cb357f9c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1111,9 +1111,6 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } } - // should FX be a component? - Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - var components = Object.keys(Registry.componentsRegistry); for(i = 0; i < components.length; i++) { _module = Registry.componentsRegistry[components[i]]; diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index a8a5fefa77d..b23620bd301 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -177,7 +177,7 @@ function assertCircularDeps() { var logs = []; // see https://github.com/plotly/plotly.js/milestone/9 - var MAX_ALLOWED_CIRCULAR_DEPS = 13; + var MAX_ALLOWED_CIRCULAR_DEPS = 12; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { console.log(circularDeps.join('\n')); From efcb027e8c7098db5acb288442d922c4201b0b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 20 Apr 2017 17:07:49 -0400 Subject: [PATCH 11/38] revert hoverlabel in arraysToCalcdata -> add Fx.calc --- src/components/fx/calc.js | 28 ++++++++++++++++++++++++ src/components/fx/index.js | 2 ++ src/plots/plots.js | 2 ++ src/traces/bar/arrays_to_calcdata.js | 8 ------- src/traces/scatter/arrays_to_calcdata.js | 10 --------- 5 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 src/components/fx/calc.js diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js new file mode 100644 index 00000000000..eda8248ff66 --- /dev/null +++ b/src/components/fx/calc.js @@ -0,0 +1,28 @@ +/** +* 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 Lib = require('../../lib'); + +module.exports = function calc(gd) { + var calcdata = gd.calcdata; + + for(var i = 0; i < calcdata.length; i++) { + var cd = calcdata[i]; + var trace = cd[0].trace; + + if(trace.hoverlabel) { + Lib.mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); + Lib.mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); + Lib.mergeArray(trace.hoverlabel.font.size, cd, 'hts'); + Lib.mergeArray(trace.hoverlabel.font.color, cd, 'htc'); + Lib.mergeArray(trace.hoverlabel.font.family, cd, 'htf'); + } + } +}; diff --git a/src/components/fx/index.js b/src/components/fx/index.js index dd7a79e9ccc..87192de38af 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -26,6 +26,8 @@ module.exports = { layoutAttributes: layoutAttributes, supplyLayoutDefaults: require('./layout_defaults'), + calc: require('./calc'), + getDistanceFunction: helpers.getDistanceFunction, getClosest: helpers.getClosest, inbox: helpers.inbox, diff --git a/src/plots/plots.js b/src/plots/plots.js index 246cb357f9c..dbe17381854 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2076,6 +2076,8 @@ plots.doCalcdata = function(gd, traces) { calcdata[i] = cd; } + Registry.getComponentMethod('fx', 'calc')(gd); + // To handle the case of components using category names as coordinates, we // need to re-supply defaults for these objects now, after calc has // finished populating the category mappings diff --git a/src/traces/bar/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js index f034117dfe5..675364e9920 100644 --- a/src/traces/bar/arrays_to_calcdata.js +++ b/src/traces/bar/arrays_to_calcdata.js @@ -28,12 +28,4 @@ module.exports = function arraysToCalcdata(cd, trace) { mergeArray(markerLine.width, cd, 'mlw'); } } - - if(trace.hoverlabel) { - mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); - mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); - mergeArray(trace.hoverlabel.font.size, cd, 'hts'); - mergeArray(trace.hoverlabel.font.color, cd, 'htc'); - mergeArray(trace.hoverlabel.font.family, cd, 'htf'); - } }; diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index b2172eb3ed3..378fc7613f0 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -17,10 +17,8 @@ module.exports = function arraysToCalcdata(cd, trace) { Lib.mergeArray(trace.text, cd, 'tx'); Lib.mergeArray(trace.hovertext, cd, 'htx'); - Lib.mergeArray(trace.customdata, cd, 'data'); Lib.mergeArray(trace.textposition, cd, 'tp'); - if(trace.textfont) { Lib.mergeArray(trace.textfont.size, cd, 'ts'); Lib.mergeArray(trace.textfont.color, cd, 'tc'); @@ -40,12 +38,4 @@ module.exports = function arraysToCalcdata(cd, trace) { Lib.mergeArray(markerLine.width, cd, 'mlw'); } } - - if(trace.hoverlabel) { - Lib.mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); - Lib.mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); - Lib.mergeArray(trace.hoverlabel.font.size, cd, 'hts'); - Lib.mergeArray(trace.hoverlabel.font.color, cd, 'htc'); - Lib.mergeArray(trace.hoverlabel.font.family, cd, 'htf'); - } }; From 7907140ce76fdbd2d659991ec6eb8c37d0e10963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 21 Apr 2017 17:56:17 -0400 Subject: [PATCH 12/38] make fx default test more robust --- test/jasmine/tests/fx_test.js | 69 ++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 123f84ec38f..39e158f5174 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -1,71 +1,96 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); -var Fx = require('@src/components/fx'); - var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); - describe('Fx defaults', function() { 'use strict'; - var layoutIn, layoutOut, fullData; + function _supply(data, layout) { + var gd = { + data: data || [], + layout: layout || {} + }; + + Plots.supplyDefaults(gd); - beforeEach(function() { - layoutIn = {}; - layoutOut = { - _has: Plots._hasPlotType + return { + data: gd._fullData, + layout: gd._fullLayout }; - fullData = [{}]; - }); + } it('should default (blank version)', function() { - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + var layoutOut = _supply().layout; expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); it('should default (cartesian version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }]; + var layoutOut = _supply([{ + type: 'bar', + y: [1, 2, 1] + }]) + .layout; - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); expect(layoutOut._isHoriz).toBe(false, 'isHoriz to false'); }); it('should default (cartesian horizontal version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }]; - fullData[0] = { orientation: 'h' }; + var layoutOut = _supply([{ + type: 'bar', + orientation: 'h', + x: [1, 2, 3], + y: [1, 2, 1] + }]) + .layout; - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.hovermode).toBe('y', 'hovermode to y'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); expect(layoutOut._isHoriz).toBe(true, 'isHoriz to true'); }); it('should default (gl3d version)', function() { - layoutOut._basePlotModules = [{ name: 'gl3d' }]; + var layoutOut = _supply([{ + type: 'scatter3d', + x: [1, 2, 3], + y: [1, 2, 3], + z: [1, 2, 1] + }]) + .layout; - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); it('should default (geo version)', function() { - layoutOut._basePlotModules = [{ name: 'geo' }]; + var layoutOut = _supply([{ + type: 'scattergeo', + lon: [1, 2, 3], + lat: [1, 2, 3] + }]) + .layout; - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); it('should default (multi plot type version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }, { name: 'gl3d' }]; + var layoutOut = _supply([{ + type: 'bar', + y: [1, 2, 1] + }, { + type: 'scatter3d', + x: [1, 2, 3], + y: [1, 2, 3], + z: [1, 2, 1] + }]) + .layout; - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); From 0d66e6eec0b7122c27350ab726f1db39c4ce5c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 21 Apr 2017 17:56:39 -0400 Subject: [PATCH 13/38] lint fx constants --- src/components/fx/constants.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/fx/constants.js b/src/components/fx/constants.js index 03d726e42da..37e21d014e0 100644 --- a/src/components/fx/constants.js +++ b/src/components/fx/constants.js @@ -16,11 +16,15 @@ module.exports = { YANGLE: 60, // size and display constants for hover text - HOVERARROWSIZE: 6, // pixel size of hover arrows - HOVERTEXTPAD: 3, // pixels padding around text + + // pixel size of hover arrows + HOVERARROWSIZE: 6, + // pixels padding around text + HOVERTEXTPAD: 3, + // hover font HOVERFONTSIZE: 13, HOVERFONT: 'Arial, sans-serif', // minimum time (msec) between hover calls - HOVERMINTIME: 50, + HOVERMINTIME: 50 }; From 3ac4c07f8d8adc9e56d0263db1439790aa9dec7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 21 Apr 2017 18:00:04 -0400 Subject: [PATCH 14/38] lint getComponentMethod call --- src/plots/plots.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index dbe17381854..c56b81e7e3f 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -973,8 +973,11 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { coerce('hidesources'); coerce('smith'); - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); - handleCalendarDefaults(layoutIn, layoutOut, 'calendar'); + Registry.getComponentMethod( + 'calendars', + 'handleDefaults' + )(layoutIn, layoutOut, 'calendar'); + }; plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { From 295659e4485af8cdb61cb7d7c75cbf8824a81ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 21 Apr 2017 18:02:00 -0400 Subject: [PATCH 15/38] add fx supplyDefaults method - to coerce hoverlabel container in a DRY way --- src/components/fx/attributes.js | 38 ++++++++++++++++++++++++ src/components/fx/defaults.js | 21 +++++++++++++ src/components/fx/hoverlabel_defaults.js | 19 ++++++++++++ src/components/fx/index.js | 2 ++ src/plots/plots.js | 7 +++-- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/components/fx/attributes.js create mode 100644 src/components/fx/defaults.js create mode 100644 src/components/fx/hoverlabel_defaults.js diff --git a/src/components/fx/attributes.js b/src/components/fx/attributes.js new file mode 100644 index 00000000000..65685335015 --- /dev/null +++ b/src/components/fx/attributes.js @@ -0,0 +1,38 @@ +/** +* 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 extendFlat = require('../../lib/extend').extendFlat; +var fontAttrs = require('../../plots/font_attributes'); + +module.exports = { + hoverlabel: { + bgcolor: { + valType: 'color', + role: 'style', + arrayOk: true, + description: [ + 'Sets the background color of the hover labels for this trace' + ].join(' ') + }, + bordercolor: { + valType: 'color', + role: 'style', + arrayOk: true, + description: [ + 'Sets the border color of the hover labels for this trace.' + ].join(' ') + }, + font: { + family: extendFlat({}, fontAttrs.family, { arrayOk: true }), + size: extendFlat({}, fontAttrs.size, { arrayOk: true }), + color: extendFlat({}, fontAttrs.color, { arrayOk: true }) + } + } +}; diff --git a/src/components/fx/defaults.js b/src/components/fx/defaults.js new file mode 100644 index 00000000000..09119ba1475 --- /dev/null +++ b/src/components/fx/defaults.js @@ -0,0 +1,21 @@ +/** +* 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 Lib = require('../../lib'); +var attributes = require('./attributes'); +var handleHoverLabelDefaults = require('./hoverlabel_defaults'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleHoverLabelDefaults(traceIn, traceOut, coerce, layout.hoverlabel); +}; diff --git a/src/components/fx/hoverlabel_defaults.js b/src/components/fx/hoverlabel_defaults.js new file mode 100644 index 00000000000..85fb682dee0 --- /dev/null +++ b/src/components/fx/hoverlabel_defaults.js @@ -0,0 +1,19 @@ +/** +* 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 Lib = require('../../lib'); + +module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts) { + opts = opts || {}; + + coerce('hoverlabel.bgcolor', opts.bgcolor); + coerce('hoverlabel.bordercolor', opts.bordercolor); + Lib.coerceFont(coerce, 'hoverlabel.font', opts.font); +}; diff --git a/src/components/fx/index.js b/src/components/fx/index.js index 87192de38af..bf35a12e382 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -23,7 +23,9 @@ module.exports = { layout: layoutAttributes }, + attributes: require('./attributes'), layoutAttributes: layoutAttributes, + supplyDefaults: require('./defaults'), supplyLayoutDefaults: require('./layout_defaults'), calc: require('./calc'), diff --git a/src/plots/plots.js b/src/plots/plots.js index c56b81e7e3f..3c19023bd22 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -846,9 +846,10 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde coerce('legendgroup'); } - coerce('hoverlabel.bgcolor'); - coerce('hoverlabel.bordercolor'); - Lib.coerceFont(coerce, 'hoverlabel.font'); + Registry.getComponentMethod( + 'fx', + 'supplyDefaults' + )(traceIn, traceOut, defaultColor, layout); // TODO add per-base-plot-module trace defaults step From 9d91e2ccd2803e0068ceea7c7956022c367562b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 14:39:07 -0400 Subject: [PATCH 16/38] fix mapbox hover label color for array `marker.color` values --- src/traces/scattermapbox/convert.js | 5 ++++- test/jasmine/tests/scattermapbox_test.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index e4c17f91973..01312110dae 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -200,7 +200,10 @@ function makeCircleGeoJSON(calcTrace, hash) { if(isBADNUM(lonlat)) continue; var props = {}; - if(colorFn) translate(props, COLOR_PROP, colorFn(calcPt.mc), i); + if(colorFn) { + var mcc = calcPt.mcc = colorFn(calcPt.mc); + translate(props, COLOR_PROP, mcc, i); + } if(sizeFn) translate(props, SIZE_PROP, sizeFn(calcPt.ms), i); features.push({ diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 180bf93618d..6b65c668f95 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -490,6 +490,24 @@ describe('@noCI scattermapbox hover', function() { done(); }); }); + + it('should generate hover label (\'marker.color\' array case)', function(done) { + Plotly.restyle(gd, 'marker.color', [['red', 'blue', 'green']]).then(function() { + var out = hoverPoints(getPointData(gd), 11, 11)[0]; + + expect(out.color).toEqual('red'); + }) + .then(done); + }); + + it('should generate hover label (\'marker.color\' w/ colorscale case)', function(done) { + Plotly.restyle(gd, 'marker.color', [[10, 5, 30]]).then(function() { + var out = hoverPoints(getPointData(gd), 11, 11)[0]; + + expect(out.color).toEqual('rgb(245, 195, 157)'); + }) + .then(done); + }); }); From add85d20be32866a2c138146cc8927d7ad274a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 14:41:22 -0400 Subject: [PATCH 17/38] lint var declarations --- src/components/fx/hover.js | 56 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 803b9e33e89..f070200ad15 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -523,28 +523,28 @@ function _hover(gd, evt, subplot) { } function createHoverText(hoverData, opts) { - var hovermode = opts.hovermode, - rotateLabels = opts.rotateLabels, - bgColor = opts.bgColor, - container = opts.container, - outerContainer = opts.outerContainer, - - // opts.fontFamily/Size are used for the common label - // and as defaults for each hover label, though the individual labels - // can override this. - fontFamily = opts.fontFamily || constants.HOVERFONT, - fontSize = opts.fontSize || constants.HOVERFONTSIZE, - - c0 = hoverData[0], - xa = c0.xa, - ya = c0.ya, - commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel', - t0 = c0[commonAttr], - t00 = (String(t0) || '').split(' ')[0], - outerContainerBB = outerContainer.node().getBoundingClientRect(), - outerTop = outerContainerBB.top, - outerWidth = outerContainerBB.width, - outerHeight = outerContainerBB.height; + var hovermode = opts.hovermode; + var rotateLabels = opts.rotateLabels; + var bgColor = opts.bgColor; + var container = opts.container; + var outerContainer = opts.outerContainer; + + // opts.fontFamily/Size are used for the common label + // and as defaults for each hover label, though the individual labels + // can override this. + var fontFamily = opts.fontFamily || constants.HOVERFONT; + var fontSize = opts.fontSize || constants.HOVERFONTSIZE; + + var c0 = hoverData[0]; + var xa = c0.xa; + var ya = c0.ya; + var commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel'; + var t0 = c0[commonAttr]; + var t00 = (String(t0) || '').split(' ')[0]; + var outerContainerBB = outerContainer.node().getBoundingClientRect(); + var outerTop = outerContainerBB.top; + var outerWidth = outerContainerBB.width; + var outerHeight = outerContainerBB.height; // show the common label, if any, on the axis // never show a common label in array mode, @@ -674,14 +674,14 @@ function createHoverText(hoverData, opts) { hoverLabels.each(function(d) { var g = d3.select(this).attr('transform', ''), name = '', - text = '', + text = ''; + // combine possible non-opaque trace color with bgColor - baseColor = Color.opacity(d.color) ? - d.color : Color.defaultLine, - traceColor = Color.combine(baseColor, bgColor), + var baseColor = Color.opacity(d.color) ? d.color : Color.defaultLine; + var traceColor = Color.combine(baseColor, bgColor); - // find a contrasting color for border and text - contrastColor = d.borderColor || Color.contrast(traceColor); + // find a contrasting color for border and text + var contrastColor = d.borderColor || Color.contrast(traceColor); // to get custom 'name' labels pass cleanPoint if(d.nameOverride !== undefined) d.name = d.nameOverride; From f986fd2e34afa4fe625a995831888899d9b2442b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 14:45:41 -0400 Subject: [PATCH 18/38] add global layout defaults step for `layout.hoverlabel` - and use coerced values for trace and annotation hoverlabel defaults. --- .../annotations/annotation_defaults.js | 20 +++-- src/components/fx/index.js | 2 + src/components/fx/layout_attributes.js | 30 +++++++ src/components/fx/layout_global_defaults.js | 21 +++++ src/plots/attributes.js | 36 +------- src/plots/plots.js | 4 + test/jasmine/tests/fx_test.js | 87 +++++++++++++++++++ 7 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 src/components/fx/layout_global_defaults.js diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index cf11a1a7f90..1f008942258 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -11,7 +11,6 @@ var Lib = require('../../lib'); var Color = require('../color'); -var Fx = require('../fx'); var Axes = require('../../plots/cartesian/axes'); var attributes = require('./attributes'); @@ -113,14 +112,21 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op } var hoverText = coerce('hovertext'); + var globalHoverLabel = fullLayout.hoverlabel || {}; + if(hoverText) { - var hoverBG = coerce('hoverlabel.bgcolor', - Color.opacity(bgColor) ? Color.rgb(bgColor) : Color.defaultLine); - var hoverBorder = coerce('hoverlabel.bordercolor', Color.contrast(hoverBG)); + var hoverBG = coerce('hoverlabel.bgcolor', globalHoverLabel.bgcolor || + (Color.opacity(bgColor) ? Color.rgb(bgColor) : Color.defaultLine) + ); + + var hoverBorder = coerce('hoverlabel.bordercolor', globalHoverLabel.bordercolor || + Color.contrast(hoverBG) + ); + Lib.coerceFont(coerce, 'hoverlabel.font', { - family: Fx.constants.HOVERFONT, - size: Fx.constants.HOVERFONTSIZE, - color: hoverBorder + family: globalHoverLabel.font.family, + size: globalHoverLabel.font.size, + color: globalHoverLabel.font.color || hoverBorder }); } coerce('captureevents', !!hoverText); diff --git a/src/components/fx/index.js b/src/components/fx/index.js index bf35a12e382..8b1e6f4cb16 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -25,6 +25,8 @@ module.exports = { attributes: require('./attributes'), layoutAttributes: layoutAttributes, + + supplyLayoutGlobalDefaults: require('./layout_global_defaults'), supplyDefaults: require('./defaults'), supplyLayoutDefaults: require('./layout_defaults'), diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index 37da49ebfc0..f09e0dff0d1 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -8,6 +8,10 @@ 'use strict'; +var extendFlat = require('../../lib/extend').extendFlat; +var fontAttrs = require('../../plots/font_attributes'); +var constants = require('./constants'); + module.exports = { dragmode: { valType: 'enumerated', @@ -26,5 +30,31 @@ module.exports = { role: 'info', values: ['x', 'y', 'closest', false], description: 'Determines the mode of hover interactions.' + }, + + hoverlabel: { + bgcolor: { + valType: 'color', + role: 'style', + description: [ + 'Sets the background color of all hover labels on graph' + ].join(' ') + }, + bordercolor: { + valType: 'color', + role: 'style', + description: [ + 'Sets the border color of all hover labels on graph.' + ].join(' ') + }, + font: { + family: extendFlat({}, fontAttrs.family, { + dflt: constants.HOVERFONT + }), + size: extendFlat({}, fontAttrs.size, { + dflt: constants.HOVERFONTSIZE + }), + color: extendFlat({}, fontAttrs.color) + } } }; diff --git a/src/components/fx/layout_global_defaults.js b/src/components/fx/layout_global_defaults.js new file mode 100644 index 00000000000..4d00bc48508 --- /dev/null +++ b/src/components/fx/layout_global_defaults.js @@ -0,0 +1,21 @@ +/** +* 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 Lib = require('../../lib'); +var handleHoverLabelDefaults = require('./hoverlabel_defaults'); +var layoutAttributes = require('./layout_attributes'); + +module.exports = function supplyLayoutGlobalDefaults(layoutIn, layoutOut) { + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + + handleHoverLabelDefaults(layoutIn, layoutOut, coerce); +}; diff --git a/src/plots/attributes.js b/src/plots/attributes.js index d8ec6780af5..54066762e8a 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -8,9 +8,7 @@ 'use strict'; -var constants = require('../components/fx/constants'); -var fontAttrs = require('./font_attributes'); -var extendFlat = require('../lib/extend').extendFlat; +var fxAttrs = require('../components/fx/attributes'); module.exports = { type: { @@ -83,37 +81,7 @@ module.exports = { 'But, if `none` is set, click and hover events are still fired.' ].join(' ') }, - hoverlabel: { - bgcolor: { - valType: 'color', - role: 'style', - arrayOk: true, - description: [ - 'Sets the background color of the hover label.' - ].join(' ') - }, - bordercolor: { - valType: 'color', - role: 'style', - arrayOk: true, - description: [ - 'Sets the border color of the hover label.' - ].join(' ') - }, - font: { - family: extendFlat({}, fontAttrs.family, { - arrayOk: true, - dflt: constants.HOVERFONT - }), - size: extendFlat({}, fontAttrs.size, { - arrayOk: true, - dflt: constants.HOVERFONTSIZE - }), - color: extendFlat({}, fontAttrs.color, { - arrayOk: true - }) - } - }, + hoverlabel: fxAttrs.hoverlabel, stream: { token: { valType: 'string', diff --git a/src/plots/plots.js b/src/plots/plots.js index 3c19023bd22..4efb546fa1d 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -979,6 +979,10 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { 'handleDefaults' )(layoutIn, layoutOut, 'calendar'); + Registry.getComponentMethod( + 'fx', + 'supplyLayoutGlobalDefaults' + )(layoutIn, layoutOut, coerce); }; plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 39e158f5174..3ffea51d51c 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -94,6 +94,93 @@ describe('Fx defaults', function() { expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); + + it('should coerce trace and annotations hoverlabel using global as defaults', function() { + var out = _supply([{ + type: 'bar', + y: [1, 2, 1], + hoverlabel: { + bgcolor: ['red', 'blue', 'black'], + font: { size: 40 } + } + }, { + type: 'scatter3d', + x: [1, 2, 3], + y: [1, 2, 3], + z: [1, 2, 1], + hoverlabel: { + bordercolor: 'yellow', + font: { color: 'red' } + } + }], { + annotations: [{ + x: 0, + y: 1, + text: '1', + hovertext: '1' + }, { + x: 2, + y: 1, + text: '2', + hovertext: '2', + hoverlabel: { + bgcolor: 'red', + font: { + family: 'Gravitas' + } + } + }], + hoverlabel: { + bgcolor: 'white', + bordercolor: 'black', + font: { + family: 'Roboto', + size: 20, + color: 'pink' + } + } + }); + + expect(out.data[0].hoverlabel).toEqual({ + bgcolor: ['red', 'blue', 'black'], + bordercolor: 'black', + font: { + family: 'Roboto', + size: 40, + color: 'pink' + } + }); + + expect(out.data[1].hoverlabel).toEqual({ + bgcolor: 'white', + bordercolor: 'yellow', + font: { + family: 'Roboto', + size: 20, + color: 'red' + } + }); + + expect(out.layout.annotations[0].hoverlabel).toEqual({ + bgcolor: 'white', + bordercolor: 'black', + font: { + family: 'Roboto', + size: 20, + color: 'pink' + } + }); + + expect(out.layout.annotations[1].hoverlabel).toEqual({ + bgcolor: 'red', + bordercolor: 'black', + font: { + family: 'Gravitas', + size: 20, + color: 'pink' + } + }); + }); }); describe('relayout', function() { From a8cc8b03efd2934265c1ed1c76092776c1596953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 17:46:24 -0400 Subject: [PATCH 19/38] make common label in 'x' and 'y' hovermode adhere to layout.hoverlabel --- src/components/fx/hover.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index f070200ad15..0243c4d124e 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -486,7 +486,8 @@ function _hover(gd, evt, subplot) { rotateLabels: rotateLabels, bgColor: bgColor, container: fullLayout._hoverlayer, - outerContainer: fullLayout._paperdiv + outerContainer: fullLayout._paperdiv, + commonLabelOpts: fullLayout.hoverlabel }; var hoverLabels = createHoverText(hoverData, labelOpts); @@ -528,6 +529,7 @@ function createHoverText(hoverData, opts) { var bgColor = opts.bgColor; var container = opts.container; var outerContainer = opts.outerContainer; + var commonLabelOpts = opts.commonLabelOpts || {}; // opts.fontFamily/Size are used for the common label // and as defaults for each hover label, though the individual labels @@ -577,9 +579,17 @@ function createHoverText(hoverData, opts) { ltext = label.selectAll('text').data([0]); lpath.enter().append('path') - .style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background}); + .style({ + fill: commonLabelOpts.bgcolor || Color.defaultLine, + stroke: commonLabelOpts.bordercolor || Color.background, + 'stroke-width': '1px' + }); ltext.enter().append('text') - .call(Drawing.font, fontFamily, fontSize, Color.background) + .call(Drawing.font, + commonLabelOpts.font.family || fontFamily, + commonLabelOpts.font.size || fontSize, + commonLabelOpts.font.color || Color.background + ) // prohibit tex interpretation until we can handle // tex and regular text together .attr('data-notex', 1); From 9ee88770eca1a17f73f354f0bfd2e091f920980d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 17:47:57 -0400 Subject: [PATCH 20/38] add tests for custom hover labels cartesian, geo, mapbox & ternary --- test/jasmine/tests/geo_test.js | 15 +++++ test/jasmine/tests/hover_label_test.js | 86 ++++++++++++++++++++++++++ test/jasmine/tests/mapbox_test.js | 16 +++++ test/jasmine/tests/ternary_test.js | 18 +++++- 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 8841292a16c..bccd6ae3425 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -502,6 +502,21 @@ describe('Test geo interactions', function() { }) .then(done); }); + + it('should show custom \`hoverlabel\' settings', function(done) { + Plotly.restyle(gd, { + 'hoverlabel.bgcolor': 'red', + 'hoverlabel.bordercolor': [['blue', 'black', 'green']] + }) + .then(function() { + mouseEventScatterGeo('mousemove'); + + var path = d3.selectAll('g.hovertext').select('path'); + expect(path.style('fill')).toEqual('rgb(255, 0, 0)', 'bgcolor'); + expect(path.style('stroke')).toEqual('rgb(0, 0, 255)', 'bordecolor[0]'); + }) + .then(done); + }); }); describe('scattergeo hover events', function() { diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index ddb987e32bd..afe5829fc1c 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -984,3 +984,89 @@ describe('hover updates', function() { }); }); + +describe('Test hover label custom styling:', function() { + afterEach(destroyGraphDiv); + + function assertLabel(className, expectation) { + var g = d3.select('g.' + className); + + var path = g.select('path'); + expect(path.style('fill')).toEqual(expectation.path[0], 'bgcolor'); + expect(path.style('stroke')).toEqual(expectation.path[1], 'bordercolor'); + + var text = g.select('text'); + expect(parseInt(text.style('font-size'))).toEqual(expectation.text[0], 'font.size'); + expect(text.style('font-family').split(',')[0]).toEqual(expectation.text[1], 'font.family'); + expect(text.style('fill')).toEqual(expectation.text[2], 'font.color'); + } + + function assertPtLabel(expectation) { + assertLabel('hovertext', expectation); + } + + function assertCommonLabel(expectation) { + assertLabel('axistext', expectation); + } + + function _hover(gd, opts) { + Fx.hover(gd, opts); + delete gd._lastHoverTime; + } + + it('should work for x/y cartesian traces', function(done) { + var gd = createGraphDiv(); + + Plotly.plot(gd, [{ + x: [1, 2, 3], + y: [1, 2, 1], + hoverlabel: { + font: { + color: ['red', 'green', 'blue'], + size: 20 + } + } + }], { + hovermode: 'x', + hoverlabel: { bgcolor: 'white' } + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[0] }); + + assertPtLabel({ + path: ['rgb(255, 255, 255)', 'rgb(68, 68, 68)'], + text: [20, 'Arial', 'rgb(255, 0, 0)'] + }); + assertCommonLabel({ + path: ['rgb(255, 255, 255)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[1] }); + + assertPtLabel({ + path: ['rgb(255, 255, 255)', 'rgb(68, 68, 68)'], + text: [20, 'Arial', 'rgb(0, 128, 0)'] + }); + assertCommonLabel({ + path: ['rgb(255, 255, 255)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[2] }); + + assertPtLabel({ + path: ['rgb(255, 255, 255)', 'rgb(68, 68, 68)'], + text: [20, 'Arial', 'rgb(0, 0, 255)'] + }); + assertCommonLabel({ + path: ['rgb(255, 255, 255)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + }) + .catch(fail) + .then(done); + }); +}); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index ae866723a75..72ced7204a5 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -709,6 +709,22 @@ describe('@noCI, mapbox plots', function() { assertMouseMove(blankPos, 0).then(function() { return assertMouseMove(pointPos, 1); }) + .then(function() { + return Plotly.restyle(gd, { + 'hoverlabel.bgcolor': 'yellow', + 'hoverlabel.font.size': [[20, 10, 30]] + }); + }) + .then(function() { + return assertMouseMove(pointPos, 1); + }) + .then(function() { + var path = d3.select('g.hovertext').select('path'); + var text = d3.select('g.hovertext').select('text'); + + expect(path.style('fill')).toEqual('rgb(255, 255, 0)', 'bgcolor'); + expect(text.style('font-size')).toEqual('20px', 'font.size[0]'); + }) .catch(failTest) .then(done); }, LONG_TIMEOUT_INTERVAL); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index 4eb9e428cc1..281509f5ee9 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -106,7 +106,7 @@ describe('ternary plots', function() { }).then(done); }); - it('should display to hover labels', function() { + it('should display to hover labels', function(done) { var hoverLabels; mouseEvent('mousemove', blankPos[0], blankPos[1]); @@ -121,6 +121,22 @@ describe('ternary plots', function() { expect(rows[0][0].innerHTML).toEqual('Component A: 0.5', 'with correct text'); expect(rows[0][1].innerHTML).toEqual('B: 0.25', 'with correct text'); expect(rows[0][2].innerHTML).toEqual('Component C: 0.25', 'with correct text'); + + Plotly.restyle(gd, { + 'hoverlabel.bordercolor': 'blue', + 'hoverlabel.font.family': [['Gravitas', 'Arial', 'Roboto']] + }) + .then(function() { + delete gd._lastHoverTime; + mouseEvent('mousemove', pointPos[0], pointPos[1]); + + var path = d3.select('g.hovertext').select('path'); + var text = d3.select('g.hovertext').select('text'); + + expect(path.style('stroke')).toEqual('rgb(0, 0, 255)', 'bordercolor'); + expect(text.style('font-family')).toEqual('Gravitas', 'font.family[0]'); + }) + .then(done); }); it('should respond to hover interactions by', function() { From 4ec88ef09cd2da22e43e1e1447396cc7e323dcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 17:48:52 -0400 Subject: [PATCH 21/38] add support for per-point `hoverlabel` setting in 2d z traces --- src/components/fx/calc.js | 23 +++++++---- src/components/fx/hover.js | 17 ++++++-- test/jasmine/tests/hover_label_test.js | 56 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/components/fx/calc.js b/src/components/fx/calc.js index eda8248ff66..5d93ccb07d9 100644 --- a/src/components/fx/calc.js +++ b/src/components/fx/calc.js @@ -9,6 +9,7 @@ 'use strict'; var Lib = require('../../lib'); +var Registry = require('../../registry'); module.exports = function calc(gd) { var calcdata = gd.calcdata; @@ -17,12 +18,20 @@ module.exports = function calc(gd) { var cd = calcdata[i]; var trace = cd[0].trace; - if(trace.hoverlabel) { - Lib.mergeArray(trace.hoverlabel.bgcolor, cd, 'hbg'); - Lib.mergeArray(trace.hoverlabel.bordercolor, cd, 'hbc'); - Lib.mergeArray(trace.hoverlabel.font.size, cd, 'hts'); - Lib.mergeArray(trace.hoverlabel.font.color, cd, 'htc'); - Lib.mergeArray(trace.hoverlabel.font.family, cd, 'htf'); - } + if(!trace.hoverlabel) continue; + + var mergeFn = Registry.traceIs(trace, '2dMap') ? paste : Lib.mergeArray; + + mergeFn(trace.hoverlabel.bgcolor, cd, 'hbg'); + mergeFn(trace.hoverlabel.bordercolor, cd, 'hbc'); + mergeFn(trace.hoverlabel.font.size, cd, 'hts'); + mergeFn(trace.hoverlabel.font.color, cd, 'htc'); + mergeFn(trace.hoverlabel.font.family, cd, 'htf'); } }; + +function paste(traceAttr, cd, cdAttr) { + if(Array.isArray(traceAttr)) { + cd[0][cdAttr] = traceAttr; + } +} diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 0243c4d124e..0ee09026d5d 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1053,6 +1053,7 @@ function alignHoverText(hoverLabels, rotateLabels) { function cleanPoint(d, hovermode) { var trace = d.trace || {}; + var cd0 = d.cd[0]; var cd = d.cd[d.index] || {}; d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; @@ -1133,10 +1134,20 @@ function cleanPoint(d, hovermode) { } function fill(key, calcKey, traceKey) { - if(cd[calcKey]) return d[key] = cd[calcKey]; + var val; + + if(cd[calcKey]) { + val = cd[calcKey]; + } else if(cd0[calcKey]) { + var arr = cd0[calcKey]; + if(Array.isArray(arr) && Array.isArray(arr[d.index[0]])) { + val = arr[d.index[0]][d.index[1]]; + } + } else { + val = Lib.nestedProperty(trace, traceKey).get(); + } - var traceVal = Lib.nestedProperty(trace, traceKey).get(); - if(traceVal) return d[key] = traceVal; + d[key] = val; } fill('color', 'hbg', 'hoverlabel.bgcolor'); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index afe5829fc1c..5a5e64adaea 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1069,4 +1069,60 @@ describe('Test hover label custom styling:', function() { .catch(fail) .then(done); }); + + it('should work for 2d z cartesian traces', function(done) { + var gd = createGraphDiv(); + + Plotly.plot(gd, [{ + type: 'heatmap', + x: [1, 2], + y: [1, 2], + z: [[1, 2], [2, 3]], + hoverlabel: { + font: { + color: 'red', + size: [[10, 20], [21, 11]] + } + } + }], { + hoverlabel: { + bordercolor: 'blue', + font: { family: 'Gravitas'} + } + }) + .then(function() { + _hover(gd, { xval: 1, yval: 1 }); + + assertPtLabel({ + path: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'], + text: [10, 'Gravitas', 'rgb(255, 0, 0)'] + }); + }) + .then(function() { + _hover(gd, { xval: 2, yval: 1 }); + + assertPtLabel({ + path: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'], + text: [20, 'Gravitas', 'rgb(255, 0, 0)'] + }); + }) + .then(function() { + _hover(gd, { xval: 1, yval: 2 }); + + assertPtLabel({ + path: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'], + text: [21, 'Gravitas', 'rgb(255, 0, 0)'] + }); + }) + .then(function() { + _hover(gd, { xval: 2, yval: 2 }); + + assertPtLabel({ + path: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'], + text: [11, 'Gravitas', 'rgb(255, 0, 0)'] + }); + }) + .catch(fail) + .then(done); + }); }); From b05362998b42ad12312f0cc1c23ac5cb9ca44d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 24 Apr 2017 17:49:15 -0400 Subject: [PATCH 22/38] add support for per-pt `hoverlabel` setting in pie traces --- src/traces/pie/plot.js | 10 +++++-- test/jasmine/tests/hover_pie_test.js | 44 ++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 6ef897e1748..322618e271d 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -124,14 +124,20 @@ module.exports = function plot(gd, cdpie) { if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v, separators)); if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); + var hoverLabelOpts = trace2.hoverlabel; + Fx.loneHover({ x0: hoverCenterX - rInscribed * cd0.r, x1: hoverCenterX + rInscribed * cd0.r, y: hoverCenterY, text: thisText.join('
'), name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined, - color: pt.color, - idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right' + idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right', + color: pt.hbg || hoverLabelOpts.bgcolor || pt.color, + borderColor: pt.hbc || hoverLabelOpts.bordercolor, + fontFamily: pt.htf || hoverLabelOpts.font.family, + fontSize: pt.hts || hoverLabelOpts.font.size, + fontColor: pt.htc || hoverLabelOpts.font.color }, { container: fullLayout2._hoverlayer.node(), outerContainer: fullLayout2._paper.node() diff --git a/test/jasmine/tests/hover_pie_test.js b/test/jasmine/tests/hover_pie_test.js index b12b9194c54..eb80224d7a7 100644 --- a/test/jasmine/tests/hover_pie_test.js +++ b/test/jasmine/tests/hover_pie_test.js @@ -187,23 +187,39 @@ describe('pie hovering', function() { function _hover() { mouseEvent('mouseover', 223, 143); + delete gd._lastHoverTime; } - function assertLabel(expected) { - var labels = d3.selectAll('.hovertext .nums .line'); + function assertLabel(content, style) { + var g = d3.selectAll('.hovertext'); + var lines = g.selectAll('.nums .line'); - expect(labels.size()).toBe(expected.length); + expect(lines.size()).toBe(content.length); - labels.each(function(_, i) { - expect(d3.select(this).text()).toBe(expected[i]); + lines.each(function(_, i) { + expect(d3.select(this).text()).toBe(content[i]); }); + + if(style) { + var path = g.select('path'); + expect(path.style('fill')).toEqual(style[0], 'bgcolor'); + expect(path.style('stroke')).toEqual(style[1], 'bordercolor'); + + var text = g.select('text'); + expect(parseInt(text.style('font-size'))).toEqual(style[2], 'font.size'); + expect(text.style('font-family').split(',')[0]).toEqual(style[3], 'font.family'); + expect(text.style('fill')).toEqual(style[4], 'font.color'); + } } it('should show the default selected values', function(done) { Plotly.plot(gd, mockCopy.data, mockCopy.layout) .then(_hover) .then(function() { - assertLabel(['4', '5', '33.3%']); + assertLabel( + ['4', '5', '33.3%'], + ['rgb(31, 119, 180)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'] + ); return Plotly.restyle(gd, 'text', [['A', 'B', 'C', 'D', 'E']]); }) @@ -224,7 +240,23 @@ describe('pie hovering', function() { .then(_hover) .then(function() { assertLabel(['4', 'SUP', '5', '33.3%']); + + return Plotly.restyle(gd, { + 'hoverlabel.bgcolor': [['red', 'green', 'blue']], + 'hoverlabel.bordercolor': 'yellow', + 'hoverlabel.font.size': [[15, 20, 30]], + 'hoverlabel.font.family': 'Roboto', + 'hoverlabel.font.color': 'blue' + }); + }) + .then(_hover) + .then(function() { + assertLabel( + ['4', 'SUP', '5', '33.3%'], + ['rgb(255, 0, 0)', 'rgb(255, 255, 0)', 15, 'Roboto', 'rgb(0, 0, 255)'] + ); }) + .catch(fail) .then(done); }); From 92de0176c26e866c3ffd18a6a1e8dc017cf4c78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 11:02:44 -0400 Subject: [PATCH 23/38] put selection coords (not hoverlabel strings) to event data --- src/plots/gl3d/scene.js | 6 +++--- test/jasmine/tests/gl_plot_interact_test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index e4c44895671..3171b89ab86 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -99,9 +99,9 @@ function render(scene) { var eventData = { points: [{ - x: xVal, - y: yVal, - z: zVal, + x: selection.traceCoordinate[0], + y: selection.traceCoordinate[1], + z: selection.traceCoordinate[2], data: trace._input, fullData: trace, curveNumber: trace.index, diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index dc5a0d70ea9..48bccd6a07b 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -102,7 +102,7 @@ describe('Test gl3d plots', function() { .then(delay) .then(function() { assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); - assertEventData('140.72', '−96.97', '−96.97', 0, 2); + assertEventData(140.72, -96.97, -96.97, 0, 2); return Plotly.restyle(gd, { x: [['2016-01-11', '2016-01-12', '2017-01-01', '2017-02']] @@ -173,7 +173,7 @@ describe('Test gl3d plots', function() { .then(_click) .then(delay) .then(function() { - assertEventData('140.72', '−96.97', '−96.97', 0, 2); + assertEventData(140.72, -96.97, -96.97, 0, 2); }) .then(done); }); From 7332d2377897b1bd226f99dc26b8918ae7be16e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 11:05:19 -0400 Subject: [PATCH 24/38] set `selection.index` during gl3d trace handlePick - so that it reflect the user input indices for surface traces (not the refined indices) - use `selection.index` for other gl3d trace type for consistency (even though selection.data.index worked fine) - add hover test for surface traces --- src/plots/gl3d/scene.js | 3 +- src/traces/mesh3d/convert.js | 2 +- src/traces/scatter3d/convert.js | 2 +- src/traces/surface/convert.js | 2 +- src/traces/surface/index.js | 2 +- test/jasmine/tests/gl_plot_interact_test.js | 35 ++++++++++++++++----- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 3171b89ab86..593c3f751d9 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -68,6 +68,7 @@ function render(scene) { var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate); trace = lastPicked.data; var hoverinfo = trace.hoverinfo; + var ptNumber = selection.index; var xVal = formatter('xaxis', selection.traceCoordinate[0]), yVal = formatter('yaxis', selection.traceCoordinate[1]), @@ -105,7 +106,7 @@ function render(scene) { data: trace._input, fullData: trace, curveNumber: trace.index, - pointNumber: selection.data.index + pointNumber: ptNumber }] }; diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index dee2f0647ca..ddbf71feaca 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -32,7 +32,7 @@ var proto = Mesh3DTrace.prototype; proto.handlePick = function(selection) { if(selection.object === this.mesh) { - var selectIndex = selection.data.index; + var selectIndex = selection.index = selection.data.index; selection.traceCoordinate = [ this.data.x[selectIndex], diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index f491d2b057f..0048b841e1a 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -67,7 +67,7 @@ proto.handlePick = function(selection) { } else selection.textLabel = ''; - var selectIndex = selection.data.index; + var selectIndex = selection.index = selection.data.index; selection.traceCoordinate = [ this.data.x[selectIndex], this.data.y[selectIndex], diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index 1a352a9df6e..90487268a75 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -33,7 +33,7 @@ var proto = SurfaceTrace.prototype; proto.handlePick = function(selection) { if(selection.object === this.surface) { - var selectIndex = [ + var selectIndex = selection.index = [ Math.min( Math.round(selection.data.index[0] / this.dataScale - 1)|0, this.data.z[0].length - 1 diff --git a/src/traces/surface/index.js b/src/traces/surface/index.js index 8a9d1efb156..03e64f5762e 100644 --- a/src/traces/surface/index.js +++ b/src/traces/surface/index.js @@ -20,7 +20,7 @@ Surface.plot = require('./convert'); Surface.moduleType = 'trace'; Surface.name = 'surface'; Surface.basePlotModule = require('../../plots/gl3d'); -Surface.categories = ['gl3d', 'noOpacity']; +Surface.categories = ['gl3d', '2dMap', 'noOpacity']; Surface.meta = { description: [ 'The data the describes the coordinates of the surface is set in `z`.', diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 48bccd6a07b..e7d7fdd3c69 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -42,9 +42,7 @@ describe('Test gl3d plots', function() { mock2.data[0].surfaceaxis = 2; mock2.layout.showlegend = true; - function mouseEventScatter3d(type, opts) { - mouseEvent(type, 605, 271, opts); - } + var mock3 = require('@mocks/gl3d_autocolorscale'); function assertHoverText(xLabel, yLabel, zLabel, textLabel) { var node = d3.selectAll('g.hovertext'); @@ -83,11 +81,11 @@ describe('Test gl3d plots', function() { destroyGraphDiv(); }); - it('@noCI should display correct hover labels and emit correct event data', function(done) { + it('@noCI should display correct hover labels and emit correct event data (scatter3d case)', function(done) { var _mock = Lib.extendDeep({}, mock2); function _hover() { - mouseEventScatter3d('mouseover'); + mouseEvent('mouseover', 605, 271); return delay(); } @@ -150,16 +148,39 @@ describe('Test gl3d plots', function() { assertHoverText('x: 二 6, 2017', 'y: c', 'z: 100k', 'Clementine'); }) .then(done); + }); + + it('@noCI should display correct hover labels and emit correct event data (surface case)', function(done) { + var _mock = Lib.extendDeep({}, mock3); + + function _hover() { + mouseEvent('mouseover', 605, 271); + return delay(); + } + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + gd.on('plotly_hover', function(eventData) { + ptData = eventData.points[0]; + }); + }) + .then(_hover) + .then(delay) + .then(function() { + assertHoverText('x: 1', 'y: 2', 'z: 43', 'one two'); + assertEventData(1, 2, 43, 0, [1, 2]); + }) + .then(done); }); - it('@noCI should emit correct event data on click', function(done) { + it('@noCI should emit correct event data on click (scatter3d case)', function(done) { var _mock = Lib.extendDeep({}, mock2); // N.B. gl3d click events are 'mouseover' events // with button 1 pressed function _click() { - mouseEventScatter3d('mouseover', {buttons: 1}); + mouseEvent('mouseover', 605, 271, {buttons: 1}); return delay(); } From 6bbfa652b4da40f6c060eb4ef72c97dbea98e253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 11:32:28 -0400 Subject: [PATCH 25/38] add support for custom hover label in gl3d --- src/plots/gl3d/scene.js | 21 ++++++++- test/jasmine/tests/gl_plot_interact_test.js | 50 ++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 593c3f751d9..5125d7341f7 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -62,6 +62,21 @@ function render(scene) { return Axes.tickText(axis, axis.d2l(val), 'hover').text; } + function castHoverOption(attr, ptNumber) { + var labelOpts = trace.hoverlabel || {}; + var val = Lib.nestedProperty(labelOpts, attr).get(); + + if(Array.isArray(val)) { + if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) { + return val[ptNumber[0]][ptNumber[1]]; + } else { + return val[ptNumber]; + } + } else { + return val; + } + } + var oldEventData; if(lastPicked !== null) { @@ -92,7 +107,11 @@ function render(scene) { zLabel: zVal, text: selection.textLabel, name: lastPicked.name, - color: lastPicked.color + color: castHoverOption('bgcolor', ptNumber) || lastPicked.color, + borderColor: castHoverOption('bordercolor', ptNumber), + fontFamily: castHoverOption('font.family', ptNumber), + fontSize: castHoverOption('font.size', ptNumber), + fontColor: castHoverOption('font.color', ptNumber) }, { container: svgContainer }); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index e7d7fdd3c69..b4a4f36e6df 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -58,6 +58,19 @@ describe('Test gl3d plots', function() { } } + function assertHoverLabelStyle(bgColor, borderColor, fontSize, fontFamily, fontColor) { + var node = d3.selectAll('g.hovertext'); + + var path = node.select('path'); + expect(path.style('fill')).toEqual(bgColor, 'bgcolor'); + expect(path.style('stroke')).toEqual(borderColor, 'bordercolor'); + + var text = node.select('text'); + expect(parseInt(text.style('font-size'))).toEqual(fontSize, 'font.size'); + expect(text.style('font-family').split(',')[0]).toEqual(fontFamily, 'font.family'); + expect(text.style('fill')).toEqual(fontColor, 'font.color'); + } + function assertEventData(x, y, z, curveNumber, pointNumber) { expect(Object.keys(ptData)).toEqual([ 'x', 'y', 'z', @@ -101,9 +114,10 @@ describe('Test gl3d plots', function() { .then(function() { assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); assertEventData(140.72, -96.97, -96.97, 0, 2); + assertHoverLabelStyle('rgb(0, 0, 255)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'); return Plotly.restyle(gd, { - x: [['2016-01-11', '2016-01-12', '2017-01-01', '2017-02']] + x: [['2016-01-11', '2016-01-12', '2017-01-01', '2017-02-01']] }); }) .then(_hover) @@ -146,6 +160,25 @@ describe('Test gl3d plots', function() { .then(_hover) .then(function() { assertHoverText('x: 二 6, 2017', 'y: c', 'z: 100k', 'Clementine'); + + return Plotly.restyle(gd, { + 'hoverlabel.bgcolor': [['red', 'blue', 'green', 'yellow']], + 'hoverlabel.font.size': 20 + }); + }) + .then(_hover) + .then(function() { + assertHoverLabelStyle('rgb(0, 128, 0)', 'rgb(255, 255, 255)', 20, 'Arial', 'rgb(255, 255, 255)'); + + return Plotly.relayout(gd, { + 'hoverlabel.bordercolor': 'yellow', + 'hoverlabel.font.color': 'cyan', + 'hoverlabel.font.family': 'Roboto' + }); + }) + .then(_hover) + .then(function() { + assertHoverLabelStyle('rgb(0, 128, 0)', 'rgb(255, 255, 0)', 20, 'Roboto', 'rgb(0, 255, 255)'); }) .then(done); }); @@ -170,6 +203,21 @@ describe('Test gl3d plots', function() { .then(function() { assertHoverText('x: 1', 'y: 2', 'z: 43', 'one two'); assertEventData(1, 2, 43, 0, [1, 2]); + assertHoverLabelStyle('rgb(68, 68, 68)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'); + + Plotly.restyle(gd, { + 'hoverlabel.bgcolor': 'white', + 'hoverlabel.font.size': 9, + 'hoverlabel.font.color': [[ + ['red', 'blue', 'green'], + ['pink', 'purple', 'cyan'], + ['black', 'orange', 'yellow'] + ]] + }); + }) + .then(_hover) + .then(function() { + assertHoverLabelStyle('rgb(255, 255, 255)', 'rgb(68, 68, 68)', 9, 'Arial', 'rgb(0, 255, 255)'); }) .then(done); }); From 28c27707cf72fce36df3ec9d05888fc9f38da92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 12:20:14 -0400 Subject: [PATCH 26/38] add support for custom hover label in gl2d --- src/plots/gl2d/scene2d.js | 25 +++++- src/traces/contourgl/convert.js | 1 + src/traces/heatmapgl/convert.js | 1 + src/traces/pointcloud/convert.js | 3 +- test/jasmine/tests/gl2d_click_test.js | 108 +++++++++++++++++++++++--- 5 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index abd054cf892..bea9534c9c0 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -9,6 +9,7 @@ 'use strict'; +var Lib = require('../../lib'); var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); var Fx = require('../../components/fx'); @@ -633,6 +634,9 @@ proto.draw = function() { if(parts.indexOf('name') === -1) selection.name = undefined; } + var trace = this.fullData[selection.trace.index] || {}; + var ptNumber = selection.pointIndex; + Fx.loneHover({ x: selection.screenCoord[0], y: selection.screenCoord[1], @@ -641,7 +645,11 @@ proto.draw = function() { zLabel: selection.traceCoord[2], text: selection.textLabel, name: selection.name, - color: selection.color + color: this.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color, + borderColor: this.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: this.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: this.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: this.castHoverOption(trace, ptNumber, 'font.color') }, { container: this.svgContainer }); @@ -667,3 +675,18 @@ proto.hoverFormatter = function(axisName, val) { var axis = this[axisName]; return Axes.tickText(axis, axis.c2l(val), 'hover').text; }; + +proto.castHoverOption = function(trace, ptNumber, attr) { + var labelOpts = trace.hoverlabel || {}; + var val = Lib.nestedProperty(labelOpts, attr).get(); + + if(Array.isArray(val)) { + if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) { + return val[ptNumber[0]][ptNumber[1]]; + } else { + return val[ptNumber]; + } + } else { + return val; + } +}; diff --git a/src/traces/contourgl/convert.js b/src/traces/contourgl/convert.js index 2c3ef01984c..182ec900b7f 100644 --- a/src/traces/contourgl/convert.js +++ b/src/traces/contourgl/convert.js @@ -85,6 +85,7 @@ proto.handlePick = function(pickResult) { proto.update = function(fullTrace, calcTrace) { var calcPt = calcTrace[0]; + this.index = fullTrace.index; this.name = fullTrace.name; this.hoverinfo = fullTrace.hoverinfo; diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js index 5bdeed828c8..fbac9353f3e 100644 --- a/src/traces/heatmapgl/convert.js +++ b/src/traces/heatmapgl/convert.js @@ -71,6 +71,7 @@ proto.handlePick = function(pickResult) { proto.update = function(fullTrace, calcTrace) { var calcPt = calcTrace[0]; + this.index = fullTrace.index; this.name = fullTrace.name; this.hoverinfo = fullTrace.hoverinfo; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 95ac5461cb4..98c25b5b668 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -48,7 +48,6 @@ function Pointcloud(scene, uid) { var proto = Pointcloud.prototype; proto.handlePick = function(pickResult) { - var index = this.idToIndex[pickResult.pointId]; // prefer the readout from XY, if present @@ -69,7 +68,7 @@ proto.handlePick = function(pickResult) { }; proto.update = function(options) { - + this.index = options.index; this.textLabels = options.text; this.name = options.name; this.hoverinfo = options.hoverinfo; diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index ac6df004e25..37067efedef 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); +var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); @@ -27,7 +28,7 @@ var mock3 = { [10, 10.625, 12.5, 15.625, 20], [5.625, 6.25, 8.125, 11.25, 15.625], [2.5, 3.125, 5, 8.125, 12.5], - [0.625, 1.25, 3.125, 6.25, 10.625], + [0.625, 1.25, 3.125, 20, 10.625], [0, 0.625, 2.5, 5.625, 10] ], colorscale: 'Jet', @@ -128,6 +129,22 @@ describe('Test hover and click interactions', function() { expect(pt.pointNumber).toEqual(expected.pointNumber, 'point number'); } + function assertHoverLabelStyle(sel, expected) { + if(sel.node() === null) { + expect(expected.noHoverLabel).toBe(true); + return; + } + + var path = sel.select('path'); + expect(path.style('fill')).toEqual(expected.bgColor, 'bgcolor'); + expect(path.style('stroke')).toEqual(expected.borderColor, 'bordercolor'); + + var text = sel.select('text.nums'); + expect(parseInt(text.style('font-size'))).toEqual(expected.fontSize, 'font.size'); + expect(text.style('font-family').split(',')[0]).toEqual(expected.fontFamily, 'font.family'); + expect(text.style('fill')).toEqual(expected.fontColor, 'font.color'); + } + // returns basic hover/click/unhover runner for one xy position function makeRunner(pos, expected, opts) { opts = opts || {}; @@ -144,6 +161,7 @@ describe('Test hover and click interactions', function() { .then(_hover) .then(function(eventData) { assertEventData(eventData, expected); + assertHoverLabelStyle(d3.select('g.hovertext'), expected); }) .then(_click) .then(function(eventData) { @@ -171,11 +189,28 @@ describe('Test hover and click interactions', function() { it('should output correct event data for scattergl', function(done) { var _mock = Lib.extendDeep({}, mock1); + + _mock.layout.hoverlabel = { + font: { + size: 20, + color: 'yellow' + } + }; + _mock.data[0].hoverlabel = { + bgcolor: 'blue', + bordercolor: _mock.data[0].x.map(function(_, i) { return i % 2 ? 'red' : 'green'; }) + }; + var run = makeRunner([655, 317], { x: 15.772, y: 0.387, curveNumber: 0, - pointNumber: 33 + pointNumber: 33, + bgColor: 'rgb(0, 0, 255)', + borderColor: 'rgb(255, 0, 0)', + fontSize: 20, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 0)' }); Plotly.plot(gd, _mock) @@ -192,7 +227,8 @@ describe('Test hover and click interactions', function() { x: 15.772, y: 0.387, curveNumber: 0, - pointNumber: 33 + pointNumber: 33, + noHoverLabel: true }); Plotly.plot(gd, _mock) @@ -204,11 +240,21 @@ describe('Test hover and click interactions', function() { it('should output correct event data for pointcloud', function(done) { var _mock = Lib.extendDeep({}, mock2); + _mock.layout.hoverlabel = { font: {size: 8} }; + _mock.data[2].hoverlabel = { + bgcolor: ['red', 'green', 'blue'] + }; + var run = makeRunner([540, 150], { x: 4.5, y: 9, curveNumber: 2, - pointNumber: 1 + pointNumber: 1, + bgColor: 'rgb(0, 128, 0)', + borderColor: 'rgb(255, 255, 255)', + fontSize: 8, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' }); Plotly.plot(gd, _mock) @@ -221,11 +267,24 @@ describe('Test hover and click interactions', function() { var _mock = Lib.extendDeep({}, mock3); _mock.data[0].type = 'heatmapgl'; + _mock.data[0].hoverlabel = { + font: { size: _mock.data[0].z } + }; + + _mock.layout.hoverlabel = { + font: { family: 'Roboto' } + }; + var run = makeRunner([540, 150], { x: 3, y: 3, curveNumber: 0, - pointNumber: [3, 3] + pointNumber: [3, 3], + bgColor: 'rgb(68, 68, 68)', + borderColor: 'rgb(255, 255, 255)', + fontSize: 20, + fontFamily: 'Roboto', + fontColor: 'rgb(255, 255, 255)' }, { noUnHover: true }); @@ -243,7 +302,12 @@ describe('Test hover and click interactions', function() { x: 8, y: 18, curveNumber: 2, - pointNumber: 0 + pointNumber: 0, + bgColor: 'rgb(44, 160, 44)', + borderColor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' }); // after the restyle, autorange changes the y range @@ -251,7 +315,12 @@ describe('Test hover and click interactions', function() { x: 8, y: 18, curveNumber: 2, - pointNumber: 0 + pointNumber: 0, + bgColor: 'rgb(255, 127, 14)', + borderColor: 'rgb(68, 68, 68)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(68, 68, 68)' }); Plotly.plot(gd, _mock) @@ -274,7 +343,12 @@ describe('Test hover and click interactions', function() { x: 8, y: 18, curveNumber: 2, - pointNumber: 0 + pointNumber: 0, + bgColor: 'rgb(44, 160, 44)', + borderColor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' }); // after the restyle, autorange changes the x AND y ranges @@ -285,7 +359,12 @@ describe('Test hover and click interactions', function() { x: 8, y: 18, curveNumber: 2, - pointNumber: 0 + pointNumber: 0, + bgColor: 'rgb(255, 127, 14)', + borderColor: 'rgb(68, 68, 68)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(68, 68, 68)' }); Plotly.plot(gd, _mock) @@ -301,11 +380,20 @@ describe('Test hover and click interactions', function() { it('should output correct event data contourgl', function(done) { var _mock = Lib.extendDeep({}, mock3); + _mock.data[0].hoverlabel = { + font: { size: _mock.data[0].z } + }; + var run = makeRunner([540, 150], { x: 3, y: 3, curveNumber: 0, - pointNumber: [3, 3] + pointNumber: [3, 3], + bgColor: 'rgb(68, 68, 68)', + borderColor: 'rgb(255, 255, 255)', + fontSize: 20, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' }, { noUnHover: true }); From cda735b99a15d78b315fa2c5222751a37955b502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 12:31:22 -0400 Subject: [PATCH 27/38] make hoverlabel text selection query more robust - so that it doesn't catch the trace name node by accident if present. --- test/jasmine/tests/gl_plot_interact_test.js | 2 +- test/jasmine/tests/hover_label_test.js | 2 +- test/jasmine/tests/hover_pie_test.js | 2 +- test/jasmine/tests/mapbox_test.js | 2 +- test/jasmine/tests/ternary_test.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index b4a4f36e6df..a20a1dd0fb9 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -65,7 +65,7 @@ describe('Test gl3d plots', function() { expect(path.style('fill')).toEqual(bgColor, 'bgcolor'); expect(path.style('stroke')).toEqual(borderColor, 'bordercolor'); - var text = node.select('text'); + var text = node.select('text.nums'); expect(parseInt(text.style('font-size'))).toEqual(fontSize, 'font.size'); expect(text.style('font-family').split(',')[0]).toEqual(fontFamily, 'font.family'); expect(text.style('fill')).toEqual(fontColor, 'font.color'); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 5a5e64adaea..b18110042b4 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -995,7 +995,7 @@ describe('Test hover label custom styling:', function() { expect(path.style('fill')).toEqual(expectation.path[0], 'bgcolor'); expect(path.style('stroke')).toEqual(expectation.path[1], 'bordercolor'); - var text = g.select('text'); + var text = g.select({hovertext: 'text.nums', axistext: 'text'}[className]); expect(parseInt(text.style('font-size'))).toEqual(expectation.text[0], 'font.size'); expect(text.style('font-family').split(',')[0]).toEqual(expectation.text[1], 'font.family'); expect(text.style('fill')).toEqual(expectation.text[2], 'font.color'); diff --git a/test/jasmine/tests/hover_pie_test.js b/test/jasmine/tests/hover_pie_test.js index eb80224d7a7..464ece53993 100644 --- a/test/jasmine/tests/hover_pie_test.js +++ b/test/jasmine/tests/hover_pie_test.js @@ -205,7 +205,7 @@ describe('pie hovering', function() { expect(path.style('fill')).toEqual(style[0], 'bgcolor'); expect(path.style('stroke')).toEqual(style[1], 'bordercolor'); - var text = g.select('text'); + var text = g.select('text.nums'); expect(parseInt(text.style('font-size'))).toEqual(style[2], 'font.size'); expect(text.style('font-family').split(',')[0]).toEqual(style[3], 'font.family'); expect(text.style('fill')).toEqual(style[4], 'font.color'); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 72ced7204a5..d0a155bc72d 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -720,7 +720,7 @@ describe('@noCI, mapbox plots', function() { }) .then(function() { var path = d3.select('g.hovertext').select('path'); - var text = d3.select('g.hovertext').select('text'); + var text = d3.select('g.hovertext').select('text.nums'); expect(path.style('fill')).toEqual('rgb(255, 255, 0)', 'bgcolor'); expect(text.style('font-size')).toEqual('20px', 'font.size[0]'); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index 281509f5ee9..741d45d511b 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -131,7 +131,7 @@ describe('ternary plots', function() { mouseEvent('mousemove', pointPos[0], pointPos[1]); var path = d3.select('g.hovertext').select('path'); - var text = d3.select('g.hovertext').select('text'); + var text = d3.select('g.hovertext').select('text.nums'); expect(path.style('stroke')).toEqual('rgb(0, 0, 255)', 'bordercolor'); expect(text.style('font-family')).toEqual('Gravitas', 'font.family[0]'); From ce27e10d40fbb02c48301b5377690b98a557d198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 25 Apr 2017 13:56:48 -0400 Subject: [PATCH 28/38] bump back max allowed circular deps to 17 - somehow `madge` identifies circular in an order-dependent way, bump back to pre PR #1613 value. --- tasks/test_syntax.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index b23620bd301..577e099fb86 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -177,7 +177,7 @@ function assertCircularDeps() { var logs = []; // see https://github.com/plotly/plotly.js/milestone/9 - var MAX_ALLOWED_CIRCULAR_DEPS = 12; + var MAX_ALLOWED_CIRCULAR_DEPS = 17; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { console.log(circularDeps.join('\n')); From be9e6b9a7c265a6e7ffb29386af5b16814a5f0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 26 Apr 2017 14:31:50 -0400 Subject: [PATCH 29/38] resolves #1575 - call Fx.hover with evt object containing 'xpx' & 'ypx' - to test main codepath instead of fallback `ax._length` --- test/jasmine/tests/hover_label_test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b18110042b4..a86ce60efa6 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -16,11 +16,8 @@ var fail = require('../assets/fail_test'); describe('hover info', function() { 'use strict'; - var mock = require('@mocks/14.json'), - evt = { - clientX: mock.layout.width / 2, - clientY: mock.layout.height / 2 - }; + var mock = require('@mocks/14.json'); + var evt = { xpx: 355, ypx: 150 }; afterEach(destroyGraphDiv); From cf61dc2f167ea2ca8704a0cf30cecd1985a071e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 26 Apr 2017 15:24:27 -0400 Subject: [PATCH 30/38] fixes #1600 - allow z cartesian traces to have a name hover label --- src/components/fx/hover.js | 2 +- test/jasmine/tests/hover_label_test.js | 46 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 0ee09026d5d..15feb60883f 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -696,7 +696,7 @@ function createHoverText(hoverData, opts) { // to get custom 'name' labels pass cleanPoint if(d.nameOverride !== undefined) d.name = d.nameOverride; - if(d.name && d.zLabelVal === undefined) { + if(d.name) { // strip out our pseudo-html elements from d.name (if it exists at all) name = svgTextUtils.plainText(d.name || ''); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index a86ce60efa6..8ebd133deaa 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -444,7 +444,53 @@ describe('hover info', function() { done(); }); + }); + }); + + describe('\'hover info for x/y/z traces', function() { + function _hover(gd, xpx, ypx) { + Fx.hover(gd, { xpx: xpx, ypx: ypx }, 'xy'); + delete gd._lastHoverTime; + } + + function _assert(nameLabel, lines) { + expect(d3.select('g.axistext').size()).toEqual(0, 'no common label'); + + var sel = d3.select('g.hovertext'); + expect(sel.select('text.name').html()).toEqual(nameLabel, 'name label'); + sel.select('text.nums').selectAll('tspan').each(function(_, i) { + expect(d3.select(this).html()).toEqual(lines[i], 'lines ' + i); + }); + } + it('should display correct label content', function(done) { + var gd = createGraphDiv(); + + Plotly.plot(gd, [{ + type: 'heatmap', + y: [0, 1], + z: [[1, 2, 3], [2, 2, 1]], + name: 'one', + }, { + type: 'heatmap', + y: [2, 3], + z: [[1, 2, 3], [2, 2, 1]], + name: 'two' + }], { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0} + }) + .then(function() { + _hover(gd, 250, 100); + _assert('two', ['x: 1', 'y: 3', 'z: 2']); + }) + .then(function() { + _hover(gd, 250, 300); + _assert('one', ['x: 1', 'y: 1', 'z: 2']); + }) + .catch(fail) + .then(done); }); }); From 9eeadcf85048bc39b53396e0b97e16497d558ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 26 Apr 2017 17:52:26 -0400 Subject: [PATCH 31/38] don't override hover label data by undefined vals - so that e.g. the hover label `color` field isn't overridden by undefined `hoverlabel.bgcolor` values. --- src/components/fx/hover.js | 2 +- test/jasmine/tests/hover_label_test.js | 42 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 0ee09026d5d..d2b0641994c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1147,7 +1147,7 @@ function cleanPoint(d, hovermode) { val = Lib.nestedProperty(trace, traceKey).get(); } - d[key] = val; + if(val) d[key] = val; } fill('color', 'hbg', 'hoverlabel.bgcolor'); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b18110042b4..aef9589b284 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1020,6 +1020,9 @@ describe('Test hover label custom styling:', function() { Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 1], + marker: { + color: ['yellow', 'black', 'cyan'] + }, hoverlabel: { font: { color: ['red', 'green', 'blue'], @@ -1065,6 +1068,45 @@ describe('Test hover label custom styling:', function() { path: ['rgb(255, 255, 255)', 'rgb(255, 255, 255)'], text: [13, 'Arial', 'rgb(255, 255, 255)'] }); + + // test base case + return Plotly.update(gd, { hoverlabel: null }, { hoverlabel: null }); + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[0] }); + + assertPtLabel({ + path: ['rgb(255, 255, 0)', 'rgb(68, 68, 68)'], + text: [13, 'Arial', 'rgb(68, 68, 68)'] + }); + assertCommonLabel({ + path: ['rgb(68, 68, 68)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[1] }); + + assertPtLabel({ + path: ['rgb(0, 0, 0)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + assertCommonLabel({ + path: ['rgb(68, 68, 68)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); + }) + .then(function() { + _hover(gd, { xval: gd._fullData[0].x[2] }); + + assertPtLabel({ + path: ['rgb(0, 255, 255)', 'rgb(68, 68, 68)'], + text: [13, 'Arial', 'rgb(68, 68, 68)'] + }); + assertCommonLabel({ + path: ['rgb(68, 68, 68)', 'rgb(255, 255, 255)'], + text: [13, 'Arial', 'rgb(255, 255, 255)'] + }); }) .catch(fail) .then(done); From 83219598333010bf2d82334620aafdd95f718f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Apr 2017 12:27:59 -0400 Subject: [PATCH 32/38] make sure arrayOk resyle logic works for hoverlabel attributes - and all (future) plots/attributes.js declarations. --- src/plot_api/plot_api.js | 4 +++- test/jasmine/tests/plot_api_test.js | 32 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 48bea3d137a..f11c89113a9 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1608,7 +1608,9 @@ function _restyle(gd, aobj, _traces) { } else { var moduleAttrs = (contFull._module || {}).attributes || {}; - var valObject = Lib.nestedProperty(moduleAttrs, ai).get() || {}; + var valObject = Lib.nestedProperty(moduleAttrs, ai).get() || + Lib.nestedProperty(Plots.attributes, ai).get() || + {}; // if restyling entire attribute container, assume worse case if(!valObject.valType) { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index eee008c4a4f..055631e6f98 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -254,7 +254,7 @@ describe('Test plot api', function() { }); }); - describe('Plotly.restyle', function() { + describe('Plotly.restyle subroutines switchboard', function() { beforeEach(function() { spyOn(PlotlyInternal, 'plot'); spyOn(Plots, 'previousPromises'); @@ -330,6 +330,36 @@ describe('Test plot api', function() { expect(PlotlyInternal.plot).toHaveBeenCalled(); }); + it('should do full replot when arrayOk base attributes are updated', function() { + var gd = { + data: [{x: [1, 2, 3], y: [1, 2, 3]}], + layout: {} + }; + + mockDefaultsAndCalc(gd); + Plotly.restyle(gd, 'hoverlabel.bgcolor', [['red', 'green', 'blue']]); + expect(gd.calcdata).toBeUndefined(); + expect(PlotlyInternal.plot).toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + PlotlyInternal.plot.calls.reset(); + Plotly.restyle(gd, 'hoverlabel.bgcolor', 'yellow'); + expect(gd.calcdata).toBeUndefined(); + expect(PlotlyInternal.plot).toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + PlotlyInternal.plot.calls.reset(); + Plotly.restyle(gd, 'hoverlabel.bgcolor', 'blue'); + expect(gd.calcdata).toBeDefined(); + expect(PlotlyInternal.plot).not.toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + PlotlyInternal.plot.calls.reset(); + Plotly.restyle(gd, 'hoverlabel.bgcolor', [['red', 'blue', 'green']]); + expect(gd.calcdata).toBeUndefined(); + expect(PlotlyInternal.plot).toHaveBeenCalled(); + }); + it('should do full replot when attribute container are updated', function() { var gd = { data: [{x: [1, 2, 3], y: [1, 2, 3]}], From 60bd4fcbe2fd768e08dd430b8ac39c7af39df1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 May 2017 15:53:53 -0400 Subject: [PATCH 33/38] factor out gl2d / gl3d cast hover options logic into 1 Fx method --- src/components/fx/index.js | 17 +++++++++++++++++ src/plots/gl2d/scene2d.js | 26 +++++--------------------- src/plots/gl3d/scene.js | 25 +++++-------------------- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/components/fx/index.js b/src/components/fx/index.js index 8b1e6f4cb16..545548bcbbd 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -35,6 +35,7 @@ module.exports = { getDistanceFunction: helpers.getDistanceFunction, getClosest: helpers.getClosest, inbox: helpers.inbox, + castHoverOption: castHoverOption, hover: require('./hover').hover, unhover: dragElement.unhover, @@ -55,3 +56,19 @@ function loneUnhover(containerOrSelection) { selection.selectAll('g.hovertext').remove(); selection.selectAll('.spikeline').remove(); } + +// Handler for trace-wide vs per-point hover label options +function castHoverOption(trace, ptNumber, attr) { + var labelOpts = trace.hoverlabel || {}; + var val = Lib.nestedProperty(labelOpts, attr).get(); + + if(Array.isArray(val)) { + if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) { + return val[ptNumber[0]][ptNumber[1]]; + } else { + return val[ptNumber]; + } + } else { + return val; + } +} diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index bea9534c9c0..68052f8da55 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -9,7 +9,6 @@ 'use strict'; -var Lib = require('../../lib'); var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); var Fx = require('../../components/fx'); @@ -645,11 +644,11 @@ proto.draw = function() { zLabel: selection.traceCoord[2], text: selection.textLabel, name: selection.name, - color: this.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color, - borderColor: this.castHoverOption(trace, ptNumber, 'bordercolor'), - fontFamily: this.castHoverOption(trace, ptNumber, 'font.family'), - fontSize: this.castHoverOption(trace, ptNumber, 'font.size'), - fontColor: this.castHoverOption(trace, ptNumber, 'font.color') + color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color, + borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color') }, { container: this.svgContainer }); @@ -675,18 +674,3 @@ proto.hoverFormatter = function(axisName, val) { var axis = this[axisName]; return Axes.tickText(axis, axis.c2l(val), 'hover').text; }; - -proto.castHoverOption = function(trace, ptNumber, attr) { - var labelOpts = trace.hoverlabel || {}; - var val = Lib.nestedProperty(labelOpts, attr).get(); - - if(Array.isArray(val)) { - if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) { - return val[ptNumber[0]][ptNumber[1]]; - } else { - return val[ptNumber]; - } - } else { - return val; - } -}; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 5125d7341f7..eb108e89899 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -62,21 +62,6 @@ function render(scene) { return Axes.tickText(axis, axis.d2l(val), 'hover').text; } - function castHoverOption(attr, ptNumber) { - var labelOpts = trace.hoverlabel || {}; - var val = Lib.nestedProperty(labelOpts, attr).get(); - - if(Array.isArray(val)) { - if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) { - return val[ptNumber[0]][ptNumber[1]]; - } else { - return val[ptNumber]; - } - } else { - return val; - } - } - var oldEventData; if(lastPicked !== null) { @@ -107,11 +92,11 @@ function render(scene) { zLabel: zVal, text: selection.textLabel, name: lastPicked.name, - color: castHoverOption('bgcolor', ptNumber) || lastPicked.color, - borderColor: castHoverOption('bordercolor', ptNumber), - fontFamily: castHoverOption('font.family', ptNumber), - fontSize: castHoverOption('font.size', ptNumber), - fontColor: castHoverOption('font.color', ptNumber) + color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || lastPicked.color, + borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color') }, { container: svgContainer }); From e45bff94d36126ba586d5a26fb5516ee374a3986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 May 2017 16:04:40 -0400 Subject: [PATCH 34/38] perf improvements in fx helpers --- src/components/fx/helpers.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index 683009efe30..c45da2b0ee5 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -17,14 +17,18 @@ exports.getSubplot = function getSubplot(trace) { // convenience functions for mapping all relevant axes exports.flat = function flat(subplots, v) { - var out = []; - for(var i = subplots.length; i > 0; i--) out.push(v); + var out = new Array(subplots.length); + for(var i = 0; i < subplots.length; i++) { + out[i] = v; + } return out; }; exports.p2c = function p2c(axArray, v) { - var out = []; - for(var i = 0; i < axArray.length; i++) out.push(axArray[i].p2c(v)); + var out = new Array(axArray.length); + for(var i = 0; i < axArray.length; i++) { + out[i] = axArray[i].p2c(v); + } return out; }; From 8d021e0640a44da68a333426d77035ff67e27108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 10 May 2017 12:23:29 -0400 Subject: [PATCH 35/38] :hocho: :evergreen_tree: in sankey tests --- test/jasmine/tests/sankey_test.js | 41 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index a03da2c9c59..5b587f68376 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -260,6 +260,12 @@ describe('sankey tests', function() { afterEach(destroyGraphDiv); + function wait() { + return new Promise(function(resolve) { + setTimeout(resolve, 60); + }); + } + it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) { var gd = createGraphDiv(); @@ -313,30 +319,23 @@ describe('sankey tests', function() { }); it('Plotly.plot shows and removes tooltip on node, link', function(done) { - var gd = createGraphDiv(); var mockCopy = Lib.extendDeep({}, mock); - Plotly.plot(gd, mockCopy) - .then(function() { - - mouseEvent('mousemove', 400, 300); - mouseEvent('mouseover', 400, 300); - - window.setTimeout(function() { - expect(d3.select('.hoverlayer>.hovertext>text').node().innerHTML) - .toEqual('447TWh', 'tooltip present'); - - mouseEvent('mousemove', 450, 300); - mouseEvent('mouseover', 450, 300); - - window.setTimeout(function() { - expect(d3.select('.hoverlayer>.hovertext>text').node().innerHTML) - .toEqual('46TWh', 'tooltip jumped to link'); - done(); - }, 60); - }, 60); - }); + Plotly.plot(gd, mockCopy).then(function() { + mouseEvent('mousemove', 400, 300); + mouseEvent('mouseover', 400, 300); + }) + .then(wait) + .then(function() { + expect(d3.select('.hoverlayer>.hovertext>text').node().innerHTML) + .toEqual('447TWh', 'tooltip present'); + }) + .then(function() { + mouseEvent('mousemove', 450, 300); + mouseEvent('mouseover', 450, 300); + }) + .then(done); }); }); }); From 4d3ce9203fc6c2a0ffa21d9236708655b689e636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 10 May 2017 12:23:52 -0400 Subject: [PATCH 36/38] implement 'hoverlabel' settings in sankey hover routines --- src/traces/sankey/plot.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index d5449c7e64c..88464036010 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -124,7 +124,8 @@ module.exports = function plot(gd, calcData) { }; var linkHoverFollow = function(element, d) { - + var trace = gd._fullData[d.traceId]; + var ptNumber = d.originalIndex; var boundingBox = element.getBoundingClientRect(); var hoverCenterX = boundingBox.left + boundingBox.width / 2; var hoverCenterY = boundingBox.top + boundingBox.height / 2; @@ -138,7 +139,11 @@ module.exports = function plot(gd, calcData) { ['Source:', d.link.source.label].join(' '), ['Target:', d.link.target.label].join(' ') ].filter(renderableValuePresent).join('
'), - color: Color.addOpacity(d.tinyColorHue, 1), + color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1), + borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), idealAlign: d3.event.x < hoverCenterX ? 'right' : 'left' }, { container: fullLayout._hoverlayer.node(), @@ -171,7 +176,8 @@ module.exports = function plot(gd, calcData) { }; var nodeHoverFollow = function(element, d) { - + var trace = gd._fullData[d.traceId]; + var ptNumber = d.originalIndex; var nodeRect = d3.select(element).select('.nodeRect'); var boundingBox = nodeRect.node().getBoundingClientRect(); var hoverCenterX0 = boundingBox.left - 2; @@ -188,7 +194,11 @@ module.exports = function plot(gd, calcData) { ['Incoming flow count:', d.node.targetLinks.length].join(' '), ['Outgoing flow count:', d.node.sourceLinks.length].join(' ') ].filter(renderableValuePresent).join('
'), - color: d.tinyColorHue, + color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || d.tinyColorHue, + borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), idealAlign: 'left' }, { container: fullLayout._hoverlayer.node(), From e50886c981018c60a460b60ce79283a01ee37031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 10 May 2017 13:27:06 -0400 Subject: [PATCH 37/38] fixup: don't support array value in sankey hoverlabel settings - see https://github.com/plotly/plotly.js/pull/1582/commits/4d3ce9203fc6c2a0ffa21d9236708655b689e636#r115788926 for rationale --- src/traces/sankey/plot.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index 88464036010..f4120480a54 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -12,6 +12,7 @@ var d3 = require('d3'); var render = require('./render'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); +var Lib = require('../../lib'); function renderableValuePresent(d) {return d !== '';} @@ -106,6 +107,13 @@ function linkNonHoveredStyle(d, sankey, visitNodes, sankeyLink) { } } +// does not support array values for now +function castHoverOption(trace, attr) { + var labelOpts = trace.hoverlabel || {}; + var val = Lib.nestedProperty(labelOpts, attr).get(); + return Array.isArray(val) ? false : val; +} + module.exports = function plot(gd, calcData) { var fullLayout = gd._fullLayout; @@ -125,7 +133,6 @@ module.exports = function plot(gd, calcData) { var linkHoverFollow = function(element, d) { var trace = gd._fullData[d.traceId]; - var ptNumber = d.originalIndex; var boundingBox = element.getBoundingClientRect(); var hoverCenterX = boundingBox.left + boundingBox.width / 2; var hoverCenterY = boundingBox.top + boundingBox.height / 2; @@ -139,11 +146,11 @@ module.exports = function plot(gd, calcData) { ['Source:', d.link.source.label].join(' '), ['Target:', d.link.target.label].join(' ') ].filter(renderableValuePresent).join('
'), - color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1), - borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), - fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), - fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), - fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), + color: castHoverOption(trace, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1), + borderColor: castHoverOption(trace, 'bordercolor'), + fontFamily: castHoverOption(trace, 'font.family'), + fontSize: castHoverOption(trace, 'font.size'), + fontColor: castHoverOption(trace, 'font.color'), idealAlign: d3.event.x < hoverCenterX ? 'right' : 'left' }, { container: fullLayout._hoverlayer.node(), @@ -177,7 +184,6 @@ module.exports = function plot(gd, calcData) { var nodeHoverFollow = function(element, d) { var trace = gd._fullData[d.traceId]; - var ptNumber = d.originalIndex; var nodeRect = d3.select(element).select('.nodeRect'); var boundingBox = nodeRect.node().getBoundingClientRect(); var hoverCenterX0 = boundingBox.left - 2; @@ -194,11 +200,11 @@ module.exports = function plot(gd, calcData) { ['Incoming flow count:', d.node.targetLinks.length].join(' '), ['Outgoing flow count:', d.node.sourceLinks.length].join(' ') ].filter(renderableValuePresent).join('
'), - color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || d.tinyColorHue, - borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), - fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), - fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), - fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), + color: castHoverOption(trace, 'bgcolor') || d.tinyColorHue, + borderColor: castHoverOption(trace, 'bordercolor'), + fontFamily: castHoverOption(trace, 'font.family'), + fontSize: castHoverOption(trace, 'font.size'), + fontColor: castHoverOption(trace, 'font.color'), idealAlign: 'left' }, { container: fullLayout._hoverlayer.node(), From f8258cb4d13040d389131dc53540491db86d52c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 10 May 2017 13:27:24 -0400 Subject: [PATCH 38/38] add sankey hover label style tests --- test/jasmine/tests/sankey_test.js | 102 +++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index 5b587f68376..3ecd6b24089 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -9,6 +9,7 @@ var Sankey = require('@src/traces/sankey'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var fail = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); describe('sankey tests', function() { @@ -257,15 +258,8 @@ describe('sankey tests', function() { }); describe('lifecycle methods', function() { - afterEach(destroyGraphDiv); - function wait() { - return new Promise(function(resolve) { - setTimeout(resolve, 60); - }); - } - it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) { var gd = createGraphDiv(); @@ -317,24 +311,102 @@ describe('sankey tests', function() { done(); }); }); + }); + + describe('Test hover/click interactions:', function() { + afterEach(destroyGraphDiv); + + function assertLabel(content, style) { + var g = d3.selectAll('.hovertext'); + var lines = g.selectAll('.nums .line'); + var name = g.selectAll('.name'); + + expect(lines.size()).toBe(content.length - 1); + + lines.each(function(_, i) { + expect(d3.select(this).text()).toBe(content[i]); + }); + + expect(name.text()).toBe(content[content.length - 1]); + + var path = g.select('path'); + expect(path.style('fill')).toEqual(style[0], 'bgcolor'); + expect(path.style('stroke')).toEqual(style[1], 'bordercolor'); + + var text = g.select('text.nums'); + expect(parseInt(text.style('font-size'))).toEqual(style[2], 'font.size'); + expect(text.style('font-family').split(',')[0]).toEqual(style[3], 'font.family'); + expect(text.style('fill')).toEqual(style[4], 'font.color'); + } - it('Plotly.plot shows and removes tooltip on node, link', function(done) { + it('should shows the correct hover labels', function(done) { var gd = createGraphDiv(); var mockCopy = Lib.extendDeep({}, mock); + function _hover(px, py) { + mouseEvent('mousemove', px, py); + mouseEvent('mouseover', px, py); + delete gd._lastHoverTime; + } + Plotly.plot(gd, mockCopy).then(function() { - mouseEvent('mousemove', 400, 300); - mouseEvent('mouseover', 400, 300); + _hover(400, 300); + + assertLabel( + ['Solid', 'Incoming flow count: 4', 'Outgoing flow count: 3', '447TWh'], + ['rgb(148, 103, 189)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'] + ); + }) + .then(function() { + _hover(450, 300); + + assertLabel( + ['Source: Solid', 'Target: Industry', '46TWh'], + ['rgb(0, 0, 96)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'] + ); + + return Plotly.relayout(gd, 'hoverlabel.font.family', 'Roboto'); + }) + .then(function() { + _hover(400, 300); + + assertLabel( + ['Solid', 'Incoming flow count: 4', 'Outgoing flow count: 3', '447TWh'], + ['rgb(148, 103, 189)', 'rgb(255, 255, 255)', 13, 'Roboto', 'rgb(255, 255, 255)'] + ); }) - .then(wait) .then(function() { - expect(d3.select('.hoverlayer>.hovertext>text').node().innerHTML) - .toEqual('447TWh', 'tooltip present'); + _hover(450, 300); + + assertLabel( + ['Source: Solid', 'Target: Industry', '46TWh'], + ['rgb(0, 0, 96)', 'rgb(255, 255, 255)', 13, 'Roboto', 'rgb(255, 255, 255)'] + ); + + return Plotly.restyle(gd, { + 'hoverlabel.bgcolor': 'red', + 'hoverlabel.bordercolor': 'blue', + 'hoverlabel.font.size': 20, + 'hoverlabel.font.color': 'black' + }); }) .then(function() { - mouseEvent('mousemove', 450, 300); - mouseEvent('mouseover', 450, 300); + _hover(400, 300); + + assertLabel( + ['Solid', 'Incoming flow count: 4', 'Outgoing flow count: 3', '447TWh'], + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 20, 'Roboto', 'rgb(0, 0, 0)'] + ); + }) + .then(function() { + _hover(450, 300); + + assertLabel( + ['Source: Solid', 'Target: Industry', '46TWh'], + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 20, 'Roboto', 'rgb(0, 0, 0)'] + ); }) + .catch(fail) .then(done); }); });