1. Creating simple web apps with Streamlit

In this tutorial we will learn basic usage of https://docs.streamlit.io/get-started/fundamentals/main-concepts.

Note

To do this tutorial you will need streamlit installed in your conda environment. To do that:

  1. Open a terminal (from in VS Code, click “Terminal” -> “New Terminal”).

  2. Activate your ist356 conda environment by running:

    conda activate ist356
  3. Install streamlit by either running:

    pip install streamlit

    or:

    conda install -c conda-forge -y streamlit

What is Streamlit?

Streamlit markets itself as a User Interface library for building simple web applications from Python scripts.

Build:

  • Interactive dashboard with tables, charts and graphs
  • Accepting user input for a data pipeline
  • Chat applications
  • and more!

The best part of streamlit is the application runs in a browser and you don’t need to learn front-end technologies like HTML, CSS and Javascript

How does it work?

To illustrate how streamlit works, here is a simple Python script that uses the Streamlit library:

import streamlit as st

st.title("Saying Hello.")
name = st.text_input("And you are?")

if name:
    st.write(f"Hello, {name}!")

To test this:

  1. Create a file called hello-ui.py using VS Code (Click “File -> New File”) and paste the above code into it.

  2. Save the file (File -> Save, or CTRL/CMD + S).

  3. Open a terminal (in VS Code, Terminal -> New Terminal). If your ist356 environment is not active, activate it by running conda activate ist356.

  4. Now run:

    python -m streamlit run hello-ui.py

    When you run the script, Streamlit launches a webserver on your computer to run your app; your default web browser will automatically open with the simple website Streamlit created.

  5. If you edit the hello-ui.py file, streamlit will automatically detect the changes when the changes are saved. If you re-do the interaction (in this case, adding a name and hitting Enter), the new code will be executed. Try it! Add a waving emoji by adding :wave: before the Hello in the st.write.

  6. The app will continue to run in the browser. You can stop it by first hitting CTRL+C in the terminal, then closing the browser tab.

Interactions

Streamlit supports both linear and event-driven interactions.

Linear style

With linear interactions the code runs from top-down each time the input changes.

The linear pattern is the simpler pattern.

The most effective way to use of this pattern is:

  • setup widgets, saving their state in variables
  • then check interations through the variables with if

An example:

import streamlit as st

st.title("Streamlit Interaction: linear")

# setup
name = st.text_input("Who are you?")
hi_clicked = st.button('Say Hi!')
clear_clicked = st.button('Clear')

# interactions
if hi_clicked:
    if name:
        st.success(f"Hello, {name}", icon="👍")
    else:
        st.error(f"I can't say hello, if you don't tell me your name!", icon="💣")

if clear_clicked:
    name = None 

Event-driven style

With event-driven interactions, you write a function to handle the event. This is similar to how most other UI libraries work.

Warning

The event-driven pattern is more complex, and might have unexpected behaviors due to streamlit’s processing order!

The most effective use of this pattern is to:

  • create handler functions with def
  • setup interactions, using the function on the event

Example:

import streamlit as st


def hi_click():
    if name:
        st.success(f"Hello, {name}", icon="👍")
    else:
        st.error(f"I can't say hello, if you don't tell me your name!", icon="💣")


def clear_click():
    name = None 

# setup
st.title("Streamlit Interaction: event-driven")
name = st.text_input("Who are you?")
st.button('Say Hi!', on_click=hi_click)
st.button('Clear', on_click=clear_click)
CautionCode Challenge 3.1.1

Write a streamlit app that takes as input a length and width of a rectangle, and outputs the permieter [2 x (L+W)] and area [(L x W)] of that rectangle. Add a “calculate” button and a “clear” button.

Hint: use st.number_input() for numbers.

import streamlit as st

st.title('Area and permieter')
length = st.number_input("Enter Length:")
width = st.number_input("Enter Width:")
btn_clicked = st.button('Calculate!')

if btn_clicked:
    area = length * width
    perm = 2 * (length + width)
    st.write(f"Area: {area}")
    st.write(f"Perimeter: {perm}")

Session State: Helping Streamlit Remember values

st.session_state is a global key/value store for data that need to persist between streamlit runs.

Any data dependent on a previous interaction would be a use case for this.

The pattern using session state is:

- initialize the session state
- create the widgets
- check the interactions that change the state
- display the widgets that update the state

The session state is necessary to store persistent data. For example, the following will not work:

Wrong way:

import streamlit as st

# Streamlit is always running, so only do this will not do what you think

count = 0

# widget setup
st.title('Counter Example: Wrong')
st.write("variables that change based on previous runs will not work as expected ")
st.write("this is because streamlit runs all this code with each interaction")
incr_clicked = st.button('increment counter', type='primary')
reset_clicked = st.button('reset counter', type='secondary')

