plotly / plotly.js

Open-source JavaScript charting library behind Plotly and Dash
https://plotly.com/javascript/
MIT License
16.92k stars 1.86k forks source link

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

Closed apalchys closed 6 years ago

apalchys commented 6 years ago

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:

This feature could be very useful for financial and stock charts

Related to #2026, #1959

PR #2150

jackparmer commented 6 years ago

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

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

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

image

alexcjohnson commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

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

alexcjohnson commented 6 years ago

@kratzert the second part of your question (labels across all subplots) is the subject of a separate issue https://github.com/plotly/plotly.js/issues/2114

apalchys commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

I added support for spikedistance, hoverdistance, spikesnap and moved functionality to the spikes feature in my branch: https://github.com/apalchys/plotly.js/commit/253cdc74a1809b9844a505f1d334e796acd62aaf

Could someone please review it before I open PR?

etpinard commented 6 years ago

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 https://github.com/plotly/plotly.js/compare/master...apalchys:crossline

peek 2017-12-06 11-43

we can't allow that to happen.

etpinard commented 6 years ago

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 :ok_hand:

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

Found another decent example of this feature in ChartIQ:

image

http://demo.chartiq.com/index.html?utm_campaign=Charting%20Library&utm_source=charting%20library%20product%20page&utm_medium=demo%20cta

deechiw commented 6 years ago

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 commented 6 years ago

@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 https://github.com/plotly/plotly.js/issues/2155#issuecomment-344924328:

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

etpinard commented 6 years ago

@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 :ok_hand:

apalchys commented 6 years ago

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:

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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:

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 commented 6 years ago

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:

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 commented 6 years ago

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 commented 6 years ago

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

Now it works like this:

https://github.com/apalchys/plotly.js/commit/8a2194f75c3fa671151cfba8f53db5fa50f5b210

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

apalchys commented 6 years ago

I am going to create a PR with my latest changes mentioned in https://github.com/plotly/plotly.js/issues/2155#issuecomment-353585476. Does it make sense?

etpinard commented 6 years ago

I am going to create a PR

I'd say: go for it!

apalchys commented 6 years ago

PR has been merged. Closing the issue.

JohnCos247 commented 5 years ago

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 commented 4 years ago

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

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

lvmajor commented 4 years ago

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 commented 3 years ago

@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 commented 2 years ago

How to easy use crossline in python?