Skip to content

Vertical or/and horizontal line that is always shown in any hovermode #2155

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

Closed
apalchys opened this issue Nov 13, 2017 · 39 comments
Closed

Vertical or/and horizontal line that is always shown in any hovermode #2155

apalchys opened this issue Nov 13, 2017 · 39 comments
Labels
feature something new

Comments

@apalchys
Copy link
Contributor

apalchys commented Nov 13, 2017

It would be nice to have an option in plotly.js to toggle on a vertical or/and horizontal line across the entire plot area on hover.

The behavior of the line should be similar to the behavior of the spikes feature, but there are several differences:

  • The line shouldn't depend on hovermode (as spikes do at the moment), moreover it can be used in 'x' or 'y' hovermode to compare data on hover.
  • This line should be always drawn over the nearest x/y point, regardless of whether this point is within the max hover distance or not.

This feature could be very useful for financial and stock charts

Related to #2026, #1959

PR #2150

@jackparmer
Copy link
Contributor

jackparmer commented Nov 16, 2017

This looks very useful @apalchys !

To make sure I'm understanding the PR correctly, this new hover mode would be enabled by setting:

layout.xaxis.showcrossline and/or layout.yaxis.showcrossline to true?

The crosslinecolor, crosslinethickness, and crosslinedash could also be optionally set in the axis object for styling?

@chriddyp and @cpsievert - Any opinions on this feature from a Dash and Shiny perspective?

In particular, from the PR description #2150

  • crosslines do not depend on the selected hovermode
  • crosslines are always drawn over the nearest x|y point, regardless of whether this point is within the max hover distance or not
  • crosslines are always drawn across the entire plot area

Thoughts @alexcjohnson ?

image

Seems like a reasonable addition to me.

Just curious @apalchys - in which financial software have you seen this feature? Looks like Highcharts has a similar hover line on the x-axis for example: https://www.highcharts.com/stock/demo/lazy-loading

image

@chriddyp
Copy link
Member

chriddyp commented Nov 16, 2017

This feature has already been requested a couple times by the dash community:

There is this built-in Plotly "Toggle Spike Lines" function in each Graph, that does nearly what I want and is pretty fast, but not customizable.

