pythonplotlyplotly-dashrangeslider

auto-rescale y-axis for range slider in plotly bar graph - python


Is there a way to force the rangeslider in plotly to auto-rescale the y-axis? Manually selecting a date range with the DatePicker works fine as the y-axis automatically updates. However, when updating through the rangeslider, the y-axis doesn't auto-rescale.

Without re-scaling, the figure is hard to interpret. It's almost not worth having at all. Are there any options to automatically force this? It may not be worthwhile also if a subsequent component (button) is required to update the plot.

The y-axis is fine initially. But when using the rangeslider to show the most recent week, the y-asic does not update (see below). Although, it works fine when using the DatePicker to get the same time period.

Both fixedrange or autorange does not force rescaling.

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd

df1 = pd.DataFrame({
        'Type': ['B','K','L','N'],
        })

N = 300
df1 = pd.concat([df1] * N, ignore_index=True)
df1['TIMESTAMP'] = pd.date_range(start='2024/01/01 07:36', end='2024/01/30 08:38', periods=len(df1))

df2 = pd.DataFrame({
        'Type': ['B','K','L','N'],
        })

N = 3
df2 = pd.concat([df2] * N, ignore_index=True)
df2['TIMESTAMP'] = pd.date_range(start='2024/01/30 08:37', end='2024/02/28 08:38', periods=len(df2))

df = pd.concat([df1,df2])

df['Date'] = df['TIMESTAMP'].dt.date
df['Date'] = df['Date'].astype(str)

external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP]

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

app.layout = dbc.Container([
        dbc.Row([
            html.Div(html.Div(children=[

            html.Div(children=[
         
            dcc.DatePickerRange(
                id = 'date-picker',
                display_format = 'DD/MM/YYYY',
                show_outside_days = True,
                minimum_nights = 0,
                initial_visible_month = df['Date'].min(),
                min_date_allowed = df['Date'].min(),
                max_date_allowed = df['Date'].max(),
                start_date = df['Date'].min(),
                end_date = df['Date'].max()
            ),

            dcc.Dropdown(
                id = 'Type',
                options = [
                    {'label': x, 'value': x} for x in df['Type'].unique()
                ],
                value = df['Type'].unique(),
                multi = True,
            ),
        ]           
        )
        ]
        )),
            dcc.Graph(id = 'date-bar-chart')
           ])
       ])

@app.callback(
        Output('date-bar-chart', 'figure'),
        Input('date-picker','start_date'), 
        Input('date-picker','end_date'),
        Input("Type", "value"),
        )     

def chart(start_date, end_date, type):

    dff = df[(df['Date'] >= start_date) & (df['Date'] <= end_date)]
    dff = dff[dff['Type'].isin(type)]

    df_count = dff.groupby(['Date','Type'])['Type'].count().reset_index(name = 'counts')

    type_fig = px.bar(x = df_count['Date'], 
                      y = df_count['counts'],
                      color = df_count['Type']
                      )

    type_fig.update_layout(
            xaxis = dict(
                rangeselector = dict(
                    buttons = list([
                        dict(count = 7,
                             label = '1w',
                             step = 'day',
                             stepmode = 'backward'),
                        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(step = 'all')
                    ])
                ),
                rangeslider = dict(
                    visible = False,
                    autorange = True
                ),
                type = 'date'
            ),
            yaxis = dict(
                autorange = True,
                fixedrange = False,
            ),
            xaxis_rangeslider_visible=True,
            xaxis_rangeslider_yaxis_rangemode="auto" 
        )

    return type_fig

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

enter image description here


Solution

  • You can achieve this using a clientside callback. The idea is the same as for Autorescaling y axis range when range slider used : we register a JS handler for the relayout event that will update the yaxis range according to the y data that are within the xaxis range.

    Add this to your current code :

    app.clientside_callback(
        ClientsideFunction(
            namespace='someApp',
            function_name='onRelayout'
        ),
        Output('date-bar-chart', 'id'),
        Input('date-bar-chart', 'relayoutData')
    )
    

    NB. Output('date-bar-chart', 'id'), is used as a "dummy output" (the zoom adjustment is done using the Plotly.js API for efficiency, so we don't need an output). While Dash supports no output callbacks (as of v2.17.0), there are still issues with clientside callbacks having no output, so we use an existing component here, and the callback function will prevent updating it by returning dash_clientside.no_update.

    In your assets_foler (default 'assets'), add a .js file with the following code :

    window.dash_clientside = Object.assign({}, window.dash_clientside, {
    
      someApp: {
    
        graphDiv: null,
    
        onRelayout(e) {
          if (
            !e || e.autosize || e.width || e.height ||  // plot init or resizing
            e['yaxis.range'] || e['yaxis.autorange'] || // yrange already adjusted
            e['yaxis.range[0]'] || e['yaxis.range[1]']  // yrange manually set
          ) {
            // Nothing to do.
            return dash_clientside.no_update;
          }
    
          if (!window.dash_clientside.someApp.graphDiv) {
            const selector = '#date-bar-chart .js-plotly-plot';
            window.dash_clientside.someApp.graphDiv = document.querySelector(selector);
          }
    
          const gd = window.dash_clientside.someApp.graphDiv;
    
          if (e['xaxis.autorange']) {
            Plotly.relayout(gd, {'yaxis.autorange': true});
            return dash_clientside.no_update;
          }
    
          // Convert xrange to timestamp so we can easily filter y data.
          const toMsTimestamp = x => new Date(x).getTime();
          const [x0, x1] = gd._fullLayout.xaxis.range.map(toMsTimestamp);
    
          // Filter y data according to the given xrange for each visible trace.
          const yFiltered = gd._fullData.filter(t => t.visible === true).flatMap(t => {
            return gd.calcdata[t.index].reduce((filtered, data) => {
              if (data.p >= x0 && data.p <= x1) {
                filtered.push(data.s0, data.s1);
              }
              return filtered;
            }, []);
          });
    
          const ymin = Math.min(...yFiltered);
          const ymax = Math.max(...yFiltered);
    
          // Add some room if needed before adjusting the yrange, taking account of
          // whether the plot has positive only vs negative only vs mixed bars.
          const room = (ymax - ymin) / 20;
          const yrange = [ymin < 0 ? ymin - room : 0, ymax > 0 ? ymax + room : 0];
    
          Plotly.relayout(gd, {'yaxis.range': yrange});
    
          return dash_clientside.no_update;
        }
      }
    });
    

    enter image description here