Dashboards Interativos com Shiny for Python

“A diferença entre uma análise que permanece na gaveta e uma que transforma decisões está na interatividade. O Shiny for Python democratiza a criação de dashboards profissionais, transformando dados estáticos em experiências envolventes.”

O Shiny for Python é um framework revolucionário que permite criar aplicações web interativas diretamente em Python, sem necessidade de conhecer HTML, CSS ou JavaScript. Seguindo os princípios da gramática dos gráficos que você já domina, agora aprenderá a construir dashboards que trazem seus dados à vida.

Neste módulo, você desenvolverá:

Vamos construir um dashboard completo para explorar as Unidades Básicas de Saúde (UBS) do Brasil, combinando mapas interativos, filtros dinâmicos e visualizações que respondem em tempo real às interações do usuário.

O que é o Shiny for Python?

A Revolução da Interatividade

Imagine transformar suas análises estáticas em experiências dinâmicas onde cada clique revela novos insights. O Shiny for Python torna isso realidade através do paradigma reativo - mudanças em inputs automaticamente disparam atualizações nos outputs relacionados.


from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np

np.random.seed(42)
data = pd.DataFrame({
    'estado': np.repeat(['CE', 'SP', 'RJ', 'MG'], 12),
    'mes': list(range(1, 13)) * 4,
    'atendimentos': np.random.normal(120000, 15000, 48).astype(int)
})

ui.input_radio_buttons("estado", "Estado:", ['CE', 'SP', 'RJ', 'MG'], inline=True)

@render_widget
def grafico():
    dados = data[data["estado"] == input.estado()]
    return alt.Chart(dados).mark_line(point=True, color='#005baa').encode(
        x=alt.X('mes:O', title='Mês', axis=alt.Axis(labelAngle=0)),
        y=alt.Y('atendimentos:Q', title='Atendimentos')
    ).properties(width=500, height=250)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 400px

from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np

ui.tags.style("""
.forward-fill-potential > * {
    display: flex;
    flex-direction: column;
    flex: 1 1 0 !important;
    min-height: 0;
    min-width: 0;
    width: 100%;
}
@media (min-width: 576px) {
    .container-sm, .container {
        max-width: none;
    }
}
""")

np.random.seed(42)
data = pd.DataFrame({
    'estado': np.repeat(['CE', 'SP', 'RJ', 'MG'], 12),
    'mes': list(range(1, 13)) * 4,
    'atendimentos': np.random.normal(120000, 15000, 48).astype(int)
})

ui.input_radio_buttons("estado", "Estado:", ['CE', 'SP', 'RJ', 'MG'], inline=True)

@render_widget
def grafico():
    dados = data[data["estado"] == input.estado()]
    return alt.Chart(dados).mark_line(point=True, color='#005baa').encode(
        x=alt.X('mes:O', title='Mês', axis=alt.Axis(labelAngle=0)),
        y=alt.Y('atendimentos:Q', title='Atendimentos')
    ).properties(width=500, height=250)

Anatomia de uma Aplicação Shiny

Toda aplicação Shiny é construída sobre três pilares fundamentais:

Componente Função Analogia
UI (User Interface) Define a aparência e layout O “corpo” da aplicação - o que o usuário vê
Server Contém a lógica e processamento O “cérebro” da aplicação - onde a mágica acontece
Reatividade Conecta UI e Server automaticamente O “sistema nervoso” - como tudo se comunica

Express vs Core: Duas Abordagens

O Shiny oferece duas sintaxes para criar aplicações:

  • Shiny Express: Sintaxe simplificada onde UI e server coexistem no mesmo arquivo
  • Shiny Core: Separação explícita entre UI e server para maior controle

Shiny Express (Recomendado para Iniciantes)

from shiny.express import input, render, ui
import numpy as np
import matplotlib.pyplot as plt

ui.h1("Explorando Dados com Shiny Express")
ui.input_slider("n", "Número de observações:", min=10, max=1000, value=500)

@render.plot
def histograma():
    data = np.random.normal(size=input.n())
    
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.hist(data, bins=30, alpha=0.7, color='#2E86C1')
    ax.set_title(f"Distribuição Normal - {input.n()} observações")
    ax.set_xlabel("Valor")
    ax.set_ylabel("Frequência")
    return fig
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 500px

from shiny.express import input, render, ui
import numpy as np
import matplotlib.pyplot as plt

ui.input_slider("n", "Número de observações:", min=10, max=1000, value=500)

@render.plot
def histograma():
    data = np.random.normal(size=input.n())
    
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.hist(data, bins=30, alpha=0.7, color='#2E86C1')
    ax.set_title(f"Distribuição Normal - {input.n()} observações")
    ax.set_xlabel("Valor")
    ax.set_ylabel("Frequência")
    return fig

Shiny Core (Separação Explícita)

from shiny import App, render, ui
import numpy as np
import matplotlib.pyplot as plt

# UI: Interface definida separadamente
app_ui = ui.page_fluid(
    ui.input_slider("n", "Número de observações:", min=10, max=1000, value=500),
    ui.output_plot("histograma")
)

# Server: Lógica separada
def server(input, output, session):
    @render.plot
    def histograma():
        data = np.random.normal(size=input.n())
        
        fig, ax = plt.subplots(figsize=(8, 5))
        ax.hist(data, bins=30, alpha=0.7, color='#E74C3C')
        ax.set_title(f"Distribuição Normal - {input.n()} observações")
        ax.set_xlabel("Valor")
        ax.set_ylabel("Frequência")
        return fig

# App: Conecta UI e Server
app = App(app_ui, server)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 500px

from shiny import App, render, ui
import numpy as np
import matplotlib.pyplot as plt

# UI: Interface definida separadamente
app_ui = ui.page_fluid(
    ui.input_slider("n", "Número de observações:", min=10, max=1000, value=500),
    ui.output_plot("histograma")
)

# Server: Lógica separada
def server(input, output, session):
    @render.plot
    def histograma():
        data = np.random.normal(size=input.n())
        
        fig, ax = plt.subplots(figsize=(8, 5))
        ax.hist(data, bins=30, alpha=0.7, color='#E74C3C')
        ax.set_title(f"Distribuição Normal - {input.n()} observações")
        ax.set_xlabel("Valor")
        ax.set_ylabel("Frequência")
        return fig

# App: Conecta UI e Server
app = App(app_ui, server)
Dica💡 O Poder da Reatividade

Em ambos os casos, quando você move o slider, o gráfico atualiza automaticamente. O Shiny detecta dependências e gerencia as atualizações!

Fundamentos da Reatividade

Como Funciona a Magia Reativa

