Spaces:
Running
Running
| from __future__ import annotations | |
| import math | |
| import re | |
| import traceback | |
| from datetime import datetime | |
| from html import escape | |
| import branca.colormap as cm | |
| import folium | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from folium import plugins | |
| from scipy import stats | |
| from statsmodels.stats.outliers_influence import OLSInfluence | |
| from app.core.map_layers import ( | |
| add_bairros_layer, | |
| add_indice_marker, | |
| add_popup_pagination_handlers, | |
| add_trabalhos_tecnicos_markers, | |
| add_zoom_responsive_circle_markers, | |
| ) | |
| COR_PRINCIPAL = "#FF8C00" | |
| COR_LINHA = "#dc3545" | |
| def _criar_grafico_obs_calc(y_obs, y_calc, indices=None): | |
| try: | |
| fig = go.Figure() | |
| scatter_args = dict( | |
| x=y_calc, | |
| y=y_obs, | |
| mode="markers", | |
| marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color="black", width=1)), | |
| name="Dados", | |
| ) | |
| if indices is not None: | |
| scatter_args["customdata"] = indices | |
| scatter_args["hovertemplate"] = ( | |
| "<b>Índice:</b> %{customdata}<br><b>Calculado:</b> %{x:.2f}<br>" | |
| "<b>Observado:</b> %{y:.2f}<extra></extra>" | |
| ) | |
| else: | |
| scatter_args["hovertemplate"] = ( | |
| "<b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>" | |
| ) | |
| fig.add_trace(go.Scatter(**scatter_args)) | |
| min_val = min(min(y_obs), min(y_calc)) | |
| max_val = max(max(y_obs), max(y_calc)) | |
| margin = (max_val - min_val) * 0.05 | |
| fig.add_trace( | |
| go.Scatter( | |
| x=[min_val - margin, max_val + margin], | |
| y=[min_val - margin, max_val + margin], | |
| mode="lines", | |
| line=dict(color=COR_LINHA, dash="dash", width=2), | |
| name="Linha de identidade", | |
| ) | |
| ) | |
| fig.update_layout( | |
| title=dict(text="Valores Observados vs Calculados", x=0.5), | |
| xaxis_title="Valores Calculados", | |
| yaxis_title="Valores Observados", | |
| showlegend=True, | |
| plot_bgcolor="white", | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| return fig | |
| except Exception as exc: | |
| print(f"Erro ao criar gráfico obs vs calc: {exc}") | |
| return None | |
| def _criar_grafico_residuos(y_calc, residuos, indices=None): | |
| try: | |
| fig = go.Figure() | |
| scatter_args = dict( | |
| x=y_calc, | |
| y=residuos, | |
| mode="markers", | |
| marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color="black", width=1)), | |
| name="Resíduos", | |
| ) | |
| if indices is not None: | |
| scatter_args["customdata"] = indices | |
| scatter_args["hovertemplate"] = ( | |
| "<b>Índice:</b> %{customdata}<br><b>Ajustado:</b> %{x:.2f}<br>" | |
| "<b>Resíduo:</b> %{y:.4f}<extra></extra>" | |
| ) | |
| else: | |
| scatter_args["hovertemplate"] = ( | |
| "<b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>" | |
| ) | |
| fig.add_trace(go.Scatter(**scatter_args)) | |
| fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2) | |
| fig.update_layout( | |
| title=dict(text="Resíduos vs Valores Ajustados", x=0.5), | |
| xaxis_title="Valores Ajustados", | |
| yaxis_title="Resíduos", | |
| showlegend=False, | |
| plot_bgcolor="white", | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| return fig | |
| except Exception as exc: | |
| print(f"Erro ao criar gráfico de resíduos: {exc}") | |
| return None | |
| def _criar_histograma_residuos(residuos): | |
| try: | |
| fig = go.Figure() | |
| fig.add_trace( | |
| go.Histogram( | |
| x=residuos, | |
| histnorm="probability density", | |
| marker=dict(color=COR_PRINCIPAL, line=dict(color="black", width=1)), | |
| opacity=0.7, | |
| name="Resíduos", | |
| ) | |
| ) | |
| mu, sigma = np.mean(residuos), np.std(residuos) | |
| x_norm = np.linspace(min(residuos) - sigma, max(residuos) + sigma, 100) | |
| y_norm = stats.norm.pdf(x_norm, mu, sigma) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=x_norm, | |
| y=y_norm, | |
| mode="lines", | |
| line=dict(color=COR_LINHA, width=3), | |
| name="Curva Normal", | |
| ) | |
| ) | |
| fig.update_layout( | |
| title=dict(text="Distribuição dos Resíduos", x=0.5), | |
| xaxis_title="Resíduos", | |
| yaxis_title="Densidade", | |
| showlegend=True, | |
| plot_bgcolor="white", | |
| barmode="overlay", | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| return fig | |
| except Exception as exc: | |
| print(f"Erro ao criar histograma: {exc}") | |
| return None | |
| def _criar_grafico_cook(modelos_sm): | |
| try: | |
| if modelos_sm is None: | |
| return None | |
| influence = OLSInfluence(modelos_sm) | |
| cooks_d = influence.cooks_distance[0] | |
| n = len(cooks_d) | |
| indices = np.arange(1, n + 1) | |
| limite = 4 / n | |
| fig = go.Figure() | |
| for idx, valor in zip(indices, cooks_d): | |
| cor = COR_LINHA if valor > limite else COR_PRINCIPAL | |
| fig.add_trace( | |
| go.Scatter( | |
| x=[idx, idx], | |
| y=[0, valor], | |
| mode="lines", | |
| line=dict(color=cor, width=1.5), | |
| showlegend=False, | |
| hoverinfo="skip", | |
| ) | |
| ) | |
| cores_pontos = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d] | |
| fig.add_trace( | |
| go.Scatter( | |
| x=indices, | |
| y=cooks_d, | |
| mode="markers", | |
| marker=dict(color=cores_pontos, size=8, line=dict(color="black", width=1)), | |
| name="Distância de Cook", | |
| hovertemplate="Obs: %{x}<br>Cook: %{y:.4f}<extra></extra>", | |
| ) | |
| ) | |
| fig.add_hline( | |
| y=limite, | |
| line_dash="dash", | |
| line_color="gray", | |
| annotation_text=f"4/n = {limite:.4f}", | |
| annotation_position="top right", | |
| ) | |
| fig.update_layout( | |
| title=dict(text="Distância de Cook", x=0.5), | |
| xaxis_title="Observação", | |
| yaxis_title="Distância de Cook", | |
| plot_bgcolor="white", | |
| margin=dict(l=60, r=40, t=60, b=60), | |
| ) | |
| fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black") | |
| return fig | |
| except Exception as exc: | |
| print(f"Erro ao criar gráfico de Cook: {exc}") | |
| return None | |
| def _criar_grafico_correlacao(modelos_sm, nome_y: str | None = None): | |
| try: | |
| if modelos_sm is None or not hasattr(modelos_sm, "model"): | |
| return None | |
| model = modelos_sm.model | |
| X = model.exog | |
| X_names = model.exog_names | |
| y = model.endog | |
| y_name_base = ( | |
| str(nome_y or "").strip() | |
| or str(getattr(model, "endog_names", "Variável Dependente") or "").strip() | |
| or "Variável Dependente" | |
| ) | |
| y_name = y_name_base if re.search(r"\(Y\)$", y_name_base, flags=re.IGNORECASE) else f"{y_name_base} (Y)" | |
| df_X = pd.DataFrame(X, columns=X_names) | |
| df_X = df_X.drop( | |
| columns=[c for c in df_X.columns if str(c).lower() in ("const", "intercept")], | |
| errors="ignore", | |
| ) | |
| df_y = pd.DataFrame({y_name: y}) | |
| df = pd.concat([df_y, df_X], axis=1).apply(pd.to_numeric, errors="coerce") | |
| variancias = df.var(ddof=0) | |
| df = df.loc[:, variancias.fillna(0) > 0] | |
| if df.shape[1] < 2: | |
| return None | |
| corr = df.corr() | |
| if corr.isnull().values.all(): | |
| return None | |
| mask = np.eye(len(corr), dtype=bool) | |
| corr = corr.where(~mask) | |
| text = np.where(np.isnan(corr.values), "", np.round(corr.values, 2).astype(str)) | |
| fig = go.Figure( | |
| go.Heatmap( | |
| z=corr.values, | |
| x=corr.columns, | |
| y=corr.index, | |
| text=text, | |
| texttemplate="%{text}", | |
| textfont=dict(size=10), | |
| zmin=-1, | |
| zmax=1, | |
| zmid=0, | |
| colorscale=[ | |
| [0.00, "rgb(103,0,31)"], | |
| [0.08, "rgb(178,24,43)"], | |
| [0.16, "rgb(214,96,77)"], | |
| [0.24, "rgb(244,165,130)"], | |
| [0.32, "rgb(253,219,199)"], | |
| [0.45, "rgb(255,255,255)"], | |
| [0.55, "rgb(255,255,255)"], | |
| [0.68, "rgb(209,229,240)"], | |
| [0.76, "rgb(146,197,222)"], | |
| [0.84, "rgb(67,147,195)"], | |
| [0.92, "rgb(33,102,172)"], | |
| [1.00, "rgb(5,48,97)"], | |
| ], | |
| colorbar=dict(title="Correlação"), | |
| hovertemplate="%{x} × %{y}<br>ρ = %{z:.3f}<extra></extra>", | |
| ) | |
| ) | |
| fig.add_shape( | |
| type="line", | |
| xref="paper", | |
| yref="paper", | |
| x0=0, | |
| y0=1, | |
| x1=1, | |
| y1=0, | |
| line=dict(color="rgba(0,0,0,0.35)", width=1), | |
| layer="above", | |
| ) | |
| fig.update_layout( | |
| title=dict(text="Matriz de Correlação", x=0.5), | |
| height=600, | |
| template="plotly_white", | |
| xaxis=dict(tickangle=45, showgrid=False), | |
| yaxis=dict(autorange="reversed", showgrid=False), | |
| ) | |
| return fig | |
| except Exception as exc: | |
| print(f"Erro na geração do gráfico: {exc}") | |
| traceback.print_exc() | |
| return None | |
| def gerar_todos_graficos(pacote): | |
| graficos = {"obs_calc": None, "residuos": None, "hist": None, "cook": None, "corr": None} | |
| obs_calc = pacote["modelo"]["obs_calc"] | |
| modelos_sm = pacote["modelo"]["sm"] | |
| info_transf = pacote.get("transformacoes", {}).get("info") or [] | |
| nome_y = None | |
| if isinstance(info_transf, list) and info_transf: | |
| primeira_linha = str(info_transf[0] or "").strip() | |
| if primeira_linha: | |
| nome_y = primeira_linha.split(": ", 1)[0].strip() or None | |
| y_obs = None | |
| y_calc = None | |
| residuos = None | |
| indices = None | |
| if obs_calc is not None and not obs_calc.empty: | |
| cols_lower = {str(c).lower(): c for c in obs_calc.columns} | |
| indices = obs_calc.index.values if obs_calc.index is not None else None | |
| for nome in ["observado", "obs", "y_obs", "y", "valor_observado"]: | |
| if nome in cols_lower: | |
| y_obs = obs_calc[cols_lower[nome]].values | |
| break | |
| for nome in ["calculado", "calc", "y_calc", "y_hat", "previsto"]: | |
| if nome in cols_lower: | |
| y_calc = obs_calc[cols_lower[nome]].values | |
| break | |
| for nome in ["residuo", "residuos", "resid"]: | |
| if nome in cols_lower: | |
| residuos = obs_calc[cols_lower[nome]].values | |
| break | |
| if modelos_sm is not None: | |
| try: | |
| if y_obs is None and hasattr(modelos_sm.model, "endog"): | |
| y_obs = modelos_sm.model.endog | |
| if y_calc is None and hasattr(modelos_sm, "fittedvalues"): | |
| y_calc = modelos_sm.fittedvalues | |
| if residuos is None and hasattr(modelos_sm, "resid"): | |
| residuos = modelos_sm.resid | |
| except Exception: | |
| pass | |
| if residuos is None and y_obs is not None and y_calc is not None: | |
| residuos = np.array(y_obs) - np.array(y_calc) | |
| y_obs = np.array(y_obs) if y_obs is not None else None | |
| y_calc = np.array(y_calc) if y_calc is not None else None | |
| residuos = np.array(residuos) if residuos is not None else None | |
| if y_obs is not None and y_calc is not None: | |
| graficos["obs_calc"] = _criar_grafico_obs_calc(y_obs, y_calc, indices) | |
| if residuos is not None and y_calc is not None: | |
| graficos["residuos"] = _criar_grafico_residuos(y_calc, residuos, indices) | |
| if residuos is not None: | |
| graficos["hist"] = _criar_histograma_residuos(residuos) | |
| if modelos_sm is not None: | |
| graficos["cook"] = _criar_grafico_cook(modelos_sm) | |
| graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y) | |
| return graficos | |
| def reorganizar_modelos_resumos(diagnosticos): | |
| gerais = diagnosticos.get("gerais", {}) | |
| return { | |
| "estatisticas_gerais": { | |
| "n": {"nome": "Número de observações", "valor": gerais.get("n")}, | |
| "k": {"nome": "Número de variáveis independentes", "valor": gerais.get("k")}, | |
| "desvio_padrao_residuos": {"nome": "Desvio padrão dos resíduos", "valor": gerais.get("desvio_padrao_residuos")}, | |
| "mse": {"nome": "MSE", "valor": gerais.get("mse")}, | |
| "r2": {"nome": "R²", "valor": gerais.get("r2")}, | |
| "r2_ajustado": {"nome": "R² ajustado", "valor": gerais.get("r2_ajustado")}, | |
| "r_pearson": {"nome": "Correlação Pearson", "valor": gerais.get("r_pearson")}, | |
| }, | |
| "teste_f": { | |
| "nome": "Teste F", | |
| "estatistica": diagnosticos.get("teste_f", {}).get("estatistica"), | |
| "pvalor": diagnosticos.get("teste_f", {}).get("p_valor"), | |
| "interpretacao": diagnosticos.get("teste_f", {}).get("interpretacao"), | |
| }, | |
| "teste_ks": { | |
| "nome": "Teste de Normalidade (Kolmogorov-Smirnov)", | |
| "estatistica": diagnosticos.get("teste_ks", {}).get("estatistica"), | |
| "pvalor": diagnosticos.get("teste_ks", {}).get("p_valor"), | |
| "interpretacao": diagnosticos.get("teste_ks", {}).get("interpretacao"), | |
| }, | |
| "perc_resid": { | |
| "nome": "Teste de Normalidade (Comparação com a Curva Normal)", | |
| "valor": diagnosticos.get("teste_normalidade", {}).get("percentuais"), | |
| "interpretacao": [ | |
| "Ideal 68% → aceitável entre 64% e 75%", | |
| "Ideal 90% → aceitável entre 88% e 95%", | |
| "Ideal 95% → aceitável entre 95% e 100%", | |
| ], | |
| }, | |
| "teste_dw": { | |
| "nome": "Teste de Autocorrelação (Durbin-Watson)", | |
| "estatistica": diagnosticos.get("teste_dw", {}).get("estatistica"), | |
| "interpretacao": diagnosticos.get("teste_dw", {}).get("interpretacao"), | |
| }, | |
| "teste_bp": { | |
| "nome": "Teste de Homocedasticidade (Breusch-Pagan)", | |
| "estatistica": diagnosticos.get("teste_bp", {}).get("estatistica"), | |
| "pvalor": diagnosticos.get("teste_bp", {}).get("p_valor"), | |
| "interpretacao": diagnosticos.get("teste_bp", {}).get("interpretacao"), | |
| }, | |
| "equacao": diagnosticos.get("equacao"), | |
| } | |
| def formatar_monetario(valor): | |
| if pd.isna(valor): | |
| return "N/A" | |
| return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") | |
| def formatar_resumo_html(resumo_reorganizado): | |
| def criar_titulo_secao(titulo): | |
| return f'<div class="section-title-orange-solid">{titulo}</div>' | |
| def criar_linha_campo(campo, valor): | |
| return f'<div class="field-row"><span class="field-row-label">{campo}</span><span class="field-row-value">{valor}</span></div>' | |
| def criar_linha_interpretacao(interpretacao): | |
| return f'<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value-italic">{interpretacao}</span></div>' | |
| def formatar_numero(valor, casas_decimais=4): | |
| if valor is None: | |
| return "N/A" | |
| if isinstance(valor, (int, float, np.floating)): | |
| return f"{valor:.{casas_decimais}f}" | |
| return str(valor) | |
| linhas_html = [] | |
| estat_gerais = resumo_reorganizado.get("estatisticas_gerais", {}) | |
| if estat_gerais: | |
| linhas_html.append(criar_titulo_secao("Estatísticas Gerais")) | |
| for chave, dados in estat_gerais.items(): | |
| linhas_html.append(criar_linha_campo(dados.get("nome", chave), formatar_numero(dados.get("valor")))) | |
| for chave_teste, label in [("teste_f", "Estatística F"), ("teste_ks", "Estatística KS")]: | |
| teste = resumo_reorganizado.get(chave_teste, {}) | |
| if teste.get("estatistica") is not None: | |
| linhas_html.append(criar_titulo_secao(teste.get("nome"))) | |
| linhas_html.append(criar_linha_campo(label, formatar_numero(teste["estatistica"]))) | |
| if teste.get("pvalor") is not None: | |
| linhas_html.append(criar_linha_campo("P-valor", formatar_numero(teste["pvalor"]))) | |
| if teste.get("interpretacao"): | |
| linhas_html.append(criar_linha_interpretacao(teste["interpretacao"])) | |
| perc_resid = resumo_reorganizado.get("perc_resid", {}) | |
| if perc_resid.get("valor") is not None: | |
| linhas_html.append(criar_titulo_secao(perc_resid.get("nome"))) | |
| linhas_html.append(criar_linha_campo("Percentuais Atingidos", perc_resid["valor"])) | |
| if perc_resid.get("interpretacao"): | |
| linhas_html.append('<div class="interpretation-label">Interpretação</div>') | |
| for ideal in perc_resid["interpretacao"]: | |
| linhas_html.append(f'<div class="interpretation-item">• {ideal}</div>') | |
| for chave_teste, label in [("teste_dw", "Estatística DW"), ("teste_bp", "Estatística LM")]: | |
| teste = resumo_reorganizado.get(chave_teste, {}) | |
| if teste.get("estatistica") is not None: | |
| linhas_html.append(criar_titulo_secao(teste.get("nome"))) | |
| linhas_html.append(criar_linha_campo(label, formatar_numero(teste["estatistica"]))) | |
| if teste.get("pvalor") is not None: | |
| linhas_html.append(criar_linha_campo("P-valor", formatar_numero(teste["pvalor"]))) | |
| if teste.get("interpretacao"): | |
| linhas_html.append(criar_linha_interpretacao(teste["interpretacao"])) | |
| return f'<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>' | |
| def criar_titulo_secao_html(titulo): | |
| return f'<div class="section-title-orange">{titulo}</div>' | |
| def formatar_escalas_html(escalas_raw): | |
| if isinstance(escalas_raw, pd.DataFrame): | |
| itens = escalas_raw.iloc[:, 0].tolist() | |
| elif isinstance(escalas_raw, list): | |
| itens = escalas_raw | |
| else: | |
| itens = [str(escalas_raw)] | |
| itens = [str(item) for item in itens if item and str(item).strip()] | |
| if not itens: | |
| return "<p style='color: #6c757d; font-style: italic;'>Nenhuma transformação disponível.</p>" | |
| max_chars = max(len(item) for item in itens) | |
| largura_min = max(150, max_chars * 7 + 28) | |
| cards_html = "" | |
| for item in itens: | |
| if ":" in item: | |
| partes = item.split(":", 1) | |
| conteudo = ( | |
| f'<span style="font-weight: 600; color: #495057;">{partes[0].strip()}:</span>' | |
| f'<span style="font-weight: 400; color: #6c757d; margin-left: 4px;">{partes[1].strip()}</span>' | |
| ) | |
| else: | |
| conteudo = f'<span style="font-weight: 600; color: #495057;">{item}</span>' | |
| cards_html += f'<div class="dai-card-light" style="min-width: {largura_min}px;">{conteudo}</div>' | |
| return ( | |
| f'<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}' | |
| f'<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">' | |
| f"{cards_html}</div></div>" | |
| ) | |
| def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plot_col): | |
| df_plot = df_mapa.copy() | |
| df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce") | |
| df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce") | |
| if len(df_plot) <= 1: | |
| return df_plot | |
| chave_lat = df_plot[lat_col].round(7) | |
| chave_lon = df_plot[lon_col].round(7) | |
| grupos = df_plot.groupby([chave_lat, chave_lon], sort=False) | |
| passo_metros = 4.0 | |
| max_raio_metros = 22.0 | |
| metros_por_grau_lat = 111_320.0 | |
| lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col)) | |
| lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col)) | |
| for _, idx_labels in grupos.indices.items(): | |
| posicoes = np.asarray(idx_labels, dtype=int) | |
| if posicoes.size <= 1: | |
| continue | |
| base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos]) | |
| base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos]) | |
| if not np.isfinite(base_lat) or not np.isfinite(base_lon): | |
| continue | |
| seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360 | |
| angulo_base = math.radians(seed_val) | |
| cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6) | |
| metros_por_grau_lon = metros_por_grau_lat * cos_lat | |
| for pos, pos_idx in enumerate(posicoes): | |
| if pos == 0: | |
| continue | |
| pos_ring = pos - 1 | |
| ring = 1 | |
| while pos_ring >= (6 * ring): | |
| pos_ring -= 6 * ring | |
| ring += 1 | |
| slots_ring = max(6 * ring, 1) | |
| angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring)) | |
| raio_m = min(ring * passo_metros, max_raio_metros) | |
| delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat | |
| delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon | |
| df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat | |
| df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon | |
| return df_plot | |
| def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8): | |
| def _limitar_texto(valor: str, limite: int) -> str: | |
| txt = str(valor) | |
| if limite <= 0 or len(txt) <= limite: | |
| return txt | |
| if limite <= 3: | |
| return txt[:limite] | |
| return txt[: limite - 3] + "..." | |
| if not itens: | |
| html = ( | |
| "<div style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">" | |
| "<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>" | |
| "<div style=\"padding:12px 15px; background:#f8f9fa; color:#6c757d; font-size:12px;\">Sem variáveis para exibir.</div>" | |
| "</div>" | |
| ) | |
| return html, 360 | |
| max_colunas_por_pagina = 2 | |
| max_chars_chave = 20 | |
| max_chars_valor = 20 | |
| char_px = 7.2 | |
| gap_cols_px = 12 | |
| popup_padding_horizontal_px = 30 | |
| coluna_largura_px = int(round((max_chars_chave + max_chars_valor) * char_px + 28)) | |
| itens_por_pagina = max_itens_pagina * max_colunas_por_pagina | |
| paginas = [itens[i:i + itens_por_pagina] for i in range(0, len(itens), itens_por_pagina)] | |
| itens_primeira_pagina = len(paginas[0]) if paginas else 0 | |
| colunas_visiveis = max( | |
| 1, | |
| min( | |
| max_colunas_por_pagina, | |
| int(math.ceil(itens_primeira_pagina / max_itens_pagina)) if itens_primeira_pagina else 1, | |
| ), | |
| ) | |
| popup_largura_px = ( | |
| popup_padding_horizontal_px | |
| + (coluna_largura_px * colunas_visiveis) | |
| + (gap_cols_px * (colunas_visiveis - 1)) | |
| ) | |
| pages_html = [] | |
| for page_idx, page_items in enumerate(paginas): | |
| colunas = [page_items[i:i + max_itens_pagina] for i in range(0, len(page_items), max_itens_pagina)] | |
| colunas_html = [] | |
| for col_itens in colunas: | |
| trs_parts = [] | |
| for c, v in col_itens: | |
| c_full = str(c) | |
| v_full = str(v) | |
| c_txt = escape(_limitar_texto(c_full, max_chars_chave)) | |
| v_txt = escape(_limitar_texto(v_full, max_chars_valor)) | |
| c_title = escape(c_full) | |
| v_title = escape(v_full) | |
| trs_parts.append( | |
| "<tr style='border-bottom:1px solid #e9ecef;'>" | |
| "<td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>" | |
| f"<span title='{c_title}'>{c_txt}</span>" | |
| "</td>" | |
| "<td style='padding:4px 0; text-align:right; color:#495057; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>" | |
| f"<span title='{v_title}'>{v_txt}</span>" | |
| "</td>" | |
| "</tr>" | |
| ) | |
| trs = "".join(trs_parts) | |
| colunas_html.append( | |
| f"<div style='flex:0 0 {coluna_largura_px}px; width:{coluna_largura_px}px; min-width:{coluna_largura_px}px; overflow:hidden;'>" | |
| f"<table style='border-collapse:collapse; table-layout:fixed; font-size:12px; width:{coluna_largura_px}px;'>{trs}</table>" | |
| "</div>" | |
| ) | |
| display = "block" if page_idx == 0 else "none" | |
| pages_html.append( | |
| f"<div class='mesa-popup-page' data-page='{page_idx + 1}' style='display:{display};'>" | |
| f"<div style='display:flex; gap:12px; align-items:flex-start; flex-wrap:nowrap;'>{''.join(colunas_html)}</div>" | |
| "</div>" | |
| ) | |
| controls_html = "" | |
| if len(paginas) > 1: | |
| controls_html = ( | |
| "<div class='mesa-popup-controls' style='display:flex; gap:5px; flex-wrap:nowrap; margin-top:8px; align-items:center; justify-content:center; white-space:nowrap; width:100%;'>" | |
| "<button type='button' data-page-nav='first' data-a='first' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">«</button>" | |
| "<button type='button' data-page-nav='prev' data-a='prev' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">‹</button>" | |
| "<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>" | |
| "<button type='button' data-page-nav='next' data-a='next' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">›</button>" | |
| "<button type='button' data-page-nav='last' data-a='last' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">»</button>" | |
| "</div>" | |
| ) | |
| html = ( | |
| f"<div id='{popup_uid}' data-pager='1' data-current-page='1' data-page-start='1' data-page-window='1' " | |
| f"style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden; width:{popup_largura_px}px; max-width:{popup_largura_px}px;\">" | |
| "<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>" | |
| "<div style=\"padding:12px 15px; background:#f8f9fa;\">" | |
| f"<div class='mesa-popup-pages' style='width:100%;'>{''.join(pages_html)}</div>" | |
| f"{controls_html}" | |
| "</div></div>" | |
| ) | |
| return html, popup_largura_px | |
| def _formatar_valor_popup_registro(coluna, valor): | |
| if valor is None: | |
| return "—" | |
| try: | |
| if pd.isna(valor): | |
| return "—" | |
| except Exception: | |
| pass | |
| col_norm = str(coluna).lower() | |
| if isinstance(valor, (int, float, np.integer, np.floating)): | |
| numero = float(valor) | |
| if not np.isfinite(numero): | |
| return "—" | |
| if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]): | |
| return formatar_monetario(numero) | |
| return f"{numero:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") | |
| return str(valor) | |
| def montar_popup_registro_html(row, popup_uid, max_itens_pagina=8): | |
| itens = [] | |
| for col, val in row.items(): | |
| col_txt = str(col) | |
| if col_txt.startswith("__mesa_"): | |
| continue | |
| if col_txt.lower() not in ["lat", "latitude", "lon", "longitude"]: | |
| itens.append((col, _formatar_valor_popup_registro(col, val))) | |
| return _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=max_itens_pagina) | |
| def _montar_popup_registro_placeholder(session_id, row_id, popup_uid, popup_endpoint, auth_token=None): | |
| popup_id = escape(str(popup_uid)) | |
| session_attr = escape(str(session_id)) | |
| row_attr = escape(str(int(row_id))) | |
| endpoint_attr = escape(str(popup_endpoint or "/api/visualizacao/map/popup")) | |
| token_attr = escape(str(auth_token or "")) | |
| html = ( | |
| f"<div id='{popup_id}' data-mesa-lazy-popup='1' data-session-id='{session_attr}' " | |
| f"data-row-id='{row_attr}' data-popup-endpoint='{endpoint_attr}' data-auth-token='{token_attr}' " | |
| "style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden; width:340px; max-width:340px;\">" | |
| "<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>" | |
| "<div style=\"padding:12px 15px; background:#f8f9fa; color:#6c757d; font-size:12px;\">Carregando detalhes...</div>" | |
| "</div>" | |
| ) | |
| return html, 380 | |
| def criar_mapa( | |
| df, | |
| lat_col="lat", | |
| lon_col="lon", | |
| cor_col=None, | |
| tamanho_col=None, | |
| col_y=None, | |
| session_id=None, | |
| popup_endpoint=None, | |
| popup_auth_token=None, | |
| avaliandos_tecnicos=None, | |
| ): | |
| lat_real = None | |
| lon_real = None | |
| for col in df.columns: | |
| col_norm = str(col).lower() | |
| if lat_real is None and col_norm in ["lat", "latitude", "siat_latitude"]: | |
| lat_real = col | |
| if lon_real is None and col_norm in ["lon", "longitude", "long", "siat_longitude"]: | |
| lon_real = col | |
| if lat_real is not None and lon_real is not None: | |
| break | |
| if lat_real is None or lon_real is None: | |
| return "<p>Coordenadas (lat/lon) não encontradas nos dados.</p>" | |
| def _primeira_serie_por_nome(dataframe, nome_coluna): | |
| matches = [i for i, c in enumerate(dataframe.columns) if str(c) == str(nome_coluna)] | |
| if not matches: | |
| return None | |
| return dataframe.iloc[:, matches[0]] | |
| df_mapa = df.copy() | |
| lat_key = "__mesa_lat__" | |
| lon_key = "__mesa_lon__" | |
| lat_serie = _primeira_serie_por_nome(df_mapa, lat_real) | |
| lon_serie = _primeira_serie_por_nome(df_mapa, lon_real) | |
| if lat_serie is None or lon_serie is None: | |
| return "<p>Coordenadas (lat/lon) não encontradas nos dados.</p>" | |
| df_mapa[lat_key] = pd.to_numeric(lat_serie, errors="coerce") | |
| df_mapa[lon_key] = pd.to_numeric(lon_serie, errors="coerce") | |
| df_mapa = df_mapa.dropna(subset=[lat_key, lon_key]) | |
| df_mapa = df_mapa[ | |
| (df_mapa[lat_key] >= -90.0) | |
| & (df_mapa[lat_key] <= 90.0) | |
| & (df_mapa[lon_key] >= -180.0) | |
| & (df_mapa[lon_key] <= 180.0) | |
| ].copy() | |
| if df_mapa.empty: | |
| return "<p>Sem coordenadas válidas para exibir.</p>" | |
| centro_lat = float(df_mapa[lat_key].median()) | |
| centro_lon = float(df_mapa[lon_key].median()) | |
| mapa = folium.Map( | |
| location=[centro_lat, centro_lon], | |
| zoom_start=12, | |
| tiles=None, | |
| prefer_canvas=True, | |
| control_scale=True, | |
| ) | |
| folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(mapa) | |
| folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(mapa) | |
| add_bairros_layer(mapa, show=True) | |
| if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col: | |
| cor_col = tamanho_col | |
| colormap = None | |
| cor_key = None | |
| if cor_col and cor_col in df_mapa.columns: | |
| serie_cor = _primeira_serie_por_nome(df_mapa, cor_col) | |
| if serie_cor is not None: | |
| cor_key = "__mesa_cor__" | |
| df_mapa[cor_key] = pd.to_numeric(serie_cor, errors="coerce") | |
| vmin = df_mapa[cor_key].min() | |
| vmax = df_mapa[cor_key].max() | |
| if pd.notna(vmin) and pd.notna(vmax): | |
| colormap = cm.LinearColormap( | |
| colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"], | |
| vmin=vmin, | |
| vmax=vmax, | |
| caption=cor_col, | |
| ) | |
| colormap.add_to(mapa) | |
| raio_min, raio_max = 3, 18 | |
| tamanho_func = None | |
| tamanho_key = None | |
| if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns: | |
| serie_tamanho = _primeira_serie_por_nome(df_mapa, tamanho_col) | |
| if serie_tamanho is not None: | |
| tamanho_key = "__mesa_tamanho__" | |
| df_mapa[tamanho_key] = pd.to_numeric(serie_tamanho, errors="coerce") | |
| t_min = df_mapa[tamanho_key].min() | |
| t_max = df_mapa[tamanho_key].max() | |
| if pd.notna(t_min) and pd.notna(t_max): | |
| if t_max > t_min: | |
| tamanho_func = lambda v, _min=t_min, _max=t_max: raio_min + (v - _min) / (_max - _min) * (raio_max - raio_min) | |
| else: | |
| tamanho_func = lambda v: (raio_min + raio_max) / 2 | |
| mostrar_indices = len(df_mapa) <= 800 | |
| camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None | |
| lat_plot_key = "__mesa_lat_plot__" | |
| lon_plot_key = "__mesa_lon_plot__" | |
| df_plot_pontos = _aplicar_jitter_sobrepostos( | |
| df_mapa, | |
| lat_col=lat_key, | |
| lon_col=lon_key, | |
| lat_plot_col=lat_plot_key, | |
| lon_plot_col=lon_plot_key, | |
| ) | |
| tooltip_col = None | |
| tooltip_key = None | |
| if tamanho_key: | |
| tooltip_col = tamanho_col | |
| tooltip_key = tamanho_key | |
| elif col_y and col_y in df_mapa.columns: | |
| serie_tooltip = _primeira_serie_por_nome(df_mapa, col_y) | |
| if serie_tooltip is not None: | |
| tooltip_col = col_y | |
| tooltip_key = "__mesa_tooltip__" | |
| df_mapa[tooltip_key] = serie_tooltip | |
| total_pontos_plot = len(df_plot_pontos) | |
| raio_padrao = 4 if total_pontos_plot <= 2500 else 3 | |
| contorno_padrao = 0.8 if total_pontos_plot <= 2500 else 0.55 | |
| opacidade_preenchimento = 0.68 if total_pontos_plot <= 2500 else 0.6 | |
| for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()): | |
| cor = colormap(row[cor_key]) if colormap and cor_key and pd.notna(row[cor_key]) else COR_PRINCIPAL | |
| if tamanho_func and tamanho_key and pd.notna(row[tamanho_key]): | |
| raio = tamanho_func(row[tamanho_key]) | |
| peso_contorno = 1 | |
| else: | |
| raio = raio_padrao | |
| peso_contorno = contorno_padrao | |
| idx_display = int(row["index"]) if "index" in row.index else idx | |
| popup_uid = f"mesa-pop-{marker_ordem}" | |
| popup_html = None | |
| popup_width = None | |
| row_id_raw = row["__mesa_row_id__"] if "__mesa_row_id__" in row.index else None | |
| if session_id is not None and row_id_raw is not None: | |
| try: | |
| popup_html, popup_width = _montar_popup_registro_placeholder( | |
| session_id=session_id, | |
| row_id=int(row_id_raw), | |
| popup_uid=popup_uid, | |
| popup_endpoint=popup_endpoint, | |
| auth_token=popup_auth_token, | |
| ) | |
| except Exception: | |
| popup_html = None | |
| popup_width = None | |
| if popup_html is None or popup_width is None: | |
| popup_html, popup_width = montar_popup_registro_html(row, popup_uid, max_itens_pagina=8) | |
| tooltip_html = ( | |
| "<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>" | |
| f"<b>Índice {idx_display}</b>" | |
| ) | |
| if tooltip_col and tooltip_key and tooltip_key in row.index: | |
| val_t = row[tooltip_key] | |
| col_norm = str(tooltip_col).lower() | |
| if isinstance(val_t, (int, float, np.floating)): | |
| if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]): | |
| val_str = formatar_monetario(val_t) | |
| else: | |
| val_str = f"{val_t:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") | |
| else: | |
| val_str = str(val_t) | |
| tooltip_html += f"<br><span style='color:#555;'>{tooltip_col}:</span> <b>{val_str}</b>" | |
| tooltip_html += "</div>" | |
| marcador = folium.CircleMarker( | |
| location=[row[lat_plot_key], row[lon_plot_key]], | |
| radius=raio, | |
| popup=folium.Popup(popup_html, max_width=popup_width), | |
| tooltip=folium.Tooltip(tooltip_html, sticky=True), | |
| color="black", | |
| weight=peso_contorno, | |
| fill=True, | |
| fillColor=cor, | |
| fillOpacity=opacidade_preenchimento, | |
| ).add_to(mapa) | |
| marcador.options["mesaBaseRadius"] = float(max(1.0, raio)) | |
| if mostrar_indices and camada_indices is not None: | |
| add_indice_marker( | |
| camada_indices, | |
| lat=float(row[lat_plot_key]), | |
| lon=float(row[lon_plot_key]), | |
| indice=idx_display, | |
| ) | |
| if mostrar_indices and camada_indices is not None: | |
| camada_indices.add_to(mapa) | |
| if avaliandos_tecnicos: | |
| camada_trabalhos_tecnicos = folium.FeatureGroup(name="Trabalhos tecnicos", show=True) | |
| add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos) | |
| camada_trabalhos_tecnicos.add_to(mapa) | |
| folium.LayerControl().add_to(mapa) | |
| plugins.Fullscreen().add_to(mapa) | |
| plugins.MeasureControl( | |
| primary_length_unit="meters", | |
| secondary_length_unit="kilometers", | |
| primary_area_unit="sqmeters", | |
| secondary_area_unit="hectares", | |
| ).add_to(mapa) | |
| add_zoom_responsive_circle_markers(mapa) | |
| add_popup_pagination_handlers(mapa) | |
| df_bounds = df_mapa | |
| if len(df_mapa) >= 8: | |
| lat_vals = df_mapa[lat_key] | |
| lon_vals = df_mapa[lon_key] | |
| lat_med = float(lat_vals.median()) | |
| lon_med = float(lon_vals.median()) | |
| lat_mad = float((lat_vals - lat_med).abs().median()) | |
| lon_mad = float((lon_vals - lon_med).abs().median()) | |
| lat_span = float(lat_vals.max() - lat_vals.min()) | |
| lon_span = float(lon_vals.max() - lon_vals.min()) | |
| lat_scale = max(lat_mad, lat_span / 30.0, 1e-6) | |
| lon_scale = max(lon_mad, lon_span / 30.0, 1e-6) | |
| score = ((lat_vals - lat_med) / lat_scale) ** 2 + ((lon_vals - lon_med) / lon_scale) ** 2 | |
| lim = float(score.quantile(0.75)) | |
| df_core = df_mapa[score <= lim] | |
| if len(df_core) >= max(5, int(len(df_mapa) * 0.45)): | |
| df_bounds = df_core | |
| if len(df_bounds) >= 50: | |
| lat_min, lat_max = df_bounds[lat_key].quantile([0.01, 0.99]).tolist() | |
| lon_min, lon_max = df_bounds[lon_key].quantile([0.01, 0.99]).tolist() | |
| else: | |
| lat_min, lat_max = float(df_bounds[lat_key].min()), float(df_bounds[lat_key].max()) | |
| lon_min, lon_max = float(df_bounds[lon_key].min()), float(df_bounds[lon_key].max()) | |
| if not np.isfinite(lat_min) or not np.isfinite(lat_max): | |
| lat_min, lat_max = float(df_mapa[lat_key].min()), float(df_mapa[lat_key].max()) | |
| if not np.isfinite(lon_min) or not np.isfinite(lon_max): | |
| lon_min, lon_max = float(df_mapa[lon_key].min()), float(df_mapa[lon_key].max()) | |
| if np.isclose(lat_min, lat_max): | |
| lat_delta = 0.0008 | |
| lat_min = float(lat_min) - lat_delta | |
| lat_max = float(lat_max) + lat_delta | |
| if np.isclose(lon_min, lon_max): | |
| lon_delta = 0.0008 | |
| lon_min = float(lon_min) - lon_delta | |
| lon_max = float(lon_max) + lon_delta | |
| mapa.fit_bounds([[float(lat_min), float(lon_min)], [float(lat_max), float(lon_max)]], padding=(48, 48), max_zoom=18) | |
| return mapa.get_root().render() | |
| def _formatar_badge_completo(pacote, nome_modelo=""): | |
| if not pacote: | |
| return "" | |
| def _esc(value): | |
| return str(value or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) | |
| def _data_br(value): | |
| texto = str(value or "").strip() | |
| if not texto: | |
| return "" | |
| match_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", texto) | |
| if match_iso: | |
| try: | |
| return datetime(int(match_iso.group(1)), int(match_iso.group(2)), int(match_iso.group(3))).strftime("%d/%m/%Y") | |
| except Exception: | |
| return texto | |
| match_iso_slash = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", texto) | |
| if match_iso_slash: | |
| try: | |
| return datetime(int(match_iso_slash.group(1)), int(match_iso_slash.group(2)), int(match_iso_slash.group(3))).strftime("%d/%m/%Y") | |
| except Exception: | |
| return texto | |
| match_br = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", texto) | |
| if match_br: | |
| try: | |
| return datetime(int(match_br.group(3)), int(match_br.group(2)), int(match_br.group(1))).strftime("%d/%m/%Y") | |
| except Exception: | |
| return texto | |
| return texto | |
| model_name = str(nome_modelo or "").strip() or "-" | |
| observacao_modelo = str(pacote.get("observacao_modelo") or "").strip() | |
| periodo = pacote.get("periodo_dados_mercado") or {} | |
| data_inicial = _data_br(periodo.get("data_inicial")) | |
| data_final = _data_br(periodo.get("data_final")) | |
| periodo_txt = f"{data_inicial} a {data_final}" if data_inicial and data_final else "-" | |
| info_variaveis = pacote.get("transformacoes", {}).get("info") or [] | |
| y_nome = "" | |
| y_transf = "" | |
| x_itens: list[tuple[str, str]] = [] | |
| if info_variaveis: | |
| y_parts = str(info_variaveis[0]).split(": ", 1) | |
| y_nome = y_parts[0].strip() | |
| y_transf = y_parts[1].strip() if len(y_parts) > 1 else "" | |
| for item in info_variaveis[1:]: | |
| parts = str(item).split(": ", 1) | |
| nome = parts[0].strip() | |
| transf = parts[1].strip() if len(parts) > 1 else "" | |
| x_itens.append((nome, transf)) | |
| y_transf_display = "" if y_transf in ("(y)", "(x)", "y", "x", "") else y_transf | |
| y_html = ( | |
| "<div class='section1-empty-hint'>Variável dependente não encontrada no modelo carregado.</div>" | |
| if not y_nome | |
| else ( | |
| "<div class='variavel-badge-line'>" | |
| "<span class='variavel-badge-label'>Dependente:</span>" | |
| "<span class='variavel-chip variavel-chip-y variavel-chip-inline'>" | |
| + _esc(y_nome) | |
| + ( | |
| f"<span class='variavel-chip-transform'> {_esc(y_transf_display)}</span>" | |
| if y_transf_display | |
| else "" | |
| ) | |
| + "</span></div>" | |
| ) | |
| ) | |
| if x_itens: | |
| x_badges = [] | |
| for nome, transf in x_itens: | |
| transf_display = "" if transf in ("(x)", "(y)", "x", "y", "") else transf | |
| x_badges.append( | |
| "<span class='variavel-chip'>" | |
| + _esc(nome) | |
| + ( | |
| f"<span class='variavel-chip-transform'> {_esc(transf_display)}</span>" | |
| if transf_display | |
| else "" | |
| ) | |
| + "</span>" | |
| ) | |
| x_html = ( | |
| "<div class='variavel-badge-line'>" | |
| "<span class='variavel-badge-label'>Independentes:</span>" | |
| "<div class='variavel-chip-wrap'>" | |
| + "".join(x_badges) | |
| + "</div></div>" | |
| ) | |
| else: | |
| x_html = "<div class='section1-empty-hint'>Sem variáveis independentes no modelo carregado.</div>" | |
| periodo_html = ( | |
| "<div class='variavel-badge-line'>" | |
| "<span class='variavel-badge-label'>Período dados:</span>" | |
| f"<span class='variavel-badge-value'>{_esc(periodo_txt)}</span>" | |
| "</div>" | |
| ) | |
| elaborador = pacote.get("elaborador") or {} | |
| nome_elaborador = str(elaborador.get("nome_completo") or "").strip() | |
| cargo = str(elaborador.get("cargo") or "").strip() | |
| conselho = str(elaborador.get("conselho") or "").strip() | |
| numero = str(elaborador.get("numero_conselho") or "").strip() | |
| estado = str(elaborador.get("estado_conselho") or "").strip() | |
| matricula = str(elaborador.get("matricula_sem_digito") or "").strip() | |
| lotacao = str(elaborador.get("lotacao") or "").strip() | |
| conselho_reg = "" | |
| if conselho and estado and numero: | |
| conselho_reg = f"{conselho}/{estado} {numero}" | |
| elif conselho or numero or estado: | |
| conselho_reg = " ".join(part for part in [conselho, numero, estado] if part) | |
| meta = [cargo, conselho_reg, f"Matrícula {matricula}" if matricula else "", lotacao] | |
| meta_txt = " | ".join(part for part in meta if part) | |
| elaborador_html = ( | |
| "<div class='section1-empty-hint'>Elaborador não informado no arquivo.</div>" | |
| if not nome_elaborador | |
| else ( | |
| f"<div class='elaborador-badge-name'>{_esc(nome_elaborador)}</div>" | |
| + (f"<div class='elaborador-badge-meta'>{_esc(meta_txt)}</div>" if meta_txt else "") | |
| ) | |
| ) | |
| nome_modelo_html = ( | |
| "<div class='modelo-info-stack-block'>" | |
| "<div class='elaborador-badge-title'>NOME DO MODELO:</div>" | |
| f"<div class='elaborador-badge-name'>{_esc(model_name)}</div>" | |
| + (f"<div class='elaborador-badge-meta'>{_esc(observacao_modelo)}</div>" if observacao_modelo else "") | |
| + "</div>" | |
| ) | |
| return ( | |
| "<div class='subpanel section1-group'>" | |
| "<h4>Informações do modelo</h4>" | |
| "<div class='modelo-info-card'>" | |
| "<div class='modelo-info-split'>" | |
| "<div class='modelo-info-col'>" | |
| + nome_modelo_html | |
| + "<div class='modelo-info-stack-block'>" | |
| "<div class='elaborador-badge-title'>ELABORADO POR:</div>" | |
| + elaborador_html | |
| + "</div>" | |
| "</div>" | |
| "<div class='modelo-info-col modelo-info-col-vars'>" | |
| "<div class='elaborador-badge-title'>Variáveis selecionadas:</div>" | |
| + y_html | |
| + x_html | |
| + periodo_html | |
| + "</div>" | |
| "</div>" | |
| "</div>" | |
| "</div>" | |
| ) | |