0

I built an app where a user inputs the stock symbol and then the chart updates based on that. I have a problem with default behavior of dash regarding y axis scaling when user zooms or drag the chart. The default setting of dash looks like this:enter image description here As can be seen when I zoom, the y axis range does not change according to the range of zoomed area. It has lots of blank space. In order to fix this, I tried many approaches including setting autorange=True and this SO link. After my research, I used the relayoutData in the callback. This is the code to fix this zooming/dragging issue:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
import plotly
import plotly.graph_objs as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np


app = dash.Dash(__name__)

app.layout = html.Div(children=[
    dcc.Input(id='input_1', debounce=True),
    html.P('input can be a or b', style={'color':'white'}),
    dcc.Graph(id='chart', config={'displayModeBar': True})
])

@app.callback(
Output('chart', 'figure'),
[Input('input_1','value'), Input('chart','relayoutData')],
 State('chart','figure'),
)
def set_range(sym, relay, fig_state):
    context = dash.callback_context
    if context.triggered[0]['prop_id'] == 'input_1.value':      # When the user inputs the sym

        global_df = pd.read_csv('https://raw.githubusercontent.com/AmirForooghi/stocks_csv/master/two_syms.csv')
        df = global_df.loc[global_df.sym == sym]

        fig = make_subplots(rows=2, cols=1, row_heights=[0.8, 0.2], vertical_spacing=0, shared_xaxes=True)
        fig.add_trace(go.Candlestick(open=df['open'], high=df['high'], low=df['low'], close=df['close'],
                                     increasing_line_color='#0384fc', decreasing_line_color='#e8482c', name=sym,
                                     showlegend=True,
                                     ), row=1, col=1)
        fig.add_trace(go.Scatter(y=df['pct'], marker_color='#fae823', name='pct', hovertemplate=[]), row=2, col=1)
        fig.update_layout({'plot_bgcolor': "#21201f", 'paper_bgcolor': "#21201f", 'legend_orientation': "h"},
                          legend=dict(y=0, x=0),
                          font=dict(color='#dedddc'), hovermode='x unified', showlegend=False, autosize=False, dragmode='pan',
                          margin=dict(b=20, t=0, l=0, r=40)
                          )
        fig.update_xaxes(showgrid=False, zeroline=False, rangeslider_visible=False, showticklabels=False,
                         showspikes=True, spikemode='across', spikesnap='data', showline=True, linecolor='#969799',
                         spikedash='dash', spikecolor='#ebeae8', spikethickness=0.5, rangemode='normal'
                         )
        fig.update_yaxes(showgrid=False, zeroline=False, showticklabels=True, showspikes=False)
        fig.update_traces(hoverinfo='skip', xaxis='x')
        return fig

    if relay!= None:

        if 'dragmode' in relay and relay['dragmode'] == 'zoom': # I added this because without it the chart shows a weird behaviour of not choosing zoom.
            print('if condition 1 --> setting dragmode to zoom')
            fig_state['layout'].update(dragmode='zoom')
            raise PreventUpdate

        if 'xaxis.range[0]' in relay:
            print('if condition 2 --> return data within the xaxis range')
            start = int(relay['xaxis.range[0]'])
            end = int(relay['xaxis.range[1]'])

            data_length = len(fig_state['data'][0]['open'])
            if start > data_length or end < 0:
                raise PreventUpdate
            if start < 0:
                start = 0
            if end > data_length:
                end = data_length

            low = fig_state['data'][0]['low'][start:end]
            high = fig_state['data'][0]['high'][start:end]
            price_min = min(low)
            price_max = max(high)

            pct = fig_state['data'][1]['y'][start:end]
            pct_min = min(pct) - 2
            pct_max = max(pct) + 2

            fig_state['layout']['yaxis'].update(range=[price_min, price_max], autorange=False)
            fig_state['layout']['yaxis2'].update(range=[pct_min, pct_max], autorange=False)
            if fig_state['layout']['dragmode'] == 'zoom':
                fig_state['layout']["dragmode"] = 'pan'
            return fig_state

        if 'xaxis.autorange' in relay and relay['xaxis.autorange'] == True:
            print('if condition 3 --> return data after autorange')
            pct = fig_state['data'][1]['y']
            pct_min = min(pct) - 2
            pct_max = max(pct) + 2
            fig_state['layout']['yaxis2'].update(range=[pct_min, pct_max])
            return fig_state

    if fig_state != None:   # just in case of not returning the correct data, I don't want to update
        print('if condition 4 --> raise preventupdate error')
        raise PreventUpdate

    empty_fig = go.Figure()
    empty_fig.update_layout({'plot_bgcolor': "#21201f", 'paper_bgcolor': "#21201f"},
                              font=dict(color='#dedddc'), showlegend=False, dragmode='pan',
                              xaxis = dict(showgrid=False, zeroline=False, showticklabels=False),
                              yaxis = dict(showgrid=False, zeroline=False, showticklabels=False)
                            )
    return empty_fig


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

I could fix the issue somehow as shown below: enter image description here

However I ran into a weird problem. When I zoom, if the cursor accidently drops in to the second subplot, an error will appear in the browser. enter image description here The error enter image description here

I added a couple of print statements in the code to see what is going on under the hood. When I zoom and the cursor does not trail to the second subplot, the code prints this:

if condition 1 --> setting dragmode to zoom

if condition 2 --> return data within the xaxis range

When the cursor drops in the second subplot it shows this instead:

if condition 1 --> setting dragmode to zoom

if condition 2 --> return data within the xaxis range

if condition 1 --> setting dragmode to zoom

It returns the data correctly and then goes back to condition 1. Is there any way that I can fix this? Or at least catch the error somehow and raise PreventUpdate. It is very weird problem. Since I want it for commercial use, I want to it to be perfect.

And this is the CSS file that I use just in case you need to replicate the exact settings. It needs a lot of editing and it is not the perfect one but It is what is used with this code.

html{
    height:100%;
    width:100%;
}

body {
    background-color: #21201f;
}

#input_1, #input_2, #input_3{
    color:#e3e6e4;
    background-color: #21201f;
    /*position: fixed;*/
    width: 60px;
    height: 14px;
    top: 2%;
    border-width: 1px;
    caret-color: transparent
}

#input_1{
    left: 1%;
}

#input_2{
    left: 35%;

}

#input_3{
    left: 68%;
}

#chart {
    position: fixed;
    height: 89%;
    width: 100%;
    top: 13%;
    bottom: 2%;
    /*color: green;
    font-size: 30px;*/
}


#detail_1, #detail_2, #detail_3{
    height:1%;
    color:#e3e6e4 ;
    position: fixed;
    font-size: 14px;
}

#detail_1{
    top: 0;
    left: 1%;
}

#detail_2{
    top: 0;
    left: 35%;

}

#detail_3{
    top: 0;
    left: 68%;
}
Amir
  • 978
  • 1
  • 9
  • 26
  • Hi Amir, it's Adam. It's not working for me. I get this error when I try to run it on my computer: ValueError: min() arg is an empty sequence – Adam Schroeder Dec 21 '20 at 17:37
  • @AdamSchroeder sorry for the delay. I was super busy. It works fine for me. Can you give me the traceback? The input can be a or b. – Amir Jan 01 '21 at 16:23

0 Answers0