A reatividade no Shiny funciona através de um sistema de dependências automáticas. Quando um output usa um input, o Shiny automaticamente:

  1. Detecta a dependência entre output e input
  2. Re-executa o output quando o input muda
  3. Minimiza atualizações executando apenas o necessário
from shiny.express import input, render, ui
import time

ui.input_text("nome", "Digite seu nome:", value="")
ui.input_slider("idade", "Sua idade:", min=0, max=100, value=25)

@render.text
def saudacao():
    # Simula processamento
    time.sleep(0.1)
    
    if input.nome():
        return f"Olá, {input.nome()}! Você tem {input.idade()} anos."
    else:
        return "Digite seu nome para começar..."

@render.text  
def apenas_idade():
    return f"Idade atual: {input.idade()}"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 250px

from shiny.express import input, render, ui
import time

ui.input_text("nome", "Digite seu nome:", value="")
ui.input_slider("idade", "Sua idade:", min=0, max=100, value=25)

@render.text
def saudacao():
    # Simula processamento
    time.sleep(0.1)
    
    if input.nome():
        return f"Olá, {input.nome()}! Você tem {input.idade()} anos."
    else:
        return "Digite seu nome para começar..."

@render.text  
def apenas_idade():
    return f"Idade atual: {input.idade()}"

Cálculos Reativos: Evitando Repetições

Para cálculos que são usados em múltiplos outputs, use @reactive.calc para evitar reprocessamento desnecessário:

from shiny import reactive
from shiny.express import input, render, ui
import pandas as pd
import numpy as np

ui.input_slider("n_amostras", "Número de amostras:", min=100, max=5000, value=1000)
ui.input_slider("seed", "Semente aleatória:", min=1, max=100, value=42)

@reactive.calc
def dados_processados():
    # Simula processamento pesado
    np.random.seed(input.seed())
    return pd.DataFrame({
        'x': np.random.normal(0, 1, input.n_amostras()),
        'y': np.random.normal(0, 1, input.n_amostras())
    })

@render.ui
def estatisticas():
    dados = dados_processados()  # Usa o cálculo reativo
    return ui.HTML(f"""
        Estatísticas dos dados:<br>
        - Média X: {dados['x'].mean():.3f}<br>
        - Média Y: {dados['y'].mean():.3f}<br>
        - Correlação: {dados['x'].corr(dados['y']):.3f}<br>
    """)

@render.text
def contagem():
    dados = dados_processados()  # Reutiliza sem recalcular!
    return f"Total de {len(dados)} pontos gerados"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 350px

from shiny import reactive
from shiny.express import input, render, ui
import pandas as pd
import numpy as np

ui.input_slider("n_amostras", "Número de amostras:", min=100, max=5000, value=1000)
ui.input_slider("seed", "Semente aleatória:", min=1, max=100, value=42)

@reactive.calc
def dados_processados():
    # Simula processamento pesado
    np.random.seed(input.seed())
    return pd.DataFrame({
        'x': np.random.normal(0, 1, input.n_amostras()),
        'y': np.random.normal(0, 1, input.n_amostras())
    })

@render.ui
def estatisticas():
    dados = dados_processados()  # Usa o cálculo reativo
    return ui.HTML(f"""
        Estatísticas dos dados:<br>
        - Média X: {dados['x'].mean():.3f}<br>
        - Média Y: {dados['y'].mean():.3f}<br>
        - Correlação: {dados['x'].corr(dados['y']):.3f}
    """)

@render.text
def contagem():
    dados = dados_processados()  # Reutiliza sem recalcular!
    return f"Total de {len(dados)} pontos gerados"

Construindo Interfaces Profissionais

Componentes Essenciais para Dashboards

O Shiny oferece uma rica coleção de componentes para criar interfaces intuitivas e responsivas. Vamos explorar os mais importantes para dashboards:

Inputs: Coletando Informações do Usuário

Os inputs são os componentes que permitem ao usuário interagir com sua aplicação. Todos seguem o padrão ui.input_*() e requerem um id único:

from shiny.express import input, render, ui
import datetime

# Layout em colunas para organizar os inputs
with ui.layout_columns(col_widths=[6, 6]):
    # Coluna 1: Inputs básicos
    with ui.card():
        ui.card_header("Inputs Básicos")
        
        ui.input_text("texto", "Campo de texto:", placeholder="Digite aqui...")
        ui.input_numeric("numero", "Entrada numérica:", value=100, min=0, max=1000)
        ui.input_slider("slider", "Controle deslizante:", min=0, max=100, value=50)
        ui.input_date("data", "Seletor de data:", value=datetime.date.today())
    
    # Coluna 2: Seleções e botões
    with ui.card():
        ui.card_header("Seleções e Controles")
        
        ui.input_selectize("opcao", "Lista de seleção:", 
                          choices=["Opção A", "Opção B", "Opção C"], 
                          selected="Opção A")
        ui.input_checkbox_group("grupo", "Múltipla escolha:",
                               choices=["Item 1", "Item 2", "Item 3"],
                               selected=["Item 1"])
        ui.input_radio_buttons("radio", "Botões de rádio:",
                              choices=["Sim", "Não"], inline=True)
        ui.input_action_button("botao", "Executar Ação", class_="btn-primary")

# Área de resultado
with ui.card():
    ui.card_header("Valores Selecionados")
    
    @render.ui
    def valores_atuais():
        return ui.div(
            ui.p(f"Texto: {input.texto()}"),
            ui.p(f"Número: {input.numero()}"),
            ui.p(f"Slider: {input.slider()}"),
            ui.p(f"Data: {input.data()}"),
            ui.p(f"Seleção: {input.opcao()}"),
            ui.p(f"Grupo: {', '.join(input.grupo())}"),
            ui.p(f"Rádio: {input.radio()}"),
            ui.p(f"Botão clicado: {input.botao()} vezes")
        )
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 860px

from shiny.express import input, render, ui
import datetime

# Layout em colunas para organizar os inputs
with ui.layout_columns(col_widths=[6, 6]):
    # Coluna 1: Inputs básicos
    with ui.card():
        ui.card_header("Inputs Básicos")
        
        ui.input_text("texto", "Campo de texto:", placeholder="Digite aqui...")
        ui.input_numeric("numero", "Entrada numérica:", value=100, min=0, max=1000)
        ui.input_slider("slider", "Controle deslizante:", min=0, max=100, value=50)
        ui.input_date("data", "Seletor de data:", value=datetime.date.today())
    
    # Coluna 2: Seleções e botões
    with ui.card():
        ui.card_header("Seleções e Controles")
        
        ui.input_selectize("opcao", "Lista de seleção:", 
                          choices=["Opção A", "Opção B", "Opção C"], 
                          selected="Opção A")
        ui.input_checkbox_group("grupo", "Múltipla escolha:",
                               choices=["Item 1", "Item 2", "Item 3"],
                               selected=["Item 1"])
        ui.input_radio_buttons("radio", "Botões de rádio:",
                              choices=["Sim", "Não"], inline=True)
        ui.input_action_button("botao", "Executar Ação", class_="btn-primary")