# interactions
if reset_clicked:
    count = 0
elif incr_clicked:
    count = count + 1
    
# display session state, after interations
st.write(f'Button clicked {count} times')

Why not? (Try it!)

However, this will work:

Right way:

import streamlit as st

# Streamlit is always running, so only do this when count is not in session_state

# initialize
if 'count' not in st.session_state:
    st.session_state.count = 0

# widget setup
st.title('Counter Example: Session State')
st.write("variables that change based on previous runs need session state")
st.write("`st.session_state` preserves the values of the variable between runs")
incr_clicked = st.button('increment counter', type='primary')
reset_clicked = st.button('reset counter', type='secondary')

# interactions
if reset_clicked:
    st.session_state.count = 0
elif incr_clicked:
    st.session_state.count = st.session_state.count + 1
    
# display session state, after interations
st.write(f'Button clicked {st.session_state.count} times')
CautionCode Challenge 3.1.2

Order total and history:

  • Write a streamlit app to input an amount.

  • Create an “add to total” button to accumulate the amount in the total.

  • Create a “clear” button to reset the session vars.

  • Display the total and the history of each item entered.

Hint: you’ll need to manage a list for history!

import streamlit as st

# initialize
if 'total' not in st.session_state:
    st.session_state.total = 0.0
if 'history' not in st.session_state:
    st.session_state.history = []

st.title('Order Total and History')
amount = st.number_input("Amount:")
btn_add = st.button('Add to Total')
btn_clear = st.button('Clear')

if btn_add:
    st.session_state.history.append(amount)
    st.session_state.total = sum(st.session_state.history)
    st.write(f"TOTAL: {st.session_state.total}")
    st.write("HISTORY:")
    for h in st.session_state.history:
        st.write(h)

if btn_clear:
    st.session_state.history = []
    st.session_state.total = 0.0

Exploring Streamlit input widgets

Streamlit offers a number of different widgets for user input, beyond the text input highlighted above. The following code highlights them:

import streamlit as st
from  datetime import datetime 
st.title('Streamlit Input Widgets!')

st.markdown("## Text Inputs")
txt = st.text_input('Enter your name:', value='John Doe')
st.text(f"OUTPUT: {txt}, type: {type(txt)}")
pw = st.text_input('Enter your password:', type="password")
st.text(f"OUTPUT: {pw}, type: {type(pw)}")
txta = st.text_area('Leave a comment:', value='Type here...')
st.text(f"OUTPUT: {txta}, type: {type(txta)}")
st.divider()

st.markdown("## Binary Widgets")
chk = st.checkbox('I agree to the terms and conditions', value=False)
st.text(f"OUTPUT: {chk}, type: {type(chk)}")
tog = st.toggle('Enable notifications', value=False)
st.text(f"OUTPUT: {tog}, type: {type(tog)}")
st.divider()

st.markdown("## Date / Time Widgets")
dt =st.date_input('Select a date:')
st.text(f"OUTPUT: {dt}, type: {type(dt)}")
tm = st.time_input('Select a time:')
st.text(f"OUTPUT: {tm}, type: {type(tm)}")
st.divider()

st.markdown("## Number Widgtets")
numi = st.number_input('Enter Hourly Wage:', value=7.25, max_value=20.0, min_value=5.25, step=0.25)
st.text(f"OUTPUT: {numi}, type: {type(numi)}")
nums = st.slider('Pick a number between 1 and 20:', min_value=1, max_value=20, value=10, step=1)
st.text(f"OUTPUT: {nums}, type: {type(nums)}")
st.divider()

st.markdown("## Selection Widgets")
selbox = st.selectbox('Choose one shipping method:', ['Jiffy Express', ' You Pee Es', 'FedUp Express'])
st.text(f"OUTPUT: {selbox}, type: {type(selbox)}")
mulselbox = st.multiselect('Select all your favorite colors:', ['Red', 'Green', 'Blue', 'Yellow', 'White'])
st.text(f"OUTPUT: {mulselbox}, type: {type(mulselbox)}")
selslider = st.select_slider('Rate us:', options=['1=Poor','2=ok','3=good','4=great','5=excellent'], value = '3=good')
st.text(f"OUTPUT: {selslider}, type: {type(selslider)}")
radio = st.radio('Rate us:', ['1=Poor','2=ok','3=good','4=great','5=excellent'], index=2, horizontal=True)
st.text(f"OUTPUT: {radio}, type: {type(radio)}")
st.divider()


st.markdown("## 'Other' Widgets")
feed = st.feedback('faces')
st.text(f"OUTPUT: {feed}, type: {type(feed)}")
color = st.color_picker('Pick a color:', value='#00f900')
st.text(f"OUTPUT: {color}, type: {type(color)}")
file = st.file_uploader('Upload a file:')
st.text(f"OUTPUT: {file}, type: {type(file)}")
pic = st.camera_input('Take a selfie:')
st.text(f"OUTPUT: {pic}, type: {type(pic)}")
st.divider()

