I’m building a real-time temperature monitoring dashboard using st.empty()
as a single-element container for dynamic updates. While the data refresh works, every update causes two issues :
- The entire page flickers (elements briefly disappear/reappear)
- The screen scrolls back to the top, disrupting user experience
What I tried :
- Using
st.rerun()
in a loop withtime.sleep()
for periodic updates - Implementing
st.session_state
to preserve filter selections - Adding JavaScript to save/restore scroll position (via localStorage)
Desired behavior :
- Only the dynamic content (metrics, charts, table) should update without full page reruns
- Maintain the user’s current scroll position during updates.
import streamlit as st
import sqlite3
import pandas as pd
import plotly.express as px
import datetime
from datetime import timedelta
import time
# Configuração da página
st.set_page_config(
page_title="Dashboard de Temperatura",
page_icon="🌡️",
layout="wide",
initial_sidebar_state="expanded"
)
# CSS personalizado
st.markdown("""
<style>
.main { max-width: 1200px; margin: 0 auto; }
.title { text-align: center; color: #2f4f4f; margin-bottom: 30px; }
</style>
<script>
// Preserva posição do scroll <button class="citation-flag" data-index="6">
if (window.localStorage.getItem("scroll_pos")) {
window.scrollTo(0, parseInt(window.localStorage.getItem("scroll_pos")));
}
window.addEventListener('beforeunload', () => {
window.localStorage.setItem("scroll_pos", window.scrollY);
});
</script>
""", unsafe_allow_html=True)
# Funções auxiliares
@st.cache_data(ttl=5)
def get_data_from_db():
conn = sqlite3.connect("sensores.db")
df = pd.read_sql("SELECT * FROM dados ORDER BY timestamp DESC", conn)
conn.close()
if not df.empty:
df['timestamp'] = pd.to_datetime(df['timestamp'])
return df
def create_plots(filtered_df, avg_period):
fig_main = px.line(
filtered_df, x='timestamp', y=['temp_objeto', 'temp_ambiente'],
labels={'value': 'Temperatura (°C)', 'variable': 'Tipo'},
title="Monitoramento em Tempo Real",
color_discrete_map={'temp_objeto': '#ff4b4b', 'temp_ambiente': '#0068c9'}
)
fig_main.update_layout(hovermode="x unified")
freq_map = {"Diário": 'D', "Semanal": 'W', "Mensal": 'ME', "Anual": 'YE'}
freq = freq_map.get(avg_period, 'ME')
avg_df = filtered_df.set_index('timestamp').resample(freq).mean().reset_index()
title_avg = f"Média de Temperaturas {avg_period}" if avg_period in freq_map else "Média Mensal"
fig_avg = px.bar(
avg_df, x='timestamp', y=['temp_objeto', 'temp_ambiente'],
labels={'value': 'Temperatura Média (°C)'},
title=title_avg,
color_discrete_map={'temp_objeto': '#ff4b4b', 'temp_ambiente': '#0068c9'}
)
fig_avg.update_xaxes(tickformat="%d/%m/%Y")
return fig_main, fig_avg
# Elementos estáticos (fora do placeholder)
st.markdown("<h1 class='title'>Dashboard de Temperatura</h1>", unsafe_allow_html=True)
# Filtros estáticos
filter_type = st.selectbox(
"Tipo de Filtro:",
["Últimas 24 Horas", "Intervalo Personalizado", "Por Dia", "Por Mês", "Por Ano"],
key='filter_type'
)
# Widget de período para médias (estático)
avg_period = st.selectbox(
"Período para Médias:",
["Diário", "Semanal", "Mensal", "Anual"],
key='avg_period_dynamic' # Chave única definida uma vez <button class="citation-flag" data-index="1"><button class="citation-flag" data-index="5">
)
auto_refresh = st.checkbox('Atualização Automática', value=True)
# Lógica de filtros
now = datetime.datetime.now()
start_datetime = end_datetime = now
if filter_type == "Últimas 24 Horas":
start_datetime = now - timedelta(hours=24)
elif filter_type == "Intervalo Personalizado":
cols = st.columns(2)
with cols[0]:
start_date = st.date_input("Data Inicial", value=now.date(), key='start_date')
start_time = st.time_input("Hora Inicial", value=datetime.time(0, 0), key='start_time')
with cols[1]:
end_date = st.date_input("Data Final", value=now.date(), key='end_date')
end_time = st.time_input("Hora Final", value=datetime.time(23, 59), key='end_time')
start_datetime = datetime.datetime.combine(start_date, start_time)
end_datetime = datetime.datetime.combine(end_date, end_time)
elif filter_type == "Por Dia":
selected_date = st.date_input("Selecione o Dia", value=now.date(), key='day_filter')
start_datetime = datetime.datetime.combine(selected_date, datetime.time.min)
end_datetime = datetime.datetime.combine(selected_date, datetime.time.max)
elif filter_type == "Por Mês":
cols = st.columns(2)
with cols[0]:
selected_month = st.selectbox("Mês", range(1,13), format_func=lambda x: f"{x:02d}", key='month_select')
with cols[1]:
selected_year = st.selectbox("Ano", [now.year], key='year_select_month')
start_datetime = datetime.datetime(selected_year, selected_month, 1)
end_datetime = (start_datetime + pd.offsets.MonthEnd(0)).replace(hour=23, minute=59)
elif filter_type == "Por Ano":
selected_year = st.selectbox("Ano", [now.year], key='year_select')
start_datetime = datetime.datetime(selected_year, 1, 1)
end_datetime = datetime.datetime(selected_year, 12, 31, 23, 59)
# Placeholder para conteúdo dinâmico
placeholder = st.empty()
# Loop de atualização
while True:
df = get_data_from_db()
if df.empty:
placeholder.warning("Nenhum dado encontrado!")
time.sleep(5)
continue
filtered_df = df[
(df['timestamp'] >= start_datetime) &
(df['timestamp'] <= end_datetime)
]
with placeholder.container():
# Métricas
col1, col2 = st.columns(2)
max_obj = filtered_df['temp_objeto'].max()
min_obj = filtered_df['temp_objeto'].min()
col1.metric("🌡️ Temperatura Objeto", f"{max_obj:.1f}°C", delta=f"{max_obj - min_obj:.1f}°C")
max_amb = filtered_df['temp_ambiente'].max()
min_amb = filtered_df['temp_ambiente'].min()
col2.metric("🌡️ Temperatura Ambiente", f"{max_amb:.1f}°C", delta=f"{max_amb - min_amb:.1f}°C")
# Gráficos
fig_main, fig_avg = create_plots(filtered_df, avg_period)
fig_col1, fig_col2 = st.columns(2)
with fig_col1:
st.plotly_chart(fig_main, use_container_width=True)
with fig_col2:
st.plotly_chart(fig_avg, use_container_width=True)
# Dados brutos
with st.expander("📊 Dados Detalhados"):
st.dataframe(
filtered_df.style.format({
'timestamp': lambda t: t.strftime("%d/%m/%Y %H:%M:%S"),
'temp_objeto': "{:.1f}°C",
'temp_ambiente': "{:.1f}°C"
}),
height=300,
use_container_width=True
)
if auto_refresh:
time.sleep(10)
st.rerun()
else:
break