# Área de resultado
with ui.card():
    ui.card_header("Valores Selecionados")
    
    @render.ui
    def valores_atuais():
        return ui.div(
            ui.p(f"Texto: {input.texto()}"),
            ui.p(f"Número: {input.numero()}"),
            ui.p(f"Slider: {input.slider()}"),
            ui.p(f"Data: {input.data()}"),
            ui.p(f"Seleção: {input.opcao()}"),
            ui.p(f"Grupo: {', '.join(input.grupo())}"),
            ui.p(f"Rádio: {input.radio()}"),
            ui.p(f"Botão clicado: {input.botao()} vezes")
        )

Layouts: Organizando o Espaço

Layout com Sidebar

O layout sidebar é ideal para dashboards, colocando controles na lateral e conteúdo principal no centro:

from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np

# Dados simulados
np.random.seed(42)
data = pd.DataFrame({
    'categoria': np.repeat(['A', 'B', 'C', 'D'], 50),
    'valor': np.random.normal(100, 20, 200),
    'quantidade': np.random.poisson(5, 200)
})

with ui.sidebar(open="desktop"):
    ui.h3("Controles")
    ui.input_selectize("categoria_sel", "Categoria:", 
                      choices=list(data['categoria'].unique()),
                      multiple=True,
                      selected=list(data['categoria'].unique()))
    ui.input_slider("valor_min", "Valor mínimo:", 
                   min=int(data['valor'].min()), 
                   max=int(data['valor'].max()), 
                   value=int(data['valor'].min()))

@reactive.calc
def dados_filtrados():
    df_filtrado = data[data['categoria'].isin(input.categoria_sel())]
    df_filtrado = df_filtrado[df_filtrado['valor'] >= input.valor_min()]
    return df_filtrado

with ui.card(full_screen=True):
    ui.card_header("Distribuição por Categoria")
    
    @render_widget
    def grafico_distribuicao():
        dados = dados_filtrados()
        
        return alt.Chart(dados).mark_boxplot().encode(
            x=alt.X('categoria:N', title='Categoria'),
            y=alt.Y('valor:Q', title='Valor'),
            color=alt.Color('categoria:N', legend=None)
        ).properties(
            width=400,
            height=300
        )
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 500px

from shiny import reactive
from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np

ui.tags.style("""
.forward-fill-potential > * {
    display: flex;
    flex-direction: column;
    flex: 1 1 350px !important;
    min-height: 0;
    min-width: 0;
    width: 100%;
}
@media (min-width: 576px) {
    .container-sm, .container {
        max-width: none;
    }
}
""")

# Dados simulados
np.random.seed(42)
data = pd.DataFrame({
    'categoria': np.repeat(['A', 'B', 'C', 'D'], 50),
    'valor': np.random.normal(100, 20, 200),
    'quantidade': np.random.poisson(5, 200)
})

with ui.sidebar(open="desktop"):
    ui.h3("Controles")
    ui.input_selectize("categoria_sel", "Categoria:", 
                      choices=list(data['categoria'].unique()),
                      multiple=True,
                      selected=list(data['categoria'].unique()))
    ui.input_slider("valor_min", "Valor mínimo:", 
                   min=int(data['valor'].min()), 
                   max=int(data['valor'].max()), 
                   value=int(data['valor'].min()))

@reactive.calc
def dados_filtrados():
    df_filtrado = data[data['categoria'].isin(input.categoria_sel())]
    df_filtrado = df_filtrado[df_filtrado['valor'] >= input.valor_min()]
    return df_filtrado

with ui.card(full_screen=True):
    ui.card_header("Distribuição por Categoria")
    
    @render_widget
    def grafico_distribuicao():
        dados = dados_filtrados()
        
        return alt.Chart(dados).mark_boxplot().encode(
            x=alt.X('categoria:N', title='Categoria'),
            y=alt.Y('valor:Q', title='Valor'),
            color=alt.Color('categoria:N', legend=None)
        ).properties(
            width=400,
            height=300
        )

Cards e Value Boxes para KPIs

Value boxes são perfeitos para destacar métricas importantes:

from shiny.express import input, render, ui
import pandas as pd
import numpy as np

# Dados simulados do SUS
np.random.seed(42)
dados_sus = pd.DataFrame({
    'mes': pd.date_range('2024-01-01', periods=12, freq='M'),
    'atendimentos': np.random.normal(15000, 2000, 12),
    'consultas_especializadas': np.random.normal(5000, 800, 12),
    'procedimentos': np.random.normal(3000, 500, 12)
})

