Skip to content

Commit 9779c96

Browse files
authored
Merge pull request #1761 from plotly/arrayok-hoverinfo
Array support for trace `hoverinfo`
2 parents bc0e06d + d1f03c0 commit 9779c96

26 files changed

+353
-104
lines changed

src/components/fx/calc.js

+25-9
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,41 @@ var Registry = require('../../registry');
1313

1414
module.exports = function calc(gd) {
1515
var calcdata = gd.calcdata;
16+
var fullLayout = gd._fullLayout;
17+
18+
function makeCoerceHoverInfo(trace) {
19+
return function(val) {
20+
return Lib.coerceHoverinfo({hoverinfo: val}, {_module: trace._module}, fullLayout);
21+
};
22+
}
1623

1724
for(var i = 0; i < calcdata.length; i++) {
1825
var cd = calcdata[i];
1926
var trace = cd[0].trace;
2027

21-
if(!trace.hoverlabel) continue;
28+
// don't include hover calc fields for pie traces
29+
// as calcdata items might be sorted by value and
30+
// won't match the data array order.
31+
if(Registry.traceIs(trace, 'pie')) continue;
32+
33+
var fillFn = Registry.traceIs(trace, '2dMap') ? paste : Lib.fillArray;
2234

23-
var mergeFn = Registry.traceIs(trace, '2dMap') ? paste : Lib.mergeArray;
35+
fillFn(trace.hoverinfo, cd, 'hi', makeCoerceHoverInfo(trace));
2436

25-
mergeFn(trace.hoverlabel.bgcolor, cd, 'hbg');
26-
mergeFn(trace.hoverlabel.bordercolor, cd, 'hbc');
27-
mergeFn(trace.hoverlabel.font.size, cd, 'hts');
28-
mergeFn(trace.hoverlabel.font.color, cd, 'htc');
29-
mergeFn(trace.hoverlabel.font.family, cd, 'htf');
37+
if(!trace.hoverlabel) continue;
38+
39+
fillFn(trace.hoverlabel.bgcolor, cd, 'hbg');
40+
fillFn(trace.hoverlabel.bordercolor, cd, 'hbc');
41+
fillFn(trace.hoverlabel.font.size, cd, 'hts');
42+
fillFn(trace.hoverlabel.font.color, cd, 'htc');
43+
fillFn(trace.hoverlabel.font.family, cd, 'htf');
3044
}
3145
};
3246