For the api, does it make sense to match the existing shapes API for lines (https://plot.ly/python/reference/#layout-shapes-line)? that is, instead of The crosslinecolor, crosslinethickness, and crosslinedash, just:

crossline: {
    color: ...,
    width: ...,
    dash: ...
}

Note that this is pretty similar to spikemode: 'across'

@alexcjohnson
Copy link
Collaborator

The behavior of the line should be similar to the behavior of the spikes feature, but there are several differences:

It's similar enough that we should really try to extend the spikes feature rather than add a new competing one.

The line shouldn't depend on hovermode (as spikes do at the moment), moreover it can be used in 'x' or 'y' hovermode to compare data on hover.

TBH I'm not sure it's desirable for spikes to depend on hovermode, the current behavior is probably just what was most convenient to implement. I guess there's a question of what to do with horizontal spikes when you're comparing multiple points at the same x value (my gut reaction: show only one, the closest to the pointer), and we currently can't show spikes if hovermode=false because they're calculated after the hover data is determined, but that we can get around. So lets just define the behavior we want and make spikes do that.

This line should be always drawn over the nearest x/y point, regardless of whether this point is within the max hover distance or not.

Do you mean this to be linked to hover behavior (ie there's no limit to max hover distance as far as hover data AND spikes) or do you mean spikes should show up all the time (based on the nearest point) but hover data should only show up when you're within our predefined max distance? The former seems clearer to me (and notice that it would encompass spikes-only mode, if used with hovermode=false) and also very easy to implement. But if there's a good reason we can do the latter instead. Seems like we should have a layout parameter like hoverdistance the defaults to the current range but can be set to any other value (including perhaps 0 to mean no cutoff). Then if the latter behavior is necessary we could make a spikedistance as well, which normally inherits from hoverdistance. Note that these are top-level layout attributes, not per-axis attributes, as in the general case that wouldn't make any sense.

For the api, does it make sense to match the existing shapes API for lines

In retrospect we probably should have put spike styling into a sub-container, but at this point I don't think we want to change the existing spike styling API until v2.0

@jackparmer
Copy link
Contributor

If interested in another comparison it looks amcharts has a version of this feature:

https://www.amcharts.com/demos/candlestick-chart/

image

@alexcjohnson
Copy link
Collaborator

it looks amcharts has a version of this feature

Interesting... per one of the pieces of #2026:

Allow hovering on arbitrary points on the plot, irrespective of whether there is data there or not.

In the amcharts version, the vertical spike is pinned to the data point while the horizontal is pinned to the cursor, even though the hover label is pinned to the data - to support that, we'd need spike positioning to be a per-axis configuration independent of hover label positioning.

@kratzert
Copy link

I personally would love to have a feature like this implemented. @chriddyp already explained in #1847 the question I raised in the dash forum. Since it seems to be a general brainstorming here again, I would like to throw in another extension of this feature that I really would appreciate: It would be wonderful if this hovering line could be extended to reach over multiple stacked subplots. Here you can see an example.
Here an example for what that might be useful. I e.g. have a model, that outputs a timeseries (e.g. of river discharge) that I want to plot against observed discharge. But then in subplots below, I would like to plot e.g. the state of the model internal snow storage, soil moisture storage etc. These usually are plotted in separate plots. But it would be very useful to have on hovering indicator line, that spans all these vertically stacked subplots and shows at each cross-section with on of the timeseries the value for this particular x-axis position.

I didn't take a into-depth look to this PR but I guess it's limited to one plot? Since this seems to be a more general discussion of where to go with this feature, I would really like to see this possibility supported as well.

@alexcjohnson
Copy link
Collaborator

@kratzert spikemode: 'across' already works as you suggest for stacked coupled subplots.

screen shot 2017-11-16 at 8 32 46 am

@alexcjohnson
Copy link
Collaborator

@kratzert the second part of your question (labels across all subplots) is the subject of a separate issue #2114

@apalchys
Copy link
Contributor Author

apalchys commented Nov 16, 2017

@alexcjohnson

It's similar enough that we should really try to extend the spikes feature rather than add a new competing one.

It sounds reasonable, I think that we can do it this way and extend the existing spikes functionality by making it work in any hovermode (except for false one, of course)

Do you mean this to be linked to hover behavior (ie there's no limit to max hover distance as far as hover data AND spikes) or do you mean spikes should show up all the time (based on the nearest point) but hover data should only show up when you're within our predefined max distance? The former seems clearer to me (and notice that it would encompass spikes-only mode, if used with hovermode=false) and also very easy to implement.

At the moment, the second one is implemented. Crosslines are displayed all the time (based on the nearest point), but the hover data is displayed only when you're within predefined max distance (this is certainly discussable)

Seems like we should have a layout parameter like hoverdistance the defaults to the current range but can be set to any other value (including perhaps 0 to mean no cutoff). Then if the latter behavior is necessary we could make a spikedistance as well, which normally inherits from hoverdistance. Note that these are top-level layout attributes, not per-axis attributes, as in the general case that wouldn't make any sense.

The idea of hoverdistance and spikedistance parameters is definitely great and looks like a solution to this dilemma!

@apalchys
Copy link
Contributor Author

@jackparmer

To make sure I'm understanding the PR correctly, this new hover mode would be enabled by setting

Yes, you understood this correctly, in the current implementation this feature can be switched on by setting the corresponding axis showcrossline attribute to true (for example, layout.yaxis.showcrossline), and then further optionally customized by crosslinecolor, crosslinethickness and crosslinedash attributes.

@apalchys
Copy link
Contributor Author

@alexcjohnson

In the amcharts version, the vertical spike is pinned to the data point while the horizontal is pinned to the cursor, even though the hover label is pinned to the data - to support that, we'd need spike positioning to be a per-axis configuration independent of hover label positioning.

We can use one more parameter, for example mouseattached, to handle this type of behavior.
The default value will be false, and the spikes will be attached to the nearest datapoint, and on true spikes will be attached to the current mouse position. What's your thoughts?

@apalchys
Copy link
Contributor Author

@jackparmer

in which financial software have you seen this feature? Looks like Highcharts has a similar hover line on the x-axis for example

Yes, highcharts/amcharts has this feature. Also many other less popular libraries provides this feature: e.g. http://techanjs.org or https://api.taucharts.com/basic/line.html

From my experience, users from financial area likes to have a vertical line for analyzing line/stock charts.

@etpinard
Copy link
Contributor

I second @alexcjohnson 's remark that we should try as much as possible to extend the existing spike attributes as opposed to creating branch new attribute containers. Yes, we should've put the spike attributes under a container e.g. spike.line.color instead of spikecolor, but that must wait for v2.

To enable the proposed behavior, we could add addition spikemode, but that attribute is already pretty crowded. Maybe a new attributes e.g. spikesnap: 'data' | 'cursor' would be best.

TBH I'm not sure it's desirable for spikes to depend on hovermode

For backward-compatibility, we could add layout.hovermode: 'spikes'.

@apalchys
Copy link
Contributor Author

apalchys commented Dec 6, 2017

I added support for spikedistance, hoverdistance, spikesnap and moved functionality to the spikes feature in my branch:
apalchys@253cdc7

Could someone please review it before I open PR?

@etpinard
Copy link
Contributor

etpinard commented Dec 6, 2017

Hmm. Before I dig deeper into your patch. Looks like the default spikes behavior has changed.

Master: http://rickyreusser.com/plotly-mock-viewer/#stacked_coupled_subplots

peek 2017-12-06 11-42

your branch master...apalchys:crossline

peek 2017-12-06 11-43

we can't allow that to happen.

@etpinard
Copy link
Contributor

etpinard commented Dec 6, 2017

Oh wait, I see why this is happening. You remove the fancy modebar logic that pinned hovermode to 'closest' when toggling on the spikes. Nice 👌

Off your branch with hovermode: 'closest', we get back the current default behavior.

peek 2017-12-06 12-10

This functionality should be covered already by axis.spikemode

@apalchys
Copy link
Contributor Author

apalchys commented Dec 7, 2017

This functionality should be covered already by axis.spikemode

New functionality allows to work with spikes in the "compare" mode when there are several traces on the same plot and it does not depend on hoverData when drawing the spikelines.

Just to summarize:
In the current implementation, we take the first point from hoverData points for drawing spikes, and in the new implementation we specifically look for two separate points for drawing horizontal and vertical lines.
In the "compare" mode, spikes are drawn across all the chart, since there may be several points on the horizontal / vertical line.

Also now we have the ability to stick the spikeline to the cursor, not to the data points, and to set the searching distance for points for drawing the hoverlabels and spikelines.

Here are some examples of new functionality:
Demo: https://codepen.io/plotly-demo/pen/NwZKav

increased hoverdistance decreased spikedistance
spikesnap cursor
work sample default props

@etpinard
Copy link
Contributor

etpinard commented Dec 7, 2017

New functionality allows to work with spikes in the "compare" mode when there are several traces on the same plot and it does not depend on hoverData when drawing the spikelines.

Ok. Then why does this new functionality depend on hovermode?

@apalchys
Copy link
Contributor Author

apalchys commented Dec 7, 2017

@etpinard are you asking why the feature depends on hovermode but doesn't use hoverData?
If so, when hovermode is compare, hoverData has info about the points with the same closest X (or Y) but there is no information about the closest Y (or X) points. As result, we can't draw the second spikeline.
I decided not to change hoverData and implemented the separate logic for looking for the closest X and Y points.

@jackparmer
Copy link
Contributor

@deechiw
Copy link

deechiw commented Dec 8, 2017

I like the vertical line aspect, but then cant we turn the horizontal line off. As the cursor moves horizontally the yvalues are reflected on the plots. The horizontal line to a certain extent is not giving out any information. My other question is can this be extended to subplots as mentioned in Hover labels across shared axes.

@apalchys
Copy link
Contributor Author

apalchys commented Dec 8, 2017

@deechiw
In my branch, you can manually control the behavior of spikelines by editing corresponding properties in the config. In your example, you can set the configuration as follows:

{
	"layout": {
		"spikedistance": 0,
		"xaxis": {
			"showspikes": true,
			...
		},
		...
	},
	...
}

By doing this, you turn on the vertical spikeline (spikeline to the shared xaxis) and set the range for drawing it to Infinity. And in the compare mode, it will be drawn by default across all subplots.

You can also set the spikesnap attribute, as shown below, to bind a vertical line to the cursor, not to data points.

"xaxis": {
	...
	"spikesnap": "cursor",
	...
}

Below are examples of this spikelines behavior for such settings. (here is the demo: https://codepen.io/plotly-demo/pen/BmgGMq)

multiple subplots vline
multiple subplots vline cursor

As for the hover labels for other subplots, I agree with @alexcjohnson on his comment #2155 (comment):

labels across all subplots is the subject of a separate issue #2114

@etpinard
Copy link
Contributor

etpinard commented Dec 8, 2017

@apalchys

New functionality allows to work with spikes in the "compare" mode when there are several traces on the same plot and it does not depend on hoverData when drawing the spikelines.

That's great. That's for fixing that.

In the current implementation, we take the first point from hoverData points for drawing spikes, and in the new implementation we specifically look for two separate points for drawing horizontal and vertical lines. In the "compare" mode, spikes are drawn across all the chart, since there may be several points on the horizontal / vertical line.

I'm confused here. Now that hover and spike data are independent, why would the behavior depend on layout.hovermode? Furthermore, how is the new behavior different from what the current spikemode attribute allows?

Also now we have the ability to stick the spikeline to the cursor, not to the data points, and to set the searching distance for points for drawing the hoverlabels and spikelines.

That's very nice. Thanks for adding this 👌

@apalchys
Copy link
Contributor Author

Now that hover and spike data are independent, why would the behavior depend on layout.hovermode?

TL;DR
Spike data and hover data are independent but I need to check hovermode in order to enable different spikeline points calculation logic:

  • compare mode: looking for two nearest points - one with the nearest x coordinate and one with y
  • closest mode: search for the absolutely nearest point twice (for x and y spike points).

Long explanation
I cannot say, my implementation strongly depends on the hovermode, it's more a question of the correct way to update the existing functionality.

At the moment, spikelines are used in the toaxis mode by default and can only be used in the closest hovermode. In this mode, the same point is nearest to x and y coordinates.

In compare hovermode (either x or y), we can have several points with the same nearest x and several points with the same nearest y coordinates.

Therefore, we simply cannot use the toaxis mode, because we cannot select just one point for drawing both spikelines, all points with the nearest x or y must be covered by the corresponding spikelines.
That's why I used across spikelines mode as default and only available mode while we use compare hovermode. At the same time, I decided not to change the current behavior of the spikelines in the closest hovermode and leave the spikeline toaxis mode as default one.

Furthermore, how is the new behavior different from what the current spikemode attribute allows?

The answer to this question is closely related to the previous one.
The key point of the new implementation is a separate process of finding points for drawing horizontal and vertical spikelines to allow it to work in any hovermode.
In the previous implementation, spikelines worked only in the closest hovermode, because the closest datapoint was always the nearest x and y point, and since this was the first point in the hoverData array, it was very easy to take it from there.

But, as I already mentioned, in compare hovermode, we might not have the corresponding x and y points.

@alexcjohnson
Copy link
Collaborator

@apalchys I think this is making it too complicated, and somewhere in there it will lead to undesirable behavior. I haven't looked at it in enough detail to know where, though it seems like a) you've brought a bunch of scatter/hover functionality into fx/hover, that really needs to be left to the individual trace modules; and b) seems like you've added two new loops through the data, one for each axis - I think we can get away with 1 or in some cases 0 (see below).

I think as far as spikelines are concerned, we SHOULD just select one point for drawing both spikelines, and do this process completely independently from selecting hover data. Hover data makes one loop through the data, using hovermode and hoverdistance. Then spikelines, when spikesnap='data' result in a second loop through the data, using a "hovermode" of closest and a limit of spikedistance.

I suppose when hovermode='closest' and either hoverdistance === spikedistance, hoverdistance < spikedistance and we found a point, or hoverdistance > spikedistance and we DIDN'T find a point, we could short-circuit the second loop and just use the results of the first loop. Might be a nice optimization but I'd make sure the behavior is correct and well-tested before getting into details like that.

@apalchys
Copy link
Contributor Author

we SHOULD just select one point for drawing both spikelines

To make sure we are on the same page:

I need to change code to use one point for drawing spikelines in 'compare' mode but anyway it will require to find 2 data points, right?
Please see visualizations below:

'closest' mode:
Looking for the nearest data point:
closest mode point distance

'compare' mode:
Looking for data point (1st) with the x coordinate nearest to the x coordinate of the cursor and data point (2nd) with the y coordinate closest to the y coordinate of the cursor. After that we calculate the final point and draw 2 spikelines.
compare mode points distance

Do I understand it correctly?

@alexcjohnson
Copy link
Collaborator

@apalchys neat effect with the grey and red/green lines, really makes it easy to see what's going on!

I think spikelines should always behave like what you have for closest mode, regardless of hovermode. It seems weird to have the x and y spikes point to different points, so their crossing point is not a data point.

This will mean sometimes spikelines will appear or disappear independently of the hover labels - sometimes we'll have only hover labels, sometimes only spike lines - but especially with the distinction between spikedistance and hoverdistance that kind of decoupling is inevitable.

@apalchys
Copy link
Contributor Author

I think spikelines should always behave like what you have for closest mode, regardless of hovermode. It seems weird to have the x and y spikes point to different points, so their crossing point is not a data point.

Sorry if I look annoying, but could you please confirm that the following behaviour is expected?
Legend:

  • Green circle - spikedistance
  • Gray rectangle - hoverdistance
  • Green line - the nearest data point.

closest spikes work in compare mode

I am asking because I would expect hover labels and spikelines are drawn for the same points. (spikedistance and hoverdistance are the same)
correct work in compare mode

For my case, the main idea of using spikelines in the 'compare' mode is to allow users to compare data they hovered to, sometimes even without any labels.
Spikelines marks the closest axis points that contain data on it, rather than the nearest data point.

Summarizing all the facts and comments, what do you think about an alternative idea:

Make spikesnap completely responsible for the behavior of the spikes with 'closest' (default), 'compare' and 'cursor' values.

In this case, the spikelines functionality will be completely independent of hovermode, and will be easily customizable for the desired behavior.

@alexcjohnson
Copy link
Collaborator

Sorry if I look annoying

Not at all! It's really important that we get the goals right before going any further.

I would expect hover labels and spikelines are drawn for the same points.

Yes, that's certainly desirable! I think we're pretty close though in your first gif above (with the green circle and grey rectangle). You really have to go looking for situations where the spikes and hover labels disagree. What about a small tweak to the behavior to cover that case:

  • Hover points get calculated first, then if spikesnap='data':
  • If we FOUND hover points, restrict the spikes to only choosing among those points (so if none of those points is close enough, no spikes are drawn even though another, non-hovered, point may be close enough)
  • If we DID NOT FIND hover points, spikes can choose any point (always in "closest" mode).

Note that this way there is still no explicit coupling between hovermode and spike mode, just a coupling between the points chosen by each feature.

@ghost
Copy link

ghost commented Dec 19, 2017

Hi, such a vertical assist line would be very helpful.
Here are my illustration:

image

Currently, the line enabled by showspikes = TRUE & spikes = across can only show in one chart, which would be useless while illustrating the data in reality.

@apalchys
Copy link
Contributor Author

@alexcjohnson Thank you for your ideas, I've reworked my code a bit according to them.

Now it works like this:

  • We scan all the curves in the data to find points for hovering (using the current hovermode).
  • If there are points in the hoverData, we check its content for a point that is close enough to the mouse (using the absolute distance, as in the closest mode), and draw the spiklines to it, if any.
  • If there are no points in the hoverData, we try to find the nearest point among all points (again using the absolute distance, as in the closest mode, regardless of the current hovermode) and draw spikelines to it, if it exists.

apalchys@8a2194f

What do you think about this behavior? Did I understand you correctly?

@apalchys
Copy link
Contributor Author

apalchys commented Jan 5, 2018

I am going to create a PR with my latest changes mentioned in #2155 (comment). Does it make sense?

@etpinard
Copy link
Contributor

etpinard commented Jan 8, 2018

I am going to create a PR

I'd say: go for it!

@apalchys
Copy link
Contributor Author

PR has been merged. Closing the issue.

@JohnCos247
Copy link

Thanks for getting in this PR it has been super helpful. For anyone looking to achieve this functionality here is the layout that I used. I overlooked @apalchys comment a few times before I realized his codepen link. Hope this helps someone in the future :).

"layout": {
    "spikedistance": 200,
    "hoverdistance": 10,
        "xaxis": {
            "showspikes": true,
            "spikemode": "across",
            "spikedash": "solid",
            "spikecolor": "#000000",
            "spikethickness": 2
        }
}

@sleighsoft
Copy link
Contributor

@apalchys How did you achieve this? What settings are required for it?

33763390-60f22082-dc21-11e7-972c-038455bcff90

@lvmajor
Copy link

lvmajor commented Oct 1, 2020

Wondering the same :) I'm also trying to find out how to add highlighted zones in the background (as of now I only found shapes which I could use to do it... but I'm not sure it is the intended use of shapes...)

