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

support template string on hover #3126

Merged
merged 53 commits into from
Nov 15, 2018
Merged

support template string on hover #3126

merged 53 commits into from
Nov 15, 2018

Conversation

antoinerg
Copy link
Contributor

@antoinerg antoinerg commented Oct 18, 2018

Closes #3007

See this #3126 (comment) for understanding how to add support for hovertemplate to new traces.

However, there are several caveats:

1 - In the process of making that PR, I realized that there are a lot of computed values on hover that are not emitted in hover events. Because we are passing the hover event data to the template, only a limited subset of the total information is currently available. Enriching the event data for each trace is probably outside the scope of the current PR since it should benefit BOTH hovertemplate and plotly_hover events. However, it is something that can be done fairly easily and separately (some traces already have an event_data.js file in place where such code would go)!

2 - Some traces have many hover labels. For example, in box traces, there are six labels when hovering on a box. As of right now, the numerical value for each of those is assigned to variable y regardless of what it represents (q(1|3), min, max, etc.). We should certainly add a label variable alongside in order for template '%{label}: %{y}' to exactly reproduce the current hover labels while enabling number formatting. All traces with several different hover labels would have to implement this change.

In the future, to support different templates for each hover labels, we could either allow hovertemplate to be an object with a key for each label ({q1: 'templatestring', min: 'templatestring', ...}) OR we can move everything in nested attributes : {q1: {hovertemplate: '...'}, min: {hovertemplate: '...'}, ...}. The latter has the advantages of allowing further customization for each label (think color or hiding) since each of them could have a nested hoverinfo and hoverlabel. We need to think carefully about this one. The latter approach is what @alexcjohnson and I decided to do in our latest work for sankey traces (see PR #3096).

3 - Right now, hovertemplate doesn't control the content of the secondary labels. As @alexcjohnson pointed out, this could be supported by a special tag <extra>Secondary box content here</extra>.

Please chime in :)

