Black Lives Matter. Please consider donating to Black Girls Code today.

Plotly/Dash — range filter resets on hoverData

I am trying capture mouse hover events using Dash. I capture the position of the mouse using hoverData. Later, I will use hoverData property to draw a circle over the time series where my mouse is pointing at.

The problem appears when I filter the time series using the range selector or the range slider. The plot correctly reduces to the selected time, but when I hover it with the mouse it resets to the main view (whole main series).

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv")

app.layout = html.Div([
    dcc.Graph(
        id='stock-plot'
    ),
], className="container")

@app.callback(
    Output('stock-plot', 'figure'),
    [Input('stock-plot', 'hoverData')])
def drawStockPrice(hoverData):
    traces = [go.Scatter(
                    x=df.Date,
                    y=df['AAPL.High'],
                    mode='lines',
                    opacity=0.7,
                    connectgaps=True),
            ]
    return {'data': traces,
            'layout': go.Layout(colorway=["#5E0DAC", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'],
                                          height=600,
                                          title=f"Closing prices",
                                          xaxis={"title": "Date",
                                                 'rangeselector': {'buttons': list([{'count': 1, 'label': '1M',
                                                                                     'step': 'month',
                                                                                     'stepmode': 'backward'},
                                                                                    {'count': 6, 'label': '6M',
                                                                                     'step': 'month',
                                                                                     'stepmode': 'backward'},
                                                                                    {'step': 'all'}])},
                                                 'rangeslider': {'visible': True}, 'type': 'date'},
                                          yaxis={"title": "Price (USD)"},
    )}

if __name__ == '__main__':
    app.run_server(debug=True)```

Hi @ellesse, moving the slider triggers a relayoutData event (see https://dash.plot.ly/interactive-graphing), which you need to capture to save the xaxis.range. See the code below! I added a div to show the relayoutData but of course you’ll want to remove it in your app.

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
from dash.dependencies import Input, Output, State
import json

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv")

app.layout = html.Div([
    dcc.Graph(
        id='stock-plot'
    ),
    html.Div(id='div'),
    dcc.Store(id='store'),
], className="container")

@app.callback(
    [Output('div', 'children'),
     Output('store', 'data')],
    [Input('stock-plot', 'relayoutData')])
def drawStockPrice(relayoutData):
    if relayoutData is not None and relayoutData.get("xaxis.range"):
        return json.dumps(relayoutData), relayoutData["xaxis.range"]
    else:
        return dash.no_update, dash.no_update


@app.callback(
    Output('stock-plot', 'figure'),
    [Input('stock-plot', 'hoverData')],
    [State('store', 'data')])
def drawStockPrice(hoverData, range_data):
    traces = [go.Scatter(
                    x=df.Date,
                    y=df['AAPL.High'],
                    mode='lines',
                    opacity=0.7,
                    connectgaps=True),
            ]
    return {'data': traces,
            'layout': go.Layout(colorway=["#5E0DAC", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'],
                                height=600,
                                title=f"Closing prices",
                                xaxis={"title": "Date",
                                       'range': range_data,
                                       'rangeselector': {'buttons': list([{'count': 1, 'label': '1M',
                                                                                     'step': 'month',
                                                                                     'stepmode': 'backward'},
                                                                                    {'count': 6, 'label': '6M',
                                                                                     'step': 'month',
                                                                                     'stepmode': 'backward'},
                                                                                    {'step': 'all'}])},
                                                 'rangeslider': {'visible': True}, 'type': 'date'},
                                          yaxis={"title": "Price (USD)"},
    )}


if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

@Emmanuelle, thank you so much! Now I think I completely missed this State property. I was able to make it work also for the rangeselector! Thanks again.