@droid192
Copy link

@sleighsoft try this:


import numpy as np
import plotly.graph_objects as pl_go
import plotly.express as px

slug = np.random.random_sample((200,))

fig = pl_go.Figure()
fig.add_trace(  # DYNAMIC
    pl_go.Scatter(x=slug[:100],
                  y=slug[100:], 
                  name='trace1_y1',
#                   line_width=0.9,
                  #                marker_line_width=0,
                                 marker_color='green', # or array,
#                 hovertemplate=None,
#                   hoverinfo='none',
                  mode='lines',
                  )
)

fig.add_hline(y=1,
              line_width=0.5,
              line_dash="dot",
              line_color="cyan",
              annotation_text="1BASE",
              annotation_font_size=12,
              annotation_position="bottom right")
fig.update_layout(
    xaxis_zeroline=False, 
    yaxis_zeroline=False,
    yaxis=dict(
        tickfont=dict(size=14, color='#e6e6e6'),
        #         title='Quote',
        #         titlefont_size=16,
        gridcolor='#283442',
        linecolor='#283442',
        spikecolor="white", spikethickness=1, spikedash='solid', spikemode='across',
        spikesnap='cursor'
    ),
    spikedistance=-1,
#     hoverdistance=0,
    hovermode='y',
    font=dict(
        color='white',
        size=15
    ),
    paper_bgcolor='rgb(17,17,17)',
    plot_bgcolor='rgb(17,17,17)',
)
fig.show()  

image

what bugs me is: how to disable the hover box on the chart; and always show the outer axis box with the coordinate of the cursor (not data point)?

@GF-Huang
Copy link

How to easy use crossline in python?

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

No branches or pull requests