@antoinerg antoinerg self-assigned this Oct 18, 2018
// hovertemplate
var trace = d.trace, hovertemplate = opts.hovertemplate || trace.hovertemplate || false;
if(hovertemplate) {
text = Lib.templateString(hovertemplate, gd._hoverdata[curveNumber], trace);
Copy link
Contributor Author

@antoinerg antoinerg Oct 18, 2018

Choose a reason for hiding this comment

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

We can pass additional objects as argument to Lib.templateString so the sky is the limit in terms of the number of values we can make available here!

However, as mentioned in point 1, we should probably put useful data into _hoverdata to begin with since it's emitted on hover.

@mojoaxel
Copy link
Contributor

@antoinerg You should consider adding a bunch of examples to plotly/documentation to demonstrate the new possibilities with this.

@etpinard
Copy link
Contributor

Enriching the event data for each trace is probably outside the scope of the current PR since it should benefit BOTH hovertemplate and plotly_hover events

Matching event data points keys with the upcoming hovertemplate %{} strings is a fantastic idea. The points keys are documented here. The list isn't complete, but garanting that keys emitted during hover/click events work in hovertemplate will be a big plus in terms of user-discoverability and maintainability.

We'll need to handle traces that support a hoveron attribute (e.g. scatter traces that can "hover-on" points and fills) which don't always emit the same event data keys (see #2504 for more info). Perhaps we could have nested hovertemplate for each case e.g. fills.hovertemplate and points.hovertemplate?


Some traces have many hover labels. For example, in box traces, there are six labels when hovering on a box.
In the future, to support different templates for each hover labels, we could either allow hovertemplate to be an object with a key for each label

Thanks writing down thoughts about traces that can generate multiple hover labels. I agree, we'll need to implement per-label templating and styling as some point.

In the case of box traces, I'm not a fan of the {q1: {hovertemplate: '...'}, min: {hovertemplate: '...'}, ...} proposal as it would conflict with #1059. I think it's important to make a distinction between cases where:

  • one part of a trace can generate multiple hover labels (e.g. 'boxes' hover) and
  • traces with multiple "parts" each with their own hover labels (e.g. sankey, but also parcats).

this could be supported by a special tag Secondary box content here

Maybe these special tags could help us out. Consider:

boxes.hovertemplate: `
  <mean><span style="color:blue">MEAN: %{y}</span><name>%{data.name}</name></mean>
  <q1>Q1: %{y}</q1>
  <q3>q3: %{y}</q3>
`

would show three labels: one for the mean, q1 and q3 when hovering on a box. The mean hover label would have the trace "name" attach to it in a secondary label and its text would drawn in blue.

Now, those <--> will conflict with other pseudo-html tags we support (e.g. <b> as b is an attribute for scatterternary traces), so we should use some other escape sequence. But all in all, this could be a nice way to support all the per-label customizations our users can think of.


Let me write down a few less-important comments here about the implementation:

  • What happens when template keys don't exist e.g. show this: %{cow}. How should this render out? e.g. show this: undefined or show this: or something else.
  • As hovertemplate overrides hoverinfo, we should not coerce hoverinfo if one provides a hovertemplate.
  • What should %{y} in a template w/o : format mean. Does it mean: print the y event data value using the default plotly formatting or print the raw y value?
  • At some point, we should implement hoverinfo as a arrayOk attribute to allow user to set different templates for each point.
  • To close out configurable hoverinfo-x placement #3145, maybe we should add layout.xaxis.hovertemplate and layout.yaxis.hovertemplate

@nicolaskruchten
Copy link
Contributor

nicolaskruchten commented Nov 1, 2018

I'm curious to talk about the interaction between hoverinfo and hovertemplate. I was thinking that hovertemplate could accept %{hoverinfo} key and then that could be the default value, so we could always use the two together.

@alexcjohnson
Copy link
Collaborator

  • one part of a trace can generate multiple hover labels (e.g. 'boxes' hover) and

The box-like case (incl. violin, ohlc, candlestick) seems like the toughest nut to crack. There's also the issue of whether you make one or many labels, ie hoverlabel.split #2959. As mentioned this morning I think we should not enable hovertemplate for these types in this PR. I can see a variety of ways to go with the API but none of them would be affected by the API for the simple case of a single label for hovering on a single class of objects. Let's make a new issue to discuss this, including enabling/disabling individual labels, independent styling (not just of the font as in <mean><span style="color:blue"> but also the bgcolor etc that wouldn't work with the mega-template idea).

  • traces with multiple "parts" each with their own hover labels (e.g. sankey, but also parcats).

Again as I mentioned earlier, I like the plan of describing each class of hover label within its own container. The difficult case for this currently is scatter hoveron: 'fills', which currently seems to be broken for the simple case of just one trace anyway. In the short term we should just fix this to ignore hoverinfo and always display the trace name since that's the only thing it can display, longer term we should make a new container (fills?) and put its config in there, like we do for sankey.

@etpinard
Copy link
Contributor

etpinard commented Nov 1, 2018

In the short term we should just fix this to ignore hoverinfo and always display the trace name since that's the only thing it can display,

👍

longer term we should make a new container (fills?)

Good plan. Related: #420 (comment)

@etpinard
Copy link
Contributor

etpinard commented Nov 1, 2018

which currently seems to be broken for the simple case of just one trace anyway

now in -> #3203

@alexcjohnson
Copy link
Collaborator

  • What happens when template keys don't exist e.g. show this: %{cow}. How should this render out? e.g. show this: undefined or show this: or something else.

Is there ever a case where this is not simply a mistake the user will want to know about and fix? If not, I'd say leave it show this: %{cow} in the output and maybe Lib.warn about it.

  • As hovertemplate overrides hoverinfo, we should not coerce hoverinfo if one provides a hovertemplate.

👍 I do think there's a possibility of having hoverinfo create the default hovertemplate, but it's fairly complicated and connected to a bunch of other things like hovermode and the existence of other traces. Probably ignore this for now but perhaps explore it later.

  • What should %{y} in a template w/o : format mean. Does it mean: print the y event data value using the default plotly formatting or print the raw y value?

I like the idea of this being the default plotly formatting. That way we don't need to augment the d3 format spec at all. I don't see a benefit to raw.

  • At some point, we should implement hoverinfo as a arrayOk attribute to allow user to set different templates for each point.

👍

interesting... sure, perhaps in conjunction with the effort to have hoverinfo create hovertemplate?

@etpinard
Copy link
Contributor

etpinard commented Nov 1, 2018

Great - thanks @alexcjohnson I think all my concerns have been resolved (at least for this first iteration).

@antoinerg should be all set now 🎉

@etpinard etpinard added this to the v1.43.0 milestone Nov 5, 2018
'hovertemplate %{percent}'
);

return Plotly.restyle(gd, 'hovertemplate', '%{label}<extra></extra>');
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we add one arrayOk hovertemplate test for pies?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely! There it is 1e4bd33

@@ -59,6 +60,7 @@ module.exports = {

text: scatterAttrs.text,
hovertext: scatterAttrs.hovertext,
hovertemplate: hovertemplateAttrs(),
Copy link
Contributor

Choose a reason for hiding this comment

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

We should append the hovertemplate descriptions with a list of flags (i.e. event data keys) available.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On a related note: I like that for some traces, like pie, we test that event data has the correct keys:

it('should contain the correct fields', function() {
var hoverData,
unhoverData;
gd.on('plotly_hover', function(data) {
hoverData = data;
});
gd.on('plotly_unhover', function(data) {
unhoverData = data;
});
mouseEvent('mouseover', width / 2 - 7, height / 2 - 7);
mouseEvent('mouseout', width / 2 - 7, height / 2 - 7);
expect(hoverData.points.length).toEqual(1);
expect(unhoverData.points.length).toEqual(1);
var fields = [
'curveNumber', 'pointNumber', 'pointNumbers',
'data', 'fullData',
'label', 'color', 'value',
'i', 'v'
];
expect(Object.keys(hoverData.points[0]).sort()).toEqual(fields.sort());
expect(hoverData.points[0].pointNumber).toEqual(3);
expect(Object.keys(unhoverData.points[0]).sort()).toEqual(fields.sort());
expect(unhoverData.points[0].pointNumber).toEqual(3);
});

We could potentially use the same array of field names for the test and for the description making sure we have one source of truth 🤔. This improvement could be done in another PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could potentially use the same array of field names for the test and for the description making sure we have one source of truth

Yeah, good call. We usually put things like that in "constants" files like this one:

https://github.com/plotly/plotly.js/blob/master/src/traces/scatter/constants.js

This improvement could be done in another PR.

That's up to you!

Copy link
Contributor Author

@antoinerg antoinerg Nov 13, 2018

Choose a reason for hiding this comment

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

I created a new function to check that event data contains a given list of keys: https://github.com/plotly/plotly.js/pull/3126/files#diff-4ddfa80f2c61e112d5587c91a961ab47

If it looks OK, I will dry up the code by listing the extra keys of a given trace into its constants.js file and then use that list to generate the description of hovertemplate attribute 🎉 !

Let me know if that route looks fine to you.

@etpinard
Copy link
Contributor

Getting close, once:

are ✅ , I believe we'll be in 💃 land.

@@ -205,7 +205,7 @@ module.exports = {
].join(' ')
},
hovertemplate: hovertemplateAttrs({}, {
keys: ['marker.size', 'marker.color']
keys: ['marker.size']
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is marker.color gone? In fact, all arrayOk attributes should be part of this list. They get added to the event data via

/** Appends values inside array attributes corresponding to given point number
*
* @param {object} pointData : point data object (gets mutated here)
* @param {object} trace : full trace object
* @param {number|Array(number)} pointNumber : point number. May be a length-2 array
* [row, col] to dig into 2D arrays
*/
exports.appendArrayPointValue = function(pointData, trace, pointNumber) {
var arrayAttrs = trace._arrayAttrs;
if(!arrayAttrs) {
return;
}
for(var i = 0; i < arrayAttrs.length; i++) {
var astr = arrayAttrs[i];
var key = getPointKey(astr);
if(pointData[key] === undefined) {
var val = Lib.nestedProperty(trace, astr).get();
var pointVal = getPointData(val, pointNumber);
if(pointVal !== undefined) pointData[key] = pointVal;
}
}
};

Now, maybe we don't need to hardcode all the arrayOk attributes into keys here, maybe we could just mention that all "per-point" values are available for hovertemplate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

marker.color is gone because it is not emitted in the event data. It used to work because hovertemplateString would be called with the full trace object as one of its argument but I removed this in 43a4cd9 to make sure everything is on par with event data. Therefore, if we want marker.color, I need to add it to the event data for scatter.

Copy link
Contributor

Choose a reason for hiding this comment

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

marker.color is gone because it is not emitted in the event data

Try

Plotly.newPlot(gd, [{
  y: [1, 2, 3],
  marker: {color: [1, 2, 3]}
}])
gd.on('plotly_hover', console.log)

Copy link
Contributor Author

@antoinerg antoinerg Nov 14, 2018

Choose a reason for hiding this comment

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

It works if it's an array but not if it isn't? 🤔

Plotly.newPlot(gd, [{
  y: [1, 2, 3],
  marker: {color: 'blue'}
}])
gd.on('plotly_hover', console.log)

That's why the test was failing: the mock I was using doesn't specify marker.color as an array.

I think it should be available in both cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

t works if it's an array but not if it isn't?

yes, that's the expected behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok so I added it and now run the test with a mock that specifies marker size and color as arrays. And it now passes.

Now for extra points, I just need to add those extra event data keys in constants.js.

Copy link
Contributor Author

@antoinerg antoinerg Nov 14, 2018

Choose a reason for hiding this comment

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

Done in 7be804e

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 just realized I didn't read your comment properly. I am sorry about that @etpinard.

Commit 14ac99f should be more satisfactory. I now list the different marker attributes that are arrayOk except for the nested line and gradient. Should I add those as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

From that same #3126 (comment)

Now, maybe we don't need to hardcode all the arrayOk attributes into keys here,

I think it might be best to NOT hardcoded all arrayOk attributes into that keys list, as it will be hard to remember to append it everytime we add another arrayOk attribute. We should keep that keys list only for keys that have no corresponding schema attributes e.g binNumber in histogram traces, and we should mention that all arrayOk (aka per-point) attribute are available to hovertemplate in the components/fx/hovertemplate_attributes' description.

Sorry for not been clearer.

@etpinard
Copy link
Contributor

Beautifully done. 💃

@antoinerg antoinerg merged commit 3a32ac0 into master Nov 15, 2018
@antoinerg antoinerg deleted the 3007-hovertemplate branch November 15, 2018 18:06
@chriddyp
Copy link
Member

chriddyp commented Dec 5, 2018

Nice example of this new hovertemplate string (courtesy @nicolaskruchten )

hovertemplate:
       "<b>%{text}</b><br><br>" +
       "%{yaxis.title}: %{y:$,.0f}<br>" +
       "%{xaxis.title}: %{x:.0%}<br>" +
       "Number Employed: %{marker.size:,}"

Codepen: https://codepen.io/nicolaskruchten/pen/QJgbxo?editors=0010

image

@Braintelligence
Copy link

Maybe stupid question: Is it possible to style the hovertemplate more? Like having a table structure?

@etpinard
Copy link
Contributor

s it possible to style the hovertemplate more? Like having a table structure?

No. That'll be in #3010

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants