Simple dash app. Yaxis autoscale on xaxis zoom in stock market plot

Hi, I must have looked everywhere for a simple explanation on how this works. I have an example where I think I’m close to a solution, but I can’t seem to get it to work. Any ideas?

Here is my code so far. I basically just want the yaxis to autoscale to the data beeing shown while working the rangeslider. Thanks a lot for the help =) Any suggestions on a simpler way of doing this is also much appretiated. I’m pretty to new to dash, so I’m sure there’s better ways of doing it.

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=[0,2])
    )
    return {'layout':layout}

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

So I did manage to get the desired behavior by using the State dependency. Performance does take a big hit due to the fact that the graph have to be re-imported every time the ‘relayoutData’ is being called. I haven’t found any better implementation so far. I wish there was a way of only updating the graph layout, or better yet the ‘yaxis’ part of the layout through the callback. That way dash would only refresh the required element and performance would be great.

I suggest adding a tree structure to the property tags. For instance in this example the Output element should be like this: Output(‘RandGraph’,‘figure.layout.yaxis’). The pandas method I implemented to find the min and max of y, based on active xaxis range works good enough, but would be great if there was a native ymin, ymax function based on active datafilters.

Here is the code for anyone interested:

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])

app = dash.Dash()

app.title = 'Random'

dataS = [dict(
    x = df.index,
    y = df['y'],
    name = 'OL meter',
    mode = 'lines'
)]

def uptLayout(yaxRang):
    return 


layoutS = 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")
            ])
        )
    ),
    yaxis=dict(range=[0,2])
)


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))
    ])
)

@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)

For performance and to update just the layout, check out the examples in dash.plotly.com/clientside-callbacks where a python callback creates the figure but a javascript callback modifies the layout

Thanks a lot for the suggestion =) I managed to change the Figure object through the clientside callback. However the figure is not update in the browser. Any idea why? The console.log prints the correct layout so everything should work. The title is not updated, and the range doesn’t change.

app.clientside_callback(
    """
    function(relOut, Figure) {
        console.log(Figure)
        Figure.layout.title = 'Triggered'
        Figure.layout.yaxis = {
            'range': [0,3],
            'type': 'linear'
        }
        return Figure
    }
    """,
    Output('RandGraph','figure'),
    [Input('RandGraph','relayoutData')],[State('RandGraph', 'figure')]
)

Finally done.
Yaxis is now automatically updated on the clientside which is very responsive (I kept the slower serverside implementation for reference).

Here is the code:

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)
1 Like

Hi Thank you for your work on this. I’ve been struggling with this quite a bit. I keep getting the following error.

`---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
in
108 “”",
109 Output(‘RandGraph’,‘figure’),
–> 110 [Input(‘RandGraph’,‘relayoutData’)],[State(‘RandGraph’, ‘figure’)]
111 )
112

~/opt/anaconda3/lib/python3.7/site-packages/dash/dash.py in clientside_callback(self, clientside_function, output, inputs, state)
1211 ],
1212 “clientside_function”: {
-> 1213 “namespace”: clientside_function.namespace,
1214 “function_name”: clientside_function.function_name,
1215 },

AttributeError: ‘str’ object has no attribute ‘namespace’`

I am using JupiterLab. I can get the server side implementation to work (though not with dynamic y axis but can not get client side call back functions to work.

Hi, no problem =)
I think it may be the relayoutData that is throwing an error (it defaults to undefined untill changed). In my code I stopped using relayoutData and instead take the xaxis range directly from the figure (xRange = Figure.layout.xaxis.range). That way I always have a valid xrange to filter on. I still have relayoutData as input though to trigger the calback, I just don’t use it for anything. You can test that.

If you share your code, I can see if I find the issue

Btw a nice way to debug client side callbacks is to display all the variables in the browser console to see if there is any errors. To see the browser console you need to right click → inspect. You print to the console like this:

app.clientside_callback(
    """
    function (var1,var2,var3) {
       console.log(var1)
       console.log(var2)
       console.log(var3)

       return [var1]
    }
    """,
    [Output("out_id","data")],
    [Input("example_id","data")],
    [State("example_id2","data"),State("example_id3","data")]
)

Hmm, interesting. I literally copied and pasted your code and ran it to get this error. Just in case I accidentally fat fingered something, I’m pasting the code below. As for your tip about debugging client side callbacks, thank you for that. First, let me say that I am fairly novice. I think one of the benefits of python is it’s low barrier to entry, but that is also one of the challenges. I just felt like I was hitting my stride, then I jumped into Dash with callbacks and decorators, it kind of feels like I accidentally took an upper division class as a freshman. :slight_smile:

So, given that I already have your function below in the clientside_callback would I add this debugging language in your function to your existing function? Or, am I misunderstanding the idea of the app callback, in other words, can I have more than one callback?

Also, your point about right click> inspect element, thanks I was using Safari before which lacks that functionality. I flipped over to Chrome. If it matters, the error is persistent.

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)

This code works perfectly here =) I guess it must be an error in your setup or something. Have you tried setting up a new virtual environment and reinstall dash, plotly, pandas and numpy? Perhaps you have an older version of on of the libraries or something.

Good luck =)

Thank you @kkollsg, I’ll try that. It could have something to do with Jupyter Lab vs Jupyter Notebook. I’ll attempt your suggestions as well as try it in Jupyter Notebook.

As for the last bit of my question, related to debugging, am I supposed to add your debugging code to my existing function below the callback or am I supposed to add a different callback?

I’m referring to your comment spelled out below. Can you treat me like a dunce and repost your entire sample code but add in the debugging language below so I can understand where and how to use it. I feel like I’m just missing a few fundamental pieces of the decorators and callbacks so converting the pseudo-code below into practice is just a bit out of my reach. I feel like I can fumble along and figure it out but I won’t really understand why it is working. Perhaps if I see your complete example, I will better understand. If I’m asking too much, feel free to tell me to go away :slight_smile: I just feel like this might be helpful for others as well.

`app.clientside_callback(
“”"
function (var1,var2,var3) {
console.log(var1)
console.log(var2)
console.log(var3)

   return [var1]
}
""",
[Output("out_id","data")],
[Input("example_id","data")],
[State("example_id2","data"),State("example_id3","data")]

)`

Hi the console.log is the javascript version of print. You just put it straight after the function declaration like this. Then you can check the output in the console in the browser (right click + inspect). The app.clientside_callback might be a bit confusing if you haven’t worked in javascript before. But I assure you javascript is relatively simple to learn as well =) It also executes faster than python code, which is a big pluss when working with complex dashboards. So any processing loads you can move across to the browser relieves the server, and reduces lagg, which again improves user experience. So its worth the extra effort =)

app.clientside_callback(
    """
    function(relOut, Figure) {
        console.log(relOut)
        console.log(Figure)
1 Like