Display order of widgets when using callbacks

I’ve made a very simple app that:

  1. Uses the file uploader widget to read in an Excel file
  2. Gets the user to choose a worksheet they’re interested in previewing using a selectbox dropdown
  3. Displays a preview of the worksheet

I’m using a callback from st.selectbox to display the preview, so that it only displays after the user has actively chosen a worksheet they’re interested in. But in order to do so I need to have defined my callback function before calling st.selectbox - and that’s leading the preview to appear above the file uploader and selectbox widgets in the app, rather than where I want it, below those widgets.

This is my code (Streamlit 1.3.1) and a screengrab of my output.

import streamlit as st
import pandas as pd

# CREATE APP
# Add file_uploader
uploaded_file = st.file_uploader(
    label='Upload a file', type=['xls', 'xlsx', 'xlsm'],
    key='file-uploader',
    help='''
        Upload an Excel file. The file must be
        closed in order for you to upload it.
    '''
)


# Define function that loads sheet preview
def load_sheet_preview():
    st.dataframe(df[st.session_state.selectbox_sheet])


# Add selectbox
if uploaded_file is not None:
    df = pd.read_excel(uploaded_file, sheet_name=None)      # sheet_name=None needs explicitly including - it isn't the default        # noqa: E501

    st.selectbox(
        key='selectbox_sheet',
        label='Select worksheet',
        options=df.keys(),
        on_change=load_sheet_preview
    )

I suspect I’ll kick myself when I hear the solution but I can’t think how to get around this. Wrapping the code that produces the file uploader and the selectbox in functions, ordered as I want them to appear in the app, doesn’t do it.

Thank you.

when you callback the load_sheet_preview function, you write to the page with st.dataframe. Since the callback function is written to the page before anything else during the streamlit reload, it means that the dataframe is written to the page before moving to the if uploaded_file is not None: statement.

I would need to thing about the solution some more, but maybe this helps you get there first.

Thanks, Ryan.

Yeah, I think that’s right. I can’t figure out how to change my code to get around the issue though - I can’t move where the function is defined, and I’m not sure what other changes I can make to my code. If you do have any ideas, I’d be very grateful to hear them!

st.container looks like a way to define where things are, but defer loading:
st.container - Streamlit Docs

I havne’t tried it, but I use a similar concept with columns - I’ll define the columns, but fill them after I run some other code.

Thanks Albert.

I’ve now tried this two ways. Including the code that displays the preview in the container gives a ‘Bad message format’ error (’‘setIn’ cannot be called on an ElementNode’"). I think it might be related to the points raised here - callbacks aren’t designed to change the display of the app.

Including the file_uploader and selectbox elements within the container instead doesn’t give an error, but the order of elements remains the same (i.e. the preview above the other elements), presumably because functions are evaluated first.

Happy to share my code on any of this if anyone would like to see it.

I have come up with a different workaround though which I’ll post below.

I’ve come up with a solution to this which involves (explicitly) using st.session_state rather than a callback and adding a default value to the dropdown options. The preview isn’t loaded while the default option is selected.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import io

import pandas as pd
import streamlit as st

# CREATE APP
# Add file_uploader
uploaded_file = st.file_uploader(
    label='Upload a file', type=['xls', 'xlsx', 'xlsm'],
    key='file-uploader',
    help='''
        Upload an Excel file. The file must be
        closed in order for you to upload it.
    '''
)

# Add selectbox
if 'selectbox_sheet' not in st.session_state:       # Initialise variable
    st.session_state['selectbox_sheet'] = '--'

if uploaded_file is not None:
    sheets_dict = pd.read_excel(uploaded_file, sheet_name=None)      # This creates a dictionary of dataframes. sheet_name=None needs explicitly including - it isn't the default        # noqa: E501

    default_option = {'--': ''}

    selectbox_options = dict(**default_option, **sheets_dict)     # Join dictionaries        # noqa: E501

    st.selectbox(
        key='selectbox_sheet',
        label='Select worksheet',
        options=selectbox_options.keys()
    )

# Load sheet preview
if st.session_state.selectbox_sheet != '--':
    st.dataframe(sheets_dict[st.session_state.selectbox_sheet])

If there’s a more elegant solution I’d still be interested to hear it - I would have thought it would be possible to do something using st.container().