St.rerun Issue - Page flickers and scrolls to top on data refresh

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 :

  1. The entire page flickers (elements briefly disappear/reappear)
  2. The screen scrolls back to the top, disrupting user experience

What I tried :

  • Using st.rerun() in a loop with time.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