pythonplotly-dashyaxisautoscalemode

Plotly-Dash stock app in python, with clientside callback (yaxis autoscale on xaxis zoom)


I'm creating a simple stock chart app in dash-plotly (python) whith an xaxis slider. When sliding the xaxis, I want the yaxis to dynamically rescale to the view. I think I have managed to get the callback function to trigger when sliding the xaxis scale through the 'relayoutData' hook. But instead of updating the yaxis the script throws errors. I'm not sure of the proper syntax to update layout from a callback function. Any ideas?

Here is my code so far. It runs, but yaxis is set at run time, and doesn't update.

Thanks a lot for any help =)

import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import numpy as np
import datetime

#some random values
a = datetime.datetime.today()
numdays = 100
dateList = []
for x in range (0, numdays):
    dateList.append(a - datetime.timedelta(days = x))
xy = [dateList,np.random.rand(100)]

app = dash.Dash()

app.title = 'Random'
dataS = go.Scatter(
        x = xy[0],
        y = xy[1],
        name = 'Rand',
        mode = 'lines'
    )
layoutS = go.Layout(
    title="Rand",
    xaxis=dict(
        rangeslider_visible=True,
        rangeselector=dict(
            buttons=list([
                dict(count=1, label="1m", step="month", stepmode="backward"),
                dict(count=6, label="6m", step="month", stepmode="backward"),
                dict(count=1, label="YTD", step="year", stepmode="todate"),
                dict(count=1, label="1y", step="year", stepmode="backward"),
                dict(count=5, label="5y", step="year", stepmode="backward"),
                dict(step="all")
            ])
        )
    )
)

app.layout = html.Div(
    html.Div([
        html.H1(children='Random nums'),
        html.Div(children='''
            Rand rand rand.
        '''),

        dcc.Graph(id='RandGraph', animate=True, figure=go.FigureWidget(data=dataS,layout=layoutS))
    ])
)

@app.callback(Output('RandGraph','figure'),[Input('RandGraph','relayoutData')])
def update_graph(relOut):
    layout = go.Layout(
        yaxis=dict(range=[min(y),max(y)])
    )
    return {'layout':layout}

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

Solution

  • Here is the result. It I tried a serverside implementation first, which ended up being really slow (I kept it for reference). The clientside impletation is a lot more responsive and faster. A joy to use. I just needed to learn a bit of javascript to make if work =) I sure was missing pandas when I was trying to do the array filtering!

    Here is the working code. Enjoy!

    import dash
    from dash.dependencies import Output, Input, State
    import dash_core_components as dcc
    import dash_html_components as html
    import plotly.graph_objs as go
    import numpy as np
    import pandas as pd
    import datetime
    
    #some random values
    a = datetime.datetime.today()
    numdays = 100
    dateList = []
    for x in range (0, numdays):
        dateList.append(a - datetime.timedelta(days = x))
    xy = [dateList,np.random.rand(100)]
    df = pd.DataFrame(data=xy[1],columns=["y"],index=xy[0])
    
    
    #Graph data
    dataS = [dict(
        x = df.index,
        y = df['y'],
        name = 'meter',
        mode = 'lines'
    )]
    
    #Graph layout
    layoutS = go.Layout(
        title="Meter",
        xaxis=dict(
            rangeslider_visible=True,
            rangeselector=dict(
                buttons=list([
                    dict(count=1, label="1m", step="month", stepmode="backward"),
                    dict(count=6, label="6m", step="month", stepmode="backward"),
                    dict(count=1, label="YTD", step="year", stepmode="todate"),
                    dict(count=1, label="1y", step="year", stepmode="backward"),
                    dict(count=5, label="5y", step="year", stepmode="backward"),
                    dict(step="all")
                ])
            )
        ),
        yaxis=dict(range=[0,2])
    )
    
    #Dash app layout
    app = dash.Dash()
    app.title = 'Random'
    
    app.layout = html.Div(
        html.Div([
            html.H1(children='Random nums'),
            html.Div(children='''
                Rand rand rand.
            '''),
    
            dcc.Input(
                id='input-y',
                placeholder='Insert y value',
                type='number',
                value='',
            ),
            html.Div(id='result'),
    
            dcc.Graph(id='RandGraph',figure=dict(data=dataS,layout=layoutS))
        ])
    )
    
    #client side implementation
    app.clientside_callback(
        """
        function(relOut, Figure) {
            if (typeof relOut !== 'undefined') {
                if (typeof relOut["xaxis.range"] !== 'undefined') {
                    //get active filter from graph
                    fromS = new Date(relOut["xaxis.range"][0]).getTime()
                    toS = new Date(relOut["xaxis.range"][1]).getTime()
    
                    xD = Figure.data[0].x
                    yD = Figure.data[0].y
    
                    //filter y data with graph display
                    yFilt = xD.reduce(function (pV,cV,cI){
                        sec = new Date(cV).getTime()
                        if (sec >= fromS && sec <= toS) {
                            pV.push(yD[cI])
                        }
                        return pV
                    }, [])
    
                    yMax = Math.max.apply(Math, yFilt)
                    yMin = Math.min.apply(Math, yFilt)
                } else { 
                    yMin = Math.min.apply(Math, Figure.data[0].y)
                    yMax = Math.max.apply(Math, Figure.data[0].y) 
                }
            } else { 
                yMin = Math.min.apply(Math, Figure.data[0].y)
                yMax = Math.max.apply(Math, Figure.data[0].y) 
            }
            Figure.layout.yaxis = {
                'range': [yMin,yMax],
                'type': 'linear'
            }
            return {'data': Figure.data, 'layout': Figure.layout};
        }
        """,
        Output('RandGraph','figure'),
        [Input('RandGraph','relayoutData')],[State('RandGraph', 'figure')]
    )
    
    #Server side implementation (slow)
    #@app.callback(
    #    Output('RandGraph','figure'),
    #    [Input('RandGraph','relayoutData')],[State('RandGraph', 'figure')]
    #)
    #def update_result(relOut,Fig):
    #    ymin = df.loc[relOut['xaxis.range'][1]:relOut['xaxis.range'][0],'y'].min()
    #    ymax = df.loc[relOut['xaxis.range'][1]:relOut['xaxis.range'][0],'y'].max()
    #    newLayout = go.Layout(
    #        title="OL Meter",
    #        xaxis=dict(
    #            rangeslider_visible=True,
    #            rangeselector=dict(
    #                buttons=list([
    #                    dict(count=0, label="1m", step="month", stepmode="backward"),
    #                    dict(count=6, label="6m", step="month", stepmode="backward"),
    #                    dict(count=1, label="YTD", step="year", stepmode="todate"),
    #                    dict(count=1, label="1y", step="year", stepmode="backward"),
    #                    dict(count=5, label="5y", step="year", stepmode="backward"),
    #                    dict(step="all")
    #                ])
    #            ),
    #            range=relOut['xaxis.range']
    #        ),
    #        yaxis=dict(range=[ymin,ymax])
    #    )
    #    
    #    
    #    Fig['layout']=newLayout
    #    return Fig
    
    if __name__ == '__main__':
        app.run_server(debug=False)