Visão Geral (interativa)
Interatividade “serverless” (Shinylive): corre no browser e pode ser servido em Cloudflare Pages.
Se o mapa demorar a carregar, é normal na 1.ª vez (download do runtime WebAssembly).
```obyrxxigupflmjsieq #| standalone: true #| components: [editor, viewer] #| viewerHeight: 820
from shiny import App, ui, render, reactive import pandas as pd
Dados (pré-gerados no build)
PREN_PATH = “data/SGCIE_Dashboard_Input_Modelado.xlsx” CENT_PATH = “data/concelhos_centroids.csv”
Leitura (fallback: se o xlsx não estiver acessível em runtime, podes trocar por CSVs compactos)
pren = pd.read_excel(PREN_PATH, sheet_name=“FATOS_PREN”) cent = pd.read_csv(CENT_PATH)
pren[“ano_ref”] = pd.to_numeric(pren[“ano_ref”], errors=“coerce”).astype(“Int64”) pren[“concelho_postal”] = pren[“concelho_postal”].fillna(“—”)
Junta lat/lon
df0 = pren.merge(cent, on=“concelho_postal”, how=“left”)
anos = sorted([int(x) for x in df0[“ano_ref”].dropna().unique()]) metric_choices = { “Nº Operadores”: “n_operadores”, “Nº PREn”: “n_pren”, “Energia primária sob gestão (tep)”: “energia_tep”, “Poupança anual (€)”: “poup_eur”, “Investimento (€)”: “inv_eur”, “ISP estimado 8 anos (€)”: “isp_eur”, }
app_ui = ui.page_fluid( ui.layout_sidebar( ui.sidebar( ui.input_slider(“ano”, “Ano de referência”, min(anos), max(anos), value=max(anos), step=1), ui.input_select(“metric”, “Métrica”, choices=list(metric_choices.keys()), selected=“Energia primária sob gestão (tep)”), ui.input_slider(“topn”, “Top N (barras)”, 5, 30, 15), ui.hr(), ui.p(“Nota: o zoom/pan depende do renderer do browser; a troca automática concelho/distrito fica para a fase 2.”), width=320 ), ui.layout_columns( ui.card( ui.card_header(“Mapa (bolhas por concelho)”), ui.output_ui(“map”), full_screen=True ), ui.card( ui.card_header(“Top concelhos (barras)”), ui.output_ui(“bars”), full_screen=True ), col_widths=(8,4) ), ui.hr(), ui.card( ui.card_header(“Matriz CAE2 × Concelho (desvio à média) — protótipo”), ui.output_ui(“heat”), full_screen=True ) ) )
@reactive.calc def dff(): d = df0[df0[“ano_ref”] == input.ano()].copy() return d
def agg_by_concelho(d: pd.DataFrame): g = d.groupby(“concelho_postal”, dropna=False).agg( n_pren=(“id_pren”, “nunique”), n_operadores=(“operador”, “nunique”) if “operador” in d.columns else (“id_pren”,“nunique”), energia_tep=(“energia_primaria_tep_total”, “sum”), poup_eur=(“poupanca_eur_ano”,“sum”), inv_eur=(“investimento_total_eur”,“sum”), isp_eur=(“isp_isencao_8anos_eur”,“sum”), lat=(“lat”,“first”), lon=(“lon”,“first”), ).reset_index() return g
@output @render.ui def map(): import plotly.express as px
d = agg_by_concelho(dff())
metric = metric_choices[input.metric()]
# Remove linhas sem coordenadas
d = d.dropna(subset=["lat","lon"])
fig = px.scatter_mapbox(
d,
lat="lat", lon="lon",
size=metric,
hover_name="concelho_postal",
hover_data={"n_operadores":True,"n_pren":True,"energia_tep":":.1f","poup_eur":":.0f","inv_eur":":.0f","isp_eur":":.0f", "lat":False, "lon":False},
zoom=5,
height=520
)
fig.update_layout(mapbox_style="open-street-map", margin={"r":0,"t":0,"l":0,"b":0})
return ui.HTML(fig.to_html(include_plotlyjs="cdn", full_html=False))
@output @render.ui def bars(): import plotly.express as px
d = agg_by_concelho(dff())
metric = metric_choices[input.metric()]
d = d.sort_values(metric, ascending=False).head(int(input.topn()))
fig = px.bar(d, x="concelho_postal", y=metric, height=520)
fig.update_layout(xaxis_title="", yaxis_title=input.metric(), margin={"r":0,"t":10,"l":0,"b":0})
return ui.HTML(fig.to_html(include_plotlyjs="cdn", full_html=False))
@output @render.ui def heat(): import plotly.express as px d = dff().copy() # protótipo: energia_tep por CAE2 × concelho d[“cae2”] = d[“cae2”].astype(str) pivot = d.pivot_table(index=“concelho_postal”, columns=“cae2”, values=“energia_primaria_tep_total”, aggfunc=“sum”, fill_value=0)
# desvio à média global (%)
global_mean = pivot.values.mean() if pivot.size else 0
if global_mean == 0:
z = pivot
else:
z = (pivot - global_mean) / global_mean
# limitar dimensões para performance
# top concelhos e top cae2 por energia
top_conc = pivot.sum(axis=1).sort_values(ascending=False).head(30).index
top_cae = pivot.sum(axis=0).sort_values(ascending=False).head(20).index
z = z.loc[top_conc, top_cae]
fig = px.imshow(z, aspect="auto", height=520, color_continuous_scale="RdBu", color_continuous_midpoint=0)
fig.update_layout(xaxis_title="CAE2", yaxis_title="Concelho (Top 30)", margin={"r":0,"t":10,"l":0,"b":0})
return ui.HTML(fig.to_html(include_plotlyjs="cdn", full_html=False))
app = App(app_ui, server=None)