Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Array support for trace hoverinfo #1761

Merged
merged 10 commits into from
Jun 6, 2017
Merged

Array support for trace hoverinfo #1761

merged 10 commits into from
Jun 6, 2017

Conversation

etpinard
Copy link
Contributor

@etpinard etpinard commented Jun 5, 2017

resolves #1749

peek 2017-06-05 13-47

TODO:

  • implement for 3d, gl2d, pie - done in e8a9ddd

- make base hoverinfo `arrayOk: true`
- merge array hoverinfo in calcdata while
  coercing its flags
- look for per-point hoverinfo, fallback to trace hoverinfo
- add checks string (vs array) hoverinfo values
- which is now handled during defaults,
  where one-trace graphs have no `'name'` hoverinfo flag by default
- this fixes the set `hoverinfo: 'all'` + 1 trace graph hover labels,
  the name label is now properly displayed.
}

return function(val) {
return Lib.coerce({hoverinfo: val}, {}, attrs, 'hoverinfo', dflt);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This here coerces hoverinfo items with no valid flags to 'all' if gd.data.length > 1 or all flags but 'name' on single-trace graphs. For example,

hoveinfo: ['sda', null, 'hello+sup']

// assuming more than one trace,
// => gets coerced to
hoverinfo: ['all', 'all', 'all']

This is debatable I feel. Perhaps, hoverinfo items with no valid flags should get coerced to 'none' instead? This would be more in line with e.g. marker.color where

marker.color: ['red', null, 'blue']

// gets coerced to
marker.color: ['red', '#444' /* i.e. Color.defaultLine */, 'blue']

I'm currently leaning toward the second option, but I have no strong opinion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think as you have it is probably correct - an invalid entry gets the default behavior, which is 'all' or all but name. This could come in handy, in as far as the default is really what you want most of the time, for tweaking hoverinfo of just a few points by providing those in the array and leaving the rest of the array undefined or null.

- there's no 'c' array in scattercarpet
- which handles the 1-vs-multi-trace dflt logic for all
  trace types
- call it once (!) during defaults
- call it again during fx/calc to coerce arrayOk hoverinfo items
- pie trace array hoverlabel settings were broken before
  this patch when pie `trace.sort = true` (the default by the way)
  as this reorders the calcdata items which led to fx/calc filling
  hoverlabel field in the wrong calcdata items.
- to solve the problem, simply don't rely on calcdata for hoverlabel
  setting in pie traces (as we currently do for gl3d and gl2d).
- To do so, add Fx.castHoverinfo helper and use it during
  gl3d, gl2d and pie hover where
  hoverinfo (trace-wide and per-pt array) is extracted from fullData
fontFamily: pt.htf || hoverLabelOpts.font.family,
fontSize: pt.hts || hoverLabelOpts.font.size,
fontColor: pt.htc || hoverLabelOpts.font.color
color: Fx.castHoverOption(trace, pt.i, 'bgcolor') || pt.color,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I never realized that pie/calc.js sorts calcdata items meaning the fullData array order isn't preserved. That is, we can't fill in pie calcdata items with hoverlabel field in order. Luckily here, we can simply reuse the gl2d/gl3d machinery.

This commit fixes pie traces with array hoverlabel values.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This commit fixes pie traces with array hoverlabel values.

Good catch! Is this covered in the tests? If it is, it's not obvious to me which one...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this covered in the tests?

yes

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... which was previously broken.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Great - I saw that change, just couldn't tell why it tested this behavior - but a failure is a failure, works for me!

* @param {object} layoutOut : full layout object (require _dataLength ref)
* @return {any} : the coerced value
*/
exports.coerceHoverinfo = function(traceIn, traceOut, layoutOut) {
Copy link
Contributor Author

@etpinard etpinard Jun 6, 2017

Choose a reason for hiding this comment

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

I hope nobody minds this abstraction. The hoverinfo dflt is ✨ : special ✨ , so I think it deserves its own coerce function.

@@ -117,7 +117,7 @@ module.exports = {
textfont: scatterAttrs.textfont,
textposition: scatterAttrs.textposition,
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
flags: ['a', 'b', 'c', 'text', 'name']
flags: ['a', 'b', 'text', 'name']
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fyi @rreusser

- e.g. hoverinfo, hoverlabel.bgcolor etc ...

expect(out[0].x).toEqual([1, 2, 3]);
expect(out[0].y).toEqual([2, 3, 1]);
expect(out[0].hoverinfo).toEqual(['none', 'skip', 'all']);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before this patch, we had

out[0].hoverinfo 
// => ['x', 'y', 'text', 'name', 'none', 'skip', 'all']

@@ -156,6 +156,7 @@ exports.findArrayAttributes = function(trace) {
return stack.join('.');
}

exports.crawl(baseAttributes, callback);
Copy link
Collaborator

Choose a reason for hiding this comment

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

ha nice, that's a lot simpler than I was afraid it might be!

- previously fx attribute arrays with length less than the
  calcdata trace length were skipped. That is, they did not get
  filled with their default
- this patch here ensures that the defaults values are present
  in the calcdata so that we don't have to rely on fallbacks in the
  hover hot code path.
})
.then(function() {
expect(gd.calcdata[0].map(function(o) { return o.hi; })).toEqual(
['none', 'x+y+z+text', 'x+y+z+text'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before this commit, we had

gd.calcdata[0].map(o => o.hi)
// => // ['none']

and no fallbacks for hoverinfo in the hover routine. Since, the hoverinfo default value isn't trivial, I chose add a (another 😑 ) Lib function fillArray.

Lib.fillArray is very similar to Lib.mergeArray, but instead of looping over the min of cd[i].length and hoverinfo.length, it loops over cd[i].length allowing calcdata items to be filled in with defaults.

src/lib/index.js Outdated
@@ -350,19 +350,25 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) {
}
};

/** merge data array into calcdata items
lib.mergeArray = function(traceAttr, cd, cdAttr) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎎 with a description of the difference between mergeArray and fillArray? And perhaps some indication of why one would use merge vs fill? Makes me wonder whether any of our other existing merge usages should be fill...

mergeArray doesn't need fn?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🎎 with a description of the difference between mergeArray and fillArray?

Sure. 👍

Makes me wonder whether any of our other existing merge usages should be fill...

Good point. Many routines in Drawing have fallbacks in case a required calcdata field is missing. The fallback pattern works well when the fallback value is constant for all cases (e.g. missing items in a marker.color array get drawn as #444), but not so well when it is depends on other things (e.g. for hoverinfo here which depends on gd.data.length and trace.type).

That said, replacing all Lib.mergeArray calls with Lib.fillArray should not break anything. I could do that if you prefer. It might slow things down a little bit when e.g.

trace.x.length // => 1e5
trace.marker.color.length // => 2

but I'd call that an edge case.

mergeArray doesn't need fn?

Not at the moment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That said, replacing all Lib.mergeArray calls with Lib.fillArray should not break anything. I could do that if you prefer. It might slow things down a little bit when e.g.

I'm not worried about perf - yes, shorter attribute array is an edge case I'm comfortable neglecting. But it does change behavior, even if it's behavior we expect users not to be depending on. I'd leave it as is for the moment, for existing uses, and make an issue about it. We'll probably have to look at each attribute individually to see if we think the change is sufficiently unambiguous as an improvement that it's worth the minor incompatibility.

Copy link
Contributor Author

@etpinard etpinard Jun 6, 2017

Choose a reason for hiding this comment

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

🎎 done in 3c3fda4

src/lib/index.js Outdated
*
* @param {array} traceAttr : trace attribute
* @param {object} cd : calcdata trace
* @param {string} cdAttr : calcdata key
* @param {function} [fn] : optional function to apply to each array item
*/
lib.mergeArray = function(traceAttr, cd, cdAttr, fn) {
lib.fillArray = function(traceAttr, cd, cdAttr, fn) {
fn = fn || lib.identity;
Copy link
Collaborator

Choose a reason for hiding this comment

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

🐎 I wonder whether it would be worth avoiding fn entirely if it's not provided? Either
cd[i][cdAttr] = fn ? fn(traceAttr[i]) : traceAttr[i];
or even

if(fn) {
    for(i = 0; i < cd.length; i++) cd[i][cdAttr] = fn(traceAttr[i]);
}
else {
    for(i = 0; i < cd.length; i++) cd[i][cdAttr] = traceAttr[i];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That won't make that much of a difference (at least in Chrome 58)

image

https://gist.github.com/etpinard/4a5c7983a619efb4c98cb927529b9af6

Copy link
Collaborator

Choose a reason for hiding this comment

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

awesome, thanks for checking.

@etpinard
Copy link
Contributor Author

etpinard commented Jun 6, 2017

tagging this as reviewable

@alexcjohnson
Copy link
Collaborator

No more comments from me - looks great! 💃

@etpinard etpinard merged commit 9779c96 into master Jun 6, 2017
@etpinard etpinard deleted the arrayok-hoverinfo branch June 6, 2017 17:09
@etpinard etpinard mentioned this pull request Jun 27, 2017
etpinard added a commit that referenced this pull request Oct 3, 2017
... scattergeo, choropleth, scatterternary and scattermapbox

- these are all the trace type that rely on `extraText` built in
  their _module.hoverPoints method
- this commit should've been part of PR #1761
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement arrayOk behavior for trace 'hoverinfo'
2 participants