Exploring Steamlit output widgets

Likewise, Streamlit offers a number of different widgets for displaying different types of output. Here’s a sampling:

import streamlit as st

st.title('Streamlit Output Widgets!')

st.markdown("## Text Output")
st.text("Plain text.\nObeys newlines.")

st.markdown("## Markdown Output")
st.markdown('''
### Heading 3
- this
- is a
- list
            
Learn markdown here: [https://www.markdownguide.org/getting-started/](https://www.markdownguide.org/getting-started/)
''')

st.markdown("## Code Output")
st.code('''
name = input("Enter your name:")
print(f"Hello, {name}")
''', language="python", line_numbers=True)

st.markdown("## Image Output")
st.image("https://ist256.com/images/logo.png",caption="IST256 logo")

st.markdown("## Metric / Card Ouput")
st.metric(label="Temperature", value="70 °F", delta="1.2 °F")
st.metric(label="Mike Fudge", value="B+", delta="-5 pts")

st.markdown("## Video Output")
st.video("https://youtu.be/soVItkifdms?si=eNNbRXnAg4efcJGi")

st.markdown("## Audio Output")
st.audio("https://file-examples.com/storage/fe6993554766e3161a375a5/2017/11/file_example_MP3_700KB.mp3")

st.markdown("## Toast Output")
if st.button("Click to show toast"):
    st.toast("Congrats! You clicked it!", icon=":material/thumb_up:")

st.markdown("## Column Layouts")
col1, col2, col3 = st.columns(3)
col1.markdown("Hello")
col2.text("There")
col2.text("Mike")
col3.warning("Warning!")
col3.error("Error!")
col3.success("Success!")

st.markdown("## Tab Layouts")
col1, col2, col3 = st.tabs(["Tab A","Tab B","Tab C"])
col1.markdown("Hello")
col2.text("There")
col2.text("Mike")
col3.warning("Warning!")
col3.error("Error!")
col3.success("Success!")

st.markdown("## Expander Output")
with st.expander("See a map"):
    st.write('Here is a map for you!')
    st.map(latitude=76,longitude=-43, zoom=13)

File Uploads

We can use st.file_uploader() to allow users to upload files. The uploaded files return a Python “file-like” that can be used in a variety of applications with little additional processing.

An example:

import streamlit as st
from io import StringIO # required to convert binary to text


st.title("File Upload Example")
st.markdown('''
This example demonstrates how to process and uploaded file. 
            
- The first example can process and file-like (image, video, data for a dataframe, etc)
- The second example shows how to process text explicitly.
            
''')
bin_file_data = st.file_uploader("Upload an image/photo file", type=["png", "jpeg", "jpg", "gif"])
text_file_data = st.file_uploader("Upload a text file", type=["txt", "csv", "md"])

if bin_file_data:
    st.markdown(f"### {bin_file_data.name}")
    st.image(bin_file_data)

if text_file_data:
    st.markdown(f"### {text_file_data.name}")
    binary_contents = text_file_data.getvalue()
    # Convert binary to text
    text_contents = StringIO(binary_contents.decode("utf-8")).read() 
    st.text(text_contents)

print(text_file_data)
CautionCode Challenge 3.1.3

Order file processing:

  • Write a Streamlit app that takes as input a text file with one line per order. Samples are provided in the data folder, but each line should have the amount of the order.

  • Output the number of orders and the total amount of all orders.

import streamlit as st

from io import StringIO # required to convert binary to text

st.title("Order File Processing")
text_file_data = st.file_uploader("Upload the order file", type=["txt"])

if text_file_data:
    binary_contents = text_file_data.getvalue()
    # Convert binary to text
    text_contents = StringIO(binary_contents.decode("utf-8")).read() 
    total = 0
    count = 0
    for line in text_contents.split("\n"):
        try:
            order = float(line)
            total = total + order
            count = count + 1
        except ValueError:
            continue 
    st.info(f"Number of orders: {count}", icon="➕")
    st.info(f"Total amount: ${total:.2f}", icon="💵")

Image Processing with the camera

We can use st.camera_input() to get images from our webcams.

From there its easy to load into popular image processing libraries.

An example:

import streamlit as st
from PIL import Image


st.title("Camera Example")
st.markdown('''
    Let's take a picture with the camera and conver the image to greyscale with PIL

    Learn More about PIL: https://pillow.readthedocs.io/en/stable/index.html
''')
pic_data = st.camera_input("Take a pic!")

if pic_data:
    img = Image.open(pic_data)
    grey_img = img.convert("L")
    st.image(grey_img)