33-
function paste(traceAttr, cd, cdAttr) {
47+
function paste(traceAttr, cd, cdAttr, fn) {
48+
fn = fn || Lib.identity;
49+
3450
if(Array.isArray(traceAttr)) {
35-
cd[0][cdAttr] = traceAttr;
51+
cd[0][cdAttr] = fn(traceAttr);
3652
}
3753
}

src/components/fx/hover.js

+30-27
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,11 @@ function _hover(gd, evt, subplot) {
354354
trace: trace,
355355
xa: xaArray[subploti],
356356
ya: yaArray[subploti],
357-
name: (gd.data.length > 1 || trace.hoverinfo.indexOf('name') !== -1) ? trace.name : undefined,
358357
// point properties - override all of these
359358
index: false, // point index in trace - only used by plotly.js hoverdata consumers
360359
distance: Math.min(distance, constants.MAXDIST), // pixel distance or pseudo-distance
361360
color: Color.defaultLine, // trace color
361+
name: trace.name,
362362
x0: undefined,
363363
x1: undefined,
364364
y0: undefined,
@@ -558,7 +558,7 @@ function createHoverText(hoverData, opts) {
558558
// to have common labels
559559
var i, traceHoverinfo;
560560
for(i = 0; i < hoverData.length; i++) {
561-
traceHoverinfo = hoverData[i].trace.hoverinfo;
561+
traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo;
562562
var parts = traceHoverinfo.split('+');
563563
if(parts.indexOf('all') === -1 &&
564564
parts.indexOf(hovermode) === -1) {
@@ -724,7 +724,9 @@ function createHoverText(hoverData, opts) {
724724
else if(d.yLabel === undefined) text = d.xLabel;
725725
else text = '(' + d.xLabel + ', ' + d.yLabel + ')';
726726

727-
if(d.text && !Array.isArray(d.text)) text += (text ? '<br>' : '') + d.text;
727+
if(d.text && !Array.isArray(d.text)) {
728+
text += (text ? '<br>' : '') + d.text;
729+
}
728730

729731
// if 'text' is empty at this point,
730732
// put 'name' in main label and don't show secondary label
@@ -1056,6 +1058,30 @@ function cleanPoint(d, hovermode) {
10561058
var cd0 = d.cd[0];
10571059
var cd = d.cd[d.index] || {};
10581060

1061+
function fill(key, calcKey, traceKey) {
1062+
var val;
1063+
1064+
if(cd[calcKey]) {
1065+
val = cd[calcKey];
1066+
} else if(cd0[calcKey]) {
1067+
var arr = cd0[calcKey];
1068+
if(Array.isArray(arr) && Array.isArray(arr[d.index[0]])) {
1069+
val = arr[d.index[0]][d.index[1]];
1070+
}
1071+
} else {
1072+
val = Lib.nestedProperty(trace, traceKey).get();
1073+
}
1074+
1075+
if(val) d[key] = val;
1076+
}
1077+
1078+
fill('hoverinfo', 'hi', 'hoverinfo');
1079+
fill('color', 'hbg', 'hoverlabel.bgcolor');
1080+
fill('borderColor', 'hbc', 'hoverlabel.bordercolor');
1081+
fill('fontFamily', 'htf', 'hoverlabel.font.family');
1082+
fill('fontSize', 'hts', 'hoverlabel.font.size');
1083+
fill('fontColor', 'htc', 'hoverlabel.font.color');
1084+
10591085
d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
10601086

10611087
// then constrain all the positions to be on the plot
@@ -1123,7 +1149,7 @@ function cleanPoint(d, hovermode) {
11231149
if(hovermode === 'y') d.distance += 1;
11241150
}
11251151

1126-
var infomode = d.trace.hoverinfo;
1152+
var infomode = d.hoverinfo || d.trace.hoverinfo;
11271153
if(infomode !== 'all') {
11281154
infomode = infomode.split('+');
11291155
if(infomode.indexOf('x') === -1) d.xLabel = undefined;
@@ -1133,29 +1159,6 @@ function cleanPoint(d, hovermode) {
11331159
if(infomode.indexOf('name') === -1) d.name = undefined;
11341160
}
11351161

1136-
function fill(key, calcKey, traceKey) {
1137-
var val;
1138-
1139-
if(cd[calcKey]) {
1140-
val = cd[calcKey];
1141-
} else if(cd0[calcKey]) {
1142-
var arr = cd0[calcKey];
1143-
if(Array.isArray(arr) && Array.isArray(arr[d.index[0]])) {
1144-
val = arr[d.index[0]][d.index[1]];
1145-
}
1146-
} else {
1147-
val = Lib.nestedProperty(trace, traceKey).get();
1148-
}
1149-
1150-
if(val) d[key] = val;
1151-
}
1152-
1153-
fill('color', 'hbg', 'hoverlabel.bgcolor');
1154-
fill('borderColor', 'hbc', 'hoverlabel.bordercolor');
1155-
fill('fontFamily', 'htf', 'hoverlabel.font.family');
1156-
fill('fontSize', 'hts', 'hoverlabel.font.size');
1157-
fill('fontColor', 'htc', 'hoverlabel.font.color');
1158-
11591162
return d;
11601163
}
11611164

src/components/fx/index.js

+12-12
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ module.exports = {
3535
getDistanceFunction: helpers.getDistanceFunction,
3636
getClosest: helpers.getClosest,
3737
inbox: helpers.inbox,
38+
3839
castHoverOption: castHoverOption,
40+
castHoverinfo: castHoverinfo,
3941

4042
hover: require('./hover').hover,
4143
unhover: dragElement.unhover,
@@ -57,18 +59,16 @@ function loneUnhover(containerOrSelection) {
5759
selection.selectAll('.spikeline').remove();
5860
}
5961

60-
// Handler for trace-wide vs per-point hover label options
62+
// helpers for traces that use Fx.loneHover
63+
6164
function castHoverOption(trace, ptNumber, attr) {
62-
var labelOpts = trace.hoverlabel || {};
63-
var val = Lib.nestedProperty(labelOpts, attr).get();
64-
65-
if(Array.isArray(val)) {
66-
if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) {
67-
return val[ptNumber[0]][ptNumber[1]];
68-
} else {
69-
return val[ptNumber];
70-
}
71-
} else {
72-
return val;
65+
return Lib.castOption(trace, ptNumber, 'hoverlabel.' + attr);
66+
}
67+
68+
function castHoverinfo(trace, fullLayout, ptNumber) {
69+
function _coerce(val) {
70+
return Lib.coerceHoverinfo({hoverinfo: val}, {_module: trace._module}, fullLayout);
7371
}
72+
73+
return Lib.castOption(trace, ptNumber, 'hoverinfo', _coerce);
7474
}

src/lib/coerce.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
var isNumeric = require('fast-isnumeric');
1313
var tinycolor = require('tinycolor2');
1414

15+
var baseTraceAttrs = require('../plots/attributes');
1516
var getColorscale = require('../components/colorscale/get_scale');
1617
var colorscaleNames = Object.keys(require('../components/colorscale/scales'));
1718
var nestedProperty = require('./nested_property');
@@ -196,7 +197,7 @@ exports.valObjects = {
196197
'Values in `extras` cannot be combined.'
197198
].join(' '),
198199
requiredOpts: ['flags'],
199-
otherOpts: ['dflt', 'extras'],
200+
otherOpts: ['dflt', 'extras', 'arrayOk'],
200201
coerceFunction: function(v, propOut, dflt, opts) {
201202
if(typeof v !== 'string') {
202203
propOut.set(dflt);
@@ -338,6 +339,35 @@ exports.coerceFont = function(coerce, attr, dfltObj) {
338339
return out;
339340
};
340341

342+
/** Coerce shortcut for 'hoverinfo'
343+
* handling 1-vs-multi-trace dflt logic
344+
*
345+
* @param {object} traceIn : user trace object
346+
* @param {object} traceOut : full trace object (requires _module ref)
347+
* @param {object} layoutOut : full layout object (require _dataLength ref)
348+
* @return {any} : the coerced value
349+
*/
350+
exports.coerceHoverinfo = function(traceIn, traceOut, layoutOut) {
351+
var moduleAttrs = traceOut._module.attributes;
352+
var attrs = moduleAttrs.hoverinfo ?
353+
{hoverinfo: moduleAttrs.hoverinfo} :
354+
baseTraceAttrs;
355+
356+
var valObj = attrs.hoverinfo;
357+
var dflt;
358+
359+
if(layoutOut._dataLength === 1) {
360+
var flags = valObj.dflt === 'all' ?
361+
valObj.flags.slice() :
362+
valObj.dflt.split('+');
363+
364+
flags.splice(flags.indexOf('name'), 1);
365+
dflt = flags.join('+');
366+
}
367+
368+
return exports.coerce(traceIn, traceOut, attrs, 'hoverinfo', dflt);
369+
};
370+
341371
exports.validate = function(value, opts) {
342372
var valObject = exports.valObjects[opts.valType];
343373

src/lib/index.js

+55
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ lib.valObjects = coerceModule.valObjects;
3131
lib.coerce = coerceModule.coerce;
3232
lib.coerce2 = coerceModule.coerce2;
3333
lib.coerceFont = coerceModule.coerceFont;
34+
lib.coerceHoverinfo = coerceModule.coerceHoverinfo;
3435
lib.validate = coerceModule.validate;
3536

3637
var datesModule = require('./dates');
@@ -349,13 +350,67 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) {
349350
}
350351
};
351352

353+
/** merges calcdata field (given by cdAttr) with traceAttr values
354+
*
355+
* N.B. Loop over minimum of cd.length and traceAttr.length
356+
* i.e. it does not try to fill in beyond traceAttr.length-1
357+
*
358+
* @param {array} traceAttr : trace attribute
359+
* @param {object} cd : calcdata trace
360+
* @param {string} cdAttr : calcdata key
361+
*/
352362
lib.mergeArray = function(traceAttr, cd, cdAttr) {
353363
if(Array.isArray(traceAttr)) {
354364
var imax = Math.min(traceAttr.length, cd.length);
355365
for(var i = 0; i < imax; i++) cd[i][cdAttr] = traceAttr[i];
356366
}
357367
};
358368

369+
/** fills calcdata field (given by cdAttr) with traceAttr values
370+
* or function of traceAttr values (e.g. some fallback)
371+
*
372+
* N.B. Loops over all cd items.
373+
*
374+
* @param {array} traceAttr : trace attribute
375+
* @param {object} cd : calcdata trace
376+
* @param {string} cdAttr : calcdata key
377+
* @param {function} [fn] : optional function to apply to each array item
378+
*/
379+
lib.fillArray = function(traceAttr, cd, cdAttr, fn) {
380+
fn = fn || lib.identity;
381+
382+
if(Array.isArray(traceAttr)) {
383+
for(var i = 0; i < cd.length; i++) {
384+
cd[i][cdAttr] = fn(traceAttr[i]);
385+
}
386+
}
387+
};
388+
389+
/** Handler for trace-wide vs per-point options
390+
*
391+
* @param {object} trace : (full) trace object
392+
* @param {number} ptNumber : index of the point in question
393+
* @param {string} astr : attribute string
394+
* @param {function} [fn] : optional function to apply to each array item
395+
*
396+
* @return {any}
397+
*/
398+
lib.castOption = function(trace, ptNumber, astr, fn) {
399+
fn = fn || lib.identity;
400+
401+
var val = lib.nestedProperty(trace, astr).get();
402+
403+
if(Array.isArray(val)) {
404+
if(Array.isArray(ptNumber) && Array.isArray(val[ptNumber[0]])) {
405+
return fn(val[ptNumber[0]][ptNumber[1]]);
406+
} else {
407+
return fn(val[ptNumber]);
408+
}
409+
} else {
410+
return val;
411+
}
412+
};
413+
359414
/** Returns target as set by 'target' transform attribute
360415
*
361416
* @param {object} trace : full trace object

src/plot_api/helpers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ exports.swapXYData = function(trace) {
415415
Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']);
416416
}
417417
}
418-
if(trace.hoverinfo) {
418+
if(typeof trace.hoverinfo === 'string') {
419419
var hoverInfoParts = trace.hoverinfo.split('+');
420420
for(i = 0; i < hoverInfoParts.length; i++) {
421421
if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y';

src/plot_api/plot_schema.js

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ exports.findArrayAttributes = function(trace) {
156156
return stack.join('.');
157157
}
158158

159+
exports.crawl(baseAttributes, callback);
159160
exports.crawl(trace._module.attributes, callback);
160161

161162
if(trace.transforms) {

src/plots/attributes.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ module.exports = {
7474
role: 'info',
7575
flags: ['x', 'y', 'z', 'text', 'name'],
7676
extras: ['all', 'none', 'skip'],
77+
arrayOk: true,
7778
dflt: 'all',
7879
description: [
7980
'Determines which trace information appear on hover.',

src/plots/gl2d/scene2d.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,11 @@ proto.draw = function() {
623623
// also it's important to copy, otherwise data is lost by the time event data is read
624624
this.emitPointAction(nextSelection, 'plotly_hover');
625625

626-
var hoverinfo = selection.hoverinfo;
627-
if(hoverinfo !== 'all') {
626+
var trace = this.fullData[selection.trace.index] || {};
627+
var ptNumber = selection.pointIndex;
628+
var hoverinfo = Fx.castHoverinfo(trace, fullLayout, ptNumber);
629+
630+
if(hoverinfo && hoverinfo !== 'all') {
628631
var parts = hoverinfo.split('+');
629632
if(parts.indexOf('x') === -1) selection.traceCoord[0] = undefined;
630633
if(parts.indexOf('y') === -1) selection.traceCoord[1] = undefined;
@@ -633,9 +636,6 @@ proto.draw = function() {
633636
if(parts.indexOf('name') === -1) selection.name = undefined;
634637
}
635638

636-
var trace = this.fullData[selection.trace.index] || {};
637-
var ptNumber = selection.pointIndex;
638-
639639
Fx.loneHover({
640640
x: selection.screenCoord[0],
641641
y: selection.screenCoord[1],

src/plots/gl3d/scene.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ function render(scene) {
6767
if(lastPicked !== null) {
6868
var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate);
6969
trace = lastPicked.data;
70-
var hoverinfo = trace.hoverinfo;
7170
var ptNumber = selection.index;
71+
var hoverinfo = Fx.castHoverinfo(trace, scene.fullLayout, ptNumber);
7272

7373
var xVal = formatter('xaxis', selection.traceCoordinate[0]),
7474
yVal = formatter('yaxis', selection.traceCoordinate[1]),

0 commit comments

Comments
 (0)