# Layout com 3 value boxes simples
with ui.layout_columns(col_widths=[4, 4, 4]):
    
    with ui.card():
        ui.h4("Atendimentos Totais", style="font-size: 0.5rem; text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_atendimentos():
            total = dados_sus['atendimentos'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(últimos 12 meses)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Consultas Especializadas", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_consultas():
            total = dados_sus['consultas_especializadas'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(cardiologia, neurologia, etc.)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Procedimentos Realizados", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_procedimentos():
            total = dados_sus['procedimentos'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(cirurgias e exames)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )

# Gráfico de tendência
with ui.card(full_screen=True):
    ui.card_header("Evolução dos Atendimentos")
    
    @render.plot
    def grafico_atendimentos():
        import matplotlib.pyplot as plt
        
        fig, ax = plt.subplots(figsize=(10, 4))
        ax.plot(dados_sus['mes'], dados_sus['atendimentos'], marker='o', linewidth=2, color='#2E86C1')
        ax.set_title('Evolução Mensal dos Atendimentos no SUS')
        ax.set_xlabel('Mês')
        ax.set_ylabel('Número de Atendimentos')
        ax.grid(True, alpha=0.3)
        
        # Formatação do eixo Y
        ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x/1000:.0f}K'))
        
        plt.tight_layout()
        return fig
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 660px

from shiny.express import input, render, ui
import pandas as pd
import numpy as np

# Dados simulados do SUS
np.random.seed(42)
dados_sus = pd.DataFrame({
    'mes': pd.date_range('2024-01-01', periods=12, freq='M'),
    'atendimentos': np.random.normal(15000, 2000, 12),
    'consultas_especializadas': np.random.normal(5000, 800, 12),
    'procedimentos': np.random.normal(3000, 500, 12)
})

# Layout com 3 value boxes simples
with ui.layout_columns(col_widths=[4, 4, 4]):
    
    with ui.card(class_="gap-0"):
        ui.h4("Atendimentos Totais", style="font-size: 1rem; text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_atendimentos():
            total = dados_sus['atendimentos'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(últimos 12 meses)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Consultas Especializadas", style="font-size: 1rem; text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_consultas():
            total = dados_sus['consultas_especializadas'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(cardiologia, neurologia, etc.)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Procedimentos Realizados", style="font-size: 1rem; text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def total_procedimentos():
            total = dados_sus['procedimentos'].sum()
            return ui.div(
                ui.strong(f"{total:,.0f}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(cirurgias e exames)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )

# Gráfico de tendência
with ui.card(full_screen=True):
    ui.card_header("Evolução dos Atendimentos")
    
    @render.plot
    def grafico_atendimentos():
        import matplotlib.pyplot as plt
        
        fig, ax = plt.subplots(figsize=(10, 4))
        ax.plot(dados_sus['mes'], dados_sus['atendimentos'], marker='o', linewidth=2, color='#2E86C1')
        ax.set_title('Evolução Mensal dos Atendimentos no SUS')
        ax.set_xlabel('Mês')
        ax.set_ylabel('Número de Atendimentos')
        ax.grid(True, alpha=0.3)
        
        # Formatação do eixo Y
        ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x/1000:.0f}K'))
        
        plt.tight_layout()
        return fig

Padrões Reativos Avançados

Controlando Atualizações com Eventos

Para dashboards mais sofisticados, você frequentemente quer controlar quando as atualizações acontecem, não apenas o que é atualizado. O Shiny oferece ferramentas poderosas para isso:

Botões de Ação e Processamento Controlado

Use @reactive.event para criar dashboards onde processamentos pesados só acontecem quando o usuário solicita:

from shiny import reactive
from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np
import time

with ui.layout_columns(col_widths=[3, 9]):
    with ui.card():
        ui.card_header("Configurações")
        ui.input_numeric("n_amostras", "Número de amostras:", value=1000, min=100, max=10000)
        ui.input_selectize("distribuicao", "Tipo de distribuição:", 
                          choices=["Normal", "Exponencial", "Uniforme"])
        ui.input_action_button("gerar", "🎲 Gerar Dados", class_="btn-primary")
        ui.input_action_button("analisar", "📊 Executar Análise", class_="btn-success")
    
    with ui.card(full_screen=True):
        ui.card_header("Resultados da Análise")
        
        @render.text
        @reactive.event(input.gerar)
        def status_dados():
            return f"✅ {input.n_amostras()} amostras da distribuição {input.distribuicao()} geradas!"
        
        @render_widget
        @reactive.event(input.analisar)
        def grafico_analise():
            # Simula processamento que demora
            time.sleep(0.5)
            
            # Gera dados baseado nas configurações
            np.random.seed(42)
            if input.distribuicao() == "Normal":
                dados = np.random.normal(0, 1, input.n_amostras())
            elif input.distribuicao() == "Exponencial":
                dados = np.random.exponential(1, input.n_amostras())
            else:  # Uniforme
                dados = np.random.uniform(-2, 2, input.n_amostras())
            
            df = pd.DataFrame({'valores': dados})
            
            return alt.Chart(df).mark_bar().encode(
                x=alt.X('valores:Q', bin=alt.Bin(maxbins=30), title='Valor'),
                y=alt.Y('count()', title='Frequência'),
                color=alt.value('#2E86C1')
            ).properties(width=600, height=300)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 550px

from shiny import reactive
from shiny.express import input, render, ui
from shinywidgets import render_widget
import altair as alt
import pandas as pd
import numpy as np
import time

with ui.layout_columns(col_widths=[3, 9]):
    with ui.card():
        ui.card_header("Configurações")
        ui.input_numeric("n_amostras", "Número de amostras:", value=1000, min=100, max=10000)
        ui.input_selectize("distribuicao", "Tipo de distribuição:", 
                          choices=["Normal", "Exponencial", "Uniforme"])
        ui.input_action_button("gerar", "🎲 Gerar Dados", class_="btn-primary")
        ui.input_action_button("analisar", "📊 Executar Análise", class_="btn-success")
    
    with ui.card(full_screen=True):
        ui.card_header("Resultados da Análise")
        
        @render.text
        @reactive.event(input.gerar)
        def status_dados():
            return f"✅ {input.n_amostras()} amostras da distribuição {input.distribuicao()} geradas!"
        
        @render_widget
        @reactive.event(input.analisar)
        def grafico_analise():
            # Simula processamento que demora
            time.sleep(0.5)
            
            # Gera dados baseado nas configurações
            np.random.seed(42)
            if input.distribuicao() == "Normal":
                dados = np.random.normal(0, 1, input.n_amostras())
            elif input.distribuicao() == "Exponencial":
                dados = np.random.exponential(1, input.n_amostras())
            else:  # Uniforme
                dados = np.random.uniform(-2, 2, input.n_amostras())
            
            df = pd.DataFrame({'valores': dados})
            
            return alt.Chart(df).mark_bar().encode(
                x=alt.X('valores:Q', bin=alt.Bin(maxbins=30), title='Valor'),
                y=alt.Y('count()', title='Frequência'),
                color=alt.value('#2E86C1')
            ).properties(width=600, height=300)

Validação de Inputs com req()

Para evitar erros quando inputs ainda não estão prontos, use req() para parar a execução até que condições sejam atendidas:

from shiny import reactive, req
from shiny.express import input, render, ui
import pandas as pd

ui.input_text("nome_usuario", "Digite seu nome:", placeholder="Insira seu nome aqui...")
ui.input_numeric("idade", "Sua idade:", value=None, min=0, max=120)
ui.input_selectize("cidade", "Sua cidade:", choices=["São Paulo", "Rio de Janeiro", "Belo Horizonte"], selected=None)

@render.text
def perfil_usuario():
    req(input.nome_usuario())  # Só continua se nome foi digitado (não vazio)
    req(input.idade())         # Só continua se idade foi informada
    req(input.cidade())        # Só continua se cidade foi selecionada
    
    return f"""
    Perfil do usuário:
    Nome: {input.nome_usuario()}
    Idade: {input.idade()} anos
    Cidade: {input.cidade()}
    """

@render.text
def validacao_status():
    status = []
    
    if input.nome_usuario():
        status.append("✅ Nome preenchido")
    else:
        status.append("❌ Nome necessário")
    
    if input.idade() and input.idade() > 0:
        status.append("✅ Idade válida")
    else:
        status.append("❌ Idade necessária")
    
    if input.cidade():
        status.append("✅ Cidade selecionada")
    else:
        status.append("❌ Cidade necessária")
    
    return "\n".join(status)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 350px

from shiny import reactive, req
from shiny.express import input, render, ui
import pandas as pd

ui.input_text("nome_usuario", "Digite seu nome:", placeholder="Insira seu nome aqui...")
ui.input_numeric("idade", "Sua idade:", value=None, min=0, max=120)
ui.input_selectize("cidade", "Sua cidade:", choices=["São Paulo", "Rio de Janeiro", "Belo Horizonte"], selected=None)

@render.text
def perfil_usuario():
    req(input.nome_usuario())  # Só continua se nome foi digitado (não vazio)
    req(input.idade())         # Só continua se idade foi informada
    req(input.cidade())        # Só continua se cidade foi selecionada
    
    return f"""
    Perfil do usuário:
    Nome: {input.nome_usuario()}
    Idade: {input.idade()} anos
    Cidade: {input.cidade()}
    """

@render.text
def validacao_status():
    status = []
    
    if input.nome_usuario():
        status.append("✅ Nome preenchido")
    else:
        status.append("❌ Nome necessário")
    
    if input.idade() and input.idade() > 0:
        status.append("✅ Idade válida")
    else:
        status.append("❌ Idade necessária")
    
    if input.cidade():
        status.append("✅ Cidade selecionada")
    else:
        status.append("❌ Cidade necessária")
    
    return "\n".join(status)

Valores Reativos: Mantendo Estado

Para armazenar estado que pode mudar durante a execução da aplicação, use reactive.value():

from shiny import reactive
from shiny.express import input, render, ui

ui.h2("Histórico de Interações")
ui.input_slider("valor", "Ajuste o valor:", min=0, max=100, value=50)
ui.input_action_button("salvar", "💾 Salvar Valor", class_="btn-primary")
ui.input_action_button("limpar", "🗑️ Limpar Histórico", class_="btn-warning")

# Valor reativo para armazenar histórico
historico = reactive.value([])

@reactive.effect
@reactive.event(input.salvar)
def salvar_valor():
    valores_atuais = list(historico())  # Cria uma nova lista
    valores_atuais.append(input.valor())
    historico.set(valores_atuais)

@reactive.effect  
@reactive.event(input.limpar)
def limpar_historico():
    historico.set([])

@render.ui
def mostrar_historico():
    valores = historico()
    if valores:
        return ui.div([ui.p(f"Valor {i+1}: {v}") for i, v in enumerate(valores)])
    else:
        return ui.p("Nenhum valor salvo ainda...")

@render.text
def estatisticas():
    valores = historico()
    if valores:
        return f"Total: {len(valores)} | Média: {sum(valores)/len(valores):.1f}"
    else:
        return "Adicione valores para ver estatísticas"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| layout: vertical  
##| viewerHeight: 400px

from shiny import reactive
from shiny.express import input, render, ui

ui.h2("Histórico de Interações")
ui.input_slider("valor", "Ajuste o valor:", min=0, max=100, value=50)
ui.input_action_button("salvar", "💾 Salvar Valor", class_="btn-primary")
ui.input_action_button("limpar", "🗑️ Limpar Histórico", class_="btn-warning")

# Valor reativo para armazenar histórico
historico = reactive.value([])

@reactive.effect
@reactive.event(input.salvar)
def salvar_valor():
    valores_atuais = list(historico())  # Cria uma nova lista
    valores_atuais.append(input.valor())
    historico.set(valores_atuais)

@reactive.effect  
@reactive.event(input.limpar)
def limpar_historico():
    historico.set([])

@render.ui
def mostrar_historico():
    valores = historico()
    if valores:
        return ui.div([ui.p(f"Valor {i+1}: {v}") for i, v in enumerate(valores)])
    else:
        return ui.p("Nenhum valor salvo ainda...")

@render.text
def estatisticas():
    valores = historico()
    if valores:
        return f"Total: {len(valores)} | Média: {sum(valores)/len(valores):.1f}"
    else:
        return "Adicione valores para ver estatísticas"

Trabalhando com Dados e Tabelas

Tabelas Interativas com @render.data_frame

Para dashboards que trabalham com dados tabulares, o Shiny oferece componentes poderosos para exibir e filtrar tabelas:

from shiny.express import input, render, ui
import pandas as pd
import numpy as np

# Dados simulados do SUS
np.random.seed(42)
sus_data = pd.DataFrame({
    'especialidade': np.random.choice(['Cardiologia', 'Neurologia', 'Ortopedia', 'Pediatria'], 200),
    'regiao': np.random.choice(['Norte', 'Sul', 'Leste', 'Oeste'], 200),
    'atendimentos': np.random.normal(100, 30, 200).round(0).astype(int),
    'data': pd.date_range('2024-01-01', periods=200, freq='D')[:200]
})

with ui.layout_columns(col_widths=[3, 9]):
    with ui.card():
        ui.card_header("Filtros")
        ui.input_selectize("especialidade_filtro", "Especialidades:", 
                          choices=list(sus_data['especialidade'].unique()),
                          multiple=True, 
                          selected=list(sus_data['especialidade'].unique()))
        ui.input_date_range("data_filtro", "Período:", 
                           start="2024-01-01",
                           end="2024-07-18")
        ui.input_slider("atendimentos_min", "Atendimentos mínimos:", 
                       min=int(sus_data['atendimentos'].min()),
                       max=int(sus_data['atendimentos'].max()),
                       value=int(sus_data['atendimentos'].min()))
    
    with ui.card():
        ui.card_header("Dados Filtrados")
        
        @render.data_frame
        def tabela_sus():
            # Aplica filtros
            df_filtrado = sus_data[
                (sus_data['especialidade'].isin(input.especialidade_filtro())) &
                (sus_data['data'] >= pd.to_datetime(input.data_filtro()[0])) &
                (sus_data['data'] <= pd.to_datetime(input.data_filtro()[1])) &
                (sus_data['atendimentos'] >= input.atendimentos_min())
            ]
            
            return render.DataGrid(
                df_filtrado,
                filters=True,  # Permite filtros adicionais na tabela
                selection_mode="rows"  # Permite seleção de linhas
            )

# Mostra estatísticas dos dados selecionados
with ui.card():
    ui.card_header("Resumo dos Dados Filtrados")
    
    @render.text
    def resumo_filtrado():
        # Recria o mesmo filtro
        df_filtrado = sus_data[
            (sus_data['especialidade'].isin(input.especialidade_filtro())) &
            (sus_data['data'] >= pd.to_datetime(input.data_filtro()[0])) &
            (sus_data['data'] <= pd.to_datetime(input.data_filtro()[1])) &
            (sus_data['atendimentos'] >= input.atendimentos_min())
        ]
        
        if len(df_filtrado) > 0:
            return f"""
            📊 Total de registros: {len(df_filtrado)}
            🏥 Atendimentos totais: {df_filtrado['atendimentos'].sum():,}
            📈 Média de atendimentos: {df_filtrado['atendimentos'].mean():.1f}
            🏆 Maior número: {df_filtrado['atendimentos'].max()}
            """
        else:
            return "Nenhum dado encontrado com os filtros aplicados."
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 760px

from shiny.express import input, render, ui
import pandas as pd
import numpy as np

# Dados simulados do SUS
np.random.seed(42)
sus_data = pd.DataFrame({
    'especialidade': np.random.choice(['Cardiologia', 'Neurologia', 'Ortopedia', 'Pediatria'], 200),
    'regiao': np.random.choice(['Norte', 'Sul', 'Leste', 'Oeste'], 200),
    'atendimentos': np.random.normal(100, 30, 200).round(0).astype(int),
    'data': pd.date_range('2024-01-01', periods=200, freq='D')[:200]
})

with ui.layout_columns(col_widths=[3, 9]):
    with ui.card():
        ui.card_header("Filtros")
        ui.input_selectize("especialidade_filtro", "Especialidades:", 
                          choices=list(sus_data['especialidade'].unique()),
                          multiple=True, 
                          selected=list(sus_data['especialidade'].unique()))
        ui.input_date_range("data_filtro", "Período:", 
                           start="2024-01-01",
                           end="2024-07-18")
        ui.input_slider("atendimentos_min", "Atendimentos mínimos:", 
                       min=int(sus_data['atendimentos'].min()),
                       max=int(sus_data['atendimentos'].max()),
                       value=int(sus_data['atendimentos'].min()))
    
    with ui.card():
        ui.card_header("Dados Filtrados")
        
        @render.data_frame
        def tabela_sus():
            # Aplica filtros
            df_filtrado = sus_data[
                (sus_data['especialidade'].isin(input.especialidade_filtro())) &
                (sus_data['data'] >= pd.to_datetime(input.data_filtro()[0])) &
                (sus_data['data'] <= pd.to_datetime(input.data_filtro()[1])) &
                (sus_data['atendimentos'] >= input.atendimentos_min())
            ]
            
            return render.DataGrid(
                df_filtrado,
                filters=True,  # Permite filtros adicionais na tabela
                selection_mode="rows"  # Permite seleção de linhas
            )

# Mostra estatísticas dos dados selecionados
with ui.card():
    ui.card_header("Resumo dos Dados Filtrados")
    
    @render.text
    def resumo_filtrado():
        # Recria o mesmo filtro
        df_filtrado = sus_data[
            (sus_data['especialidade'].isin(input.especialidade_filtro())) &
            (sus_data['data'] >= pd.to_datetime(input.data_filtro()[0])) &
            (sus_data['data'] <= pd.to_datetime(input.data_filtro()[1])) &
            (sus_data['atendimentos'] >= input.atendimentos_min())
        ]
        
        if len(df_filtrado) > 0:
            return f"""
            📊 Total de registros: {len(df_filtrado)}
            🏥 Atendimentos totais: {df_filtrado['atendimentos'].sum():,}
            📈 Média de atendimentos: {df_filtrado['atendimentos'].mean():.1f}
            🏆 Maior número: {df_filtrado['atendimentos'].max()}
            """
        else:
            return "Nenhum dado encontrado com os filtros aplicados."

Interfaces Dinâmicas com @render.ui

Para dashboards que precisam adaptar a interface baseado nos dados ou inputs do usuário, use @render.ui:

from shiny.express import input, render, ui
import pandas as pd

ui.input_selectize("tipo_analise", "Tipo de Análise:", 
                  choices=["Simples", "Avançada", "Comparativa"])

@render.ui
def interface_dinamica():
    if input.tipo_analise() == "Simples":
        return ui.div(
            ui.input_slider("valor_simples", "Valor:", min=0, max=100, value=50),
            ui.h4("Análise Simples Ativada")
        )
    elif input.tipo_analise() == "Avançada":
        return ui.div(
            ui.input_numeric("n_simulacoes", "Número de simulações:", value=1000),
            ui.input_selectize("metodo", "Método:", choices=["Monte Carlo", "Bootstrap"]),
            ui.input_checkbox("incluir_intervalo", "Incluir intervalo de confiança", value=True),
            ui.h4("Análise Avançada Configurada")
        )
    else:  # Comparativa
        return ui.div(
            ui.input_selectize("grupo_a", "Grupo A:", choices=["Dataset 1", "Dataset 2"]),
            ui.input_selectize("grupo_b", "Grupo B:", choices=["Dataset 1", "Dataset 2"]),
            ui.input_selectize("teste_estatistico", "Teste:", choices=["t-test", "Mann-Whitney"]),
            ui.h4("Análise Comparativa Pronta")
        )

@render.text
def resultado_dinamico():
    if input.tipo_analise() == "Simples":
        if hasattr(input, 'valor_simples') and input.valor_simples():
            return f"Resultado simples: {input.valor_simples() * 2}"
    elif input.tipo_analise() == "Avançada":
        if hasattr(input, 'n_simulacoes') and input.n_simulacoes():
            return f"Executando {input.n_simulacoes()} simulações..."
    else:
        return "Configure os grupos para comparação"
    
    return "Aguardando configuração..."
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 450px

from shiny.express import input, render, ui
import pandas as pd

ui.input_selectize("tipo_analise", "Tipo de Análise:", 
                  choices=["Simples", "Avançada", "Comparativa"])

@render.ui
def interface_dinamica():
    if input.tipo_analise() == "Simples":
        return ui.div(
            ui.input_slider("valor_simples", "Valor:", min=0, max=100, value=50),
            ui.h4("Análise Simples Ativada")
        )
    elif input.tipo_analise() == "Avançada":
        return ui.div(
            ui.input_numeric("n_simulacoes", "Número de simulações:", value=1000),
            ui.input_selectize("metodo", "Método:", choices=["Monte Carlo", "Bootstrap"]),
            ui.input_checkbox("incluir_intervalo", "Incluir intervalo de confiança", value=True),
            ui.h4("Análise Avançada Configurada")
        )
    else:  # Comparativa
        return ui.div(
            ui.input_selectize("grupo_a", "Grupo A:", choices=["Dataset 1", "Dataset 2"]),
            ui.input_selectize("grupo_b", "Grupo B:", choices=["Dataset 1", "Dataset 2"]),
            ui.input_selectize("teste_estatistico", "Teste:", choices=["t-test", "Mann-Whitney"]),
            ui.h4("Análise Comparativa Pronta")
        )

@render.text
def resultado_dinamico():
    if input.tipo_analise() == "Simples":
        if hasattr(input, 'valor_simples') and input.valor_simples():
            return f"Resultado simples: {input.valor_simples() * 2}"
    elif input.tipo_analise() == "Avançada":
        if hasattr(input, 'n_simulacoes') and input.n_simulacoes():
            return f"Executando {input.n_simulacoes()} simulações..."
    else:
        return "Configure os grupos para comparação"
    
    return "Aguardando configuração..."

UI Condicional e Dinâmica

Para dashboards mais sofisticados, você pode mostrar/esconder elementos baseado na interação do usuário:

from shiny import reactive
from shiny.express import input, render, ui
import pandas as pd

ui.h2("Interface Adaptativa")

ui.input_selectize("modo_dashboard", "Modo do Dashboard:",
                  choices=["Básico", "Avançado", "Executivo"])

# UI condicional baseada em JavaScript
with ui.panel_conditional("input.modo_dashboard === 'Básico'"):
    ui.h4("🟢 Modo Básico Ativo")
    ui.input_slider("valor_basico", "Configuração simples:", min=0, max=100, value=50)

with ui.panel_conditional("input.modo_dashboard === 'Avançado'"):
    ui.h4("🟡 Modo Avançado Ativo")
    ui.input_numeric("iteracoes", "Número de iterações:", value=1000)
    ui.input_selectize("algoritmo", "Algoritmo:", choices=["A", "B", "C"])

with ui.panel_conditional("input.modo_dashboard === 'Executivo'"):
    ui.h4("🔴 Modo Executivo Ativo")
    ui.p("Apenas visualizações de alto nível e KPIs")

# Atualizando inputs programaticamente
ui.input_selectize("categoria", "Categoria:", choices=["Vendas", "Marketing", "Operações"])
ui.input_selectize("subcategoria", "Subcategoria:", choices=[])

@reactive.effect
def atualizar_subcategorias():
    opcoes = {
        "Vendas": ["Receita", "Comissões", "Metas"],
        "Marketing": ["Campanhas", "ROI", "Leads"],
        "Operações": ["Custos", "Eficiência", "Qualidade"]
    }
    
    ui.update_selectize("subcategoria", 
                       choices=opcoes.get(input.categoria(), []))

@render.text
def resultado_configuracao():
    if input.categoria() and input.subcategoria():
        return f"Analisando: {input.categoria()}{input.subcategoria()}"
    return "Selecione categoria e subcategoria"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 500px

from shiny import reactive
from shiny.express import input, render, ui
import pandas as pd

ui.h2("Interface Adaptativa")

ui.input_selectize("modo_dashboard", "Modo do Dashboard:",
                  choices=["Básico", "Avançado", "Executivo"])

# UI condicional baseada em JavaScript
with ui.panel_conditional("input.modo_dashboard === 'Básico'"):
    ui.h4("🟢 Modo Básico Ativo")
    ui.input_slider("valor_basico", "Configuração simples:", min=0, max=100, value=50)

with ui.panel_conditional("input.modo_dashboard === 'Avançado'"):
    ui.h4("🟡 Modo Avançado Ativo")
    ui.input_numeric("iteracoes", "Número de iterações:", value=1000)
    ui.input_selectize("algoritmo", "Algoritmo:", choices=["A", "B", "C"])

with ui.panel_conditional("input.modo_dashboard === 'Executivo'"):
    ui.h4("🔴 Modo Executivo Ativo")
    ui.p("Apenas visualizações de alto nível e KPIs")

# Atualizando inputs programaticamente
ui.input_selectize("categoria", "Categoria:", choices=["Vendas", "Marketing", "Operações"])
ui.input_selectize("subcategoria", "Subcategoria:", choices=[])

@reactive.effect
def atualizar_subcategorias():
    opcoes = {
        "Vendas": ["Receita", "Comissões", "Metas"],
        "Marketing": ["Campanhas", "ROI", "Leads"],
        "Operações": ["Custos", "Eficiência", "Qualidade"]
    }
    
    ui.update_selectize("subcategoria", 
                       choices=opcoes.get(input.categoria(), []))

@render.text
def resultado_configuracao():
    if input.categoria() and input.subcategoria():
        return f"Analisando: {input.categoria()} → {input.subcategoria()}"
    return "Selecione categoria e subcategoria"

Feedback Visual para o Usuário

Para melhor experiência do usuário, forneça feedback visual durante operações:

from shiny import reactive
from shiny.express import input, render, ui
import asyncio

ui.input_action_button("processar_dados", "🚀 Processar Dados", class_="btn-primary")
ui.input_action_button("mostrar_notificacao", "💬 Mostrar Notificação", class_="btn-info")

@reactive.effect
@reactive.event(input.processar_dados)
async def processar_com_progresso():
    # Cria barra de progresso
    with ui.Progress(min=0, max=10) as progress:
        progress.set(0, message="Iniciando processamento...")
        
        for i in range(10):
            await asyncio.sleep(0.3)  # Simula processamento
            progress.set(i + 1, 
                        message=f"Processando etapa {i + 1}/10",
                        detail=f"Completando análise...")
    
    # Notificação de sucesso
    ui.notification_show(
        "✅ Processamento concluído com sucesso!",
        type="success",
        duration=3
    )

@reactive.effect  
@reactive.event(input.mostrar_notificacao)
def mostrar_notificacoes():
    ui.notification_show("ℹ️ Esta é uma informação", type="message")
    ui.notification_show("⚠️ Este é um aviso", type="warning")
    ui.notification_show("❌ Este é um erro", type="error")

@render.text
def status():
    return f"Botão processar clicado: {input.processar_dados()} vezes"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 500px

from shiny import reactive
from shiny.express import input, render, ui
import asyncio

ui.input_action_button("processar_dados", "🚀 Processar Dados", class_="btn-primary")
ui.input_action_button("mostrar_notificacao", "💬 Mostrar Notificação", class_="btn-info")

@reactive.effect
@reactive.event(input.processar_dados)
async def processar_com_progresso():
    # Cria barra de progresso
    with ui.Progress(min=0, max=10) as progress:
        progress.set(0, message="Iniciando processamento...")
        
        for i in range(10):
            await asyncio.sleep(0.3)  # Simula processamento
            progress.set(i + 1, 
                        message=f"Processando etapa {i + 1}/10",
                        detail=f"Completando análise...")
    
    # Notificação de sucesso
    ui.notification_show(
        "✅ Processamento concluído com sucesso!",
        type="success",
        duration=3
    )

@reactive.effect  
@reactive.event(input.mostrar_notificacao)
def mostrar_notificacoes():
    ui.notification_show("ℹ️ Esta é uma informação", type="message")
    ui.notification_show("⚠️ Este é um aviso", type="warning")
    ui.notification_show("❌ Este é um erro", type="error")

@render.text
def status():
    return f"Botão processar clicado: {input.processar_dados()} vezes"

Dashboards em Tempo Real

Para dashboards que precisam atualizar automaticamente (sem interação do usuário), use reactive.invalidate_later():

from shiny import reactive
from shiny.express import input, render, ui
from datetime import datetime
import pandas as pd
import numpy as np

with ui.layout_columns(col_widths=[4, 4, 4]):
    
    with ui.card():
        ui.h4("Hora Atual", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def hora_atual():
            reactive.invalidate_later(1)  # Atualiza a cada 1 segundo
            return ui.div(
                ui.strong(datetime.now().strftime("%H:%M:%S"), style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(tempo real)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Atendimentos Agora", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def valor_aleatorio():
            reactive.invalidate_later(3)  # Atualiza a cada 3 segundos
            return ui.div(
                ui.strong(f"{np.random.randint(100, 999)}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(simulação ao vivo)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Status Sistema", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def status_sistema():
            reactive.invalidate_later(5)  # Atualiza a cada 5 segundos
            status = np.random.choice(["🟢 Online", "🟡 Lento", "🔴 Offline"], p=[0.8, 0.15, 0.05])
            return ui.div(
                ui.strong(status, style="font-size: 1.5rem; text-align: center; display: block;"),
                ui.p("(monitoramento)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )

@render.plot
def grafico_tempo_real():
    reactive.invalidate_later(2)  # Atualiza a cada 2 segundos
    
    # Simula dados que mudam com o tempo
    import matplotlib.pyplot as plt
    
    x = list(range(10))
    y = [np.random.randint(1, 20) for _ in range(10)]
    
    fig, ax = plt.subplots(figsize=(8, 4))
    ax.plot(x, y, marker='o', linewidth=2, color='#E74C3C')
    ax.set_title('Dados em Tempo Real')
    ax.set_xlabel('Tempo')
    ax.set_ylabel('Valor')
    ax.grid(True, alpha=0.3)
    
    return fig
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 600px

from shiny import reactive
from shiny.express import input, render, ui
from datetime import datetime
import pandas as pd
import numpy as np

with ui.layout_columns(col_widths=[4, 4, 4]):
    
    with ui.card():
        ui.h4("Hora Atual", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def hora_atual():
            reactive.invalidate_later(1)  # Atualiza a cada 1 segundo
            return ui.div(
                ui.strong(datetime.now().strftime("%H:%M:%S"), style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(tempo real)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Atendimentos Agora", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def valor_aleatorio():
            reactive.invalidate_later(3)  # Atualiza a cada 3 segundos
            return ui.div(
                ui.strong(f"{np.random.randint(100, 999)}", style="font-size: 2rem; text-align: center; display: block;"),
                ui.p("(simulação ao vivo)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )
    
    with ui.card():
        ui.h4("Status Sistema", style="text-align: center; margin-bottom: 10px;")
        
        @render.ui
        def status_sistema():
            reactive.invalidate_later(5)  # Atualiza a cada 5 segundos
            status = np.random.choice(["🟢 Online", "🟡 Lento", "🔴 Offline"], p=[0.8, 0.15, 0.05])
            return ui.div(
                ui.strong(status, style="font-size: 1.5rem; text-align: center; display: block;"),
                ui.p("(monitoramento)", style="text-align: center; font-size: 0.9rem; color: #666;")
            )

@render.plot
def grafico_tempo_real():
    reactive.invalidate_later(2)  # Atualiza a cada 2 segundos
    
    # Simula dados que mudam com o tempo
    import matplotlib.pyplot as plt
    
    x = list(range(10))
    y = [np.random.randint(1, 20) for _ in range(10)]
    
    fig, ax = plt.subplots(figsize=(8, 4))
    ax.plot(x, y, marker='o', linewidth=2, color='#E74C3C')
    ax.set_title('Dados em Tempo Real')
    ax.set_xlabel('Tempo')
    ax.set_ylabel('Valor')
    ax.grid(True, alpha=0.3)
    
    return fig
Aviso⚠️ Cuidado com Performance

Use reactive.invalidate_later() com moderação. Atualizações muito frequentes podem sobrecarregar o servidor e a interface do usuário. Para dados críticos, considere intervalos de 5-30 segundos.

Isolamento e Eventos

Use @reactive.event para controlar quando uma função reativa é executada:

from shiny import reactive
from shiny.express import input, render, ui
import time
import asyncio

ui.input_numeric("numero", "Insira um número:", value=10)
ui.input_action_button("processar", "🚀 Processar", class_="btn-success")

@render.text
@reactive.event(input.processar)  # Executa APENAS quando o botão é clicado
async def resultado():
    numero = input.numero()
    
    # Simula processamento longo
    await asyncio.sleep(1)
    
    resultado = numero ** 2
    return f"O quadrado de {numero} é {resultado}"

@render.text
def valor_atual():
    # Este sempre atualiza quando o input muda
    return f"Valor atual: {input.numero()}"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
##| standalone: true
##| components: [viewer]
##| viewerHeight: 200px

from shiny import reactive
from shiny.express import input, render, ui
import asyncio

ui.input_numeric("numero", "Insira um número:", value=10)
ui.input_action_button("processar", "🚀 Processar", class_="btn-success")

@render.text
@reactive.event(input.processar)  # Executa APENAS quando o botão é clicado
async def resultado():
    numero = input.numero()
    
    # Simula processamento longo
    await asyncio.sleep(1)
    
    resultado = numero ** 2
    return f"O quadrado de {numero} é {resultado}"

@render.text
def valor_atual():
    # Este sempre atualiza quando o input muda
    return f"Valor atual: {input.numero()}"

Conclusão

Parabéns! Você dominou os fundamentos para criar dashboards interativos profissionais com Shiny for Python. Esta ferramenta revolucionária democratiza a criação de aplicações web, permitindo que você transforme análises estáticas em experiências dinâmicas e envolventes.

Ao longo deste módulo, você desenvolveu competências essenciais:

Programação reativa - Entendimento do paradigma que conecta inputs e outputs automaticamente
Arquitetura de dashboards - Domínio de layouts, cards, value boxes e componentes profissionais
Controle avançado de fluxo - Uso de @reactive.event, req() e validação para experiências robustas
Interfaces dinâmicas - Criação de UIs condicionais e feedback visual responsivo
Visualizações interativas - Integração fluida com Altair para gráficos profissionais
Monitoramento em tempo real - Implementação de dashboards que atualizam automaticamente

Com essas habilidades, você está equipado para criar desde dashboards simples para análises pessoais até aplicações complexas para sua organização. O Shiny for Python oferece um caminho escalável: comece simples e evolua conforme suas necessidades crescem.

Recursos Adicionais