Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
4e2aace
1
Parent(s): c88d3e9
correcao de mapas
Browse files- backend/app/api/elaboracao.py +7 -1
- backend/app/core/elaboracao/charts.py +188 -18
- backend/app/core/visualizacao/app.py +146 -15
- backend/app/services/elaboracao_service.py +57 -7
- frontend/src/App.jsx +2 -2
- frontend/src/api.js +2 -1
- frontend/src/components/ElaboracaoTab.jsx +220 -51
- frontend/src/styles.css +51 -0
backend/app/api/elaboracao.py
CHANGED
|
@@ -143,6 +143,7 @@ class ExportModeloPayload(SessionPayload):
|
|
| 143 |
class UpdateMapaPayload(SessionPayload):
|
| 144 |
variavel_mapa: str | None = None
|
| 145 |
modo_mapa: str | None = None
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
class ColunaDataMercadoPayload(SessionPayload):
|
|
@@ -468,7 +469,12 @@ def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
|
| 468 |
@router.post("/residuos/map/update")
|
| 469 |
def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
| 470 |
session = session_store.get(payload.session_id)
|
| 471 |
-
return elaboracao_service.atualizar_mapa_residuos(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
|
| 473 |
|
| 474 |
@router.post("/market-date/preview")
|
|
|
|
| 143 |
class UpdateMapaPayload(SessionPayload):
|
| 144 |
variavel_mapa: str | None = None
|
| 145 |
modo_mapa: str | None = None
|
| 146 |
+
escala_extremo_abs: float | None = None
|
| 147 |
|
| 148 |
|
| 149 |
class ColunaDataMercadoPayload(SessionPayload):
|
|
|
|
| 469 |
@router.post("/residuos/map/update")
|
| 470 |
def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
| 471 |
session = session_store.get(payload.session_id)
|
| 472 |
+
return elaboracao_service.atualizar_mapa_residuos(
|
| 473 |
+
session,
|
| 474 |
+
payload.variavel_mapa,
|
| 475 |
+
payload.modo_mapa,
|
| 476 |
+
payload.escala_extremo_abs,
|
| 477 |
+
)
|
| 478 |
|
| 479 |
|
| 480 |
@router.post("/market-date/preview")
|
backend/app/core/elaboracao/charts.py
CHANGED
|
@@ -7,6 +7,7 @@ import numpy as np
|
|
| 7 |
import pandas as pd
|
| 8 |
import plotly.graph_objects as go
|
| 9 |
from plotly.subplots import make_subplots
|
|
|
|
| 10 |
from scipy import stats
|
| 11 |
from scipy.interpolate import griddata
|
| 12 |
from scipy.spatial import ConvexHull, QhullError
|
|
@@ -15,6 +16,7 @@ from folium import plugins
|
|
| 15 |
import branca.colormap as cm
|
| 16 |
from branca.element import Element
|
| 17 |
from html import escape
|
|
|
|
| 18 |
from app.core.map_layers import add_bairros_layer, add_indice_marker, add_zoom_responsive_circle_markers
|
| 19 |
|
| 20 |
# ============================================================
|
|
@@ -606,6 +608,159 @@ def _mascara_dentro_poligono(x_grid: np.ndarray, y_grid: np.ndarray, poligono: n
|
|
| 606 |
return inside
|
| 607 |
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
def _normalizar_stops_cor(
|
| 610 |
cor_stops: list[float] | None,
|
| 611 |
colors: list[str],
|
|
@@ -974,6 +1129,20 @@ def criar_mapa(
|
|
| 974 |
# Camada de índices (oculta por padrão, ativável pelo controle de camadas)
|
| 975 |
mostrar_indices = (not modo_calor and not modo_superficie) and len(df_mapa) <= 800
|
| 976 |
camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 977 |
|
| 978 |
if modo_superficie:
|
| 979 |
superficie_ok = _adicionar_superficie_continua(
|
|
@@ -1074,7 +1243,7 @@ def criar_mapa(
|
|
| 1074 |
).add_to(m)
|
| 1075 |
elif not modo_superficie:
|
| 1076 |
# Adiciona pontos
|
| 1077 |
-
for idx, row in
|
| 1078 |
# Cor do ponto
|
| 1079 |
if colormap and cor_col:
|
| 1080 |
cor = colormap(row[cor_col])
|
|
@@ -1091,19 +1260,20 @@ def criar_mapa(
|
|
| 1091 |
peso = 3 if idx == indice_destacado else 1
|
| 1092 |
|
| 1093 |
# Popup com informações
|
| 1094 |
-
|
| 1095 |
-
if len(
|
| 1096 |
-
for
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
|
|
|
| 1107 |
|
| 1108 |
# Tooltip (hover): índice + variável selecionada no dropdown
|
| 1109 |
tooltip_html = (
|
|
@@ -1121,9 +1291,9 @@ def criar_mapa(
|
|
| 1121 |
tooltip_html += "</div>"
|
| 1122 |
|
| 1123 |
marcador = folium.CircleMarker(
|
| 1124 |
-
location=[row[
|
| 1125 |
radius=raio,
|
| 1126 |
-
popup=folium.Popup(popup_html, max_width=
|
| 1127 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 1128 |
color='black',
|
| 1129 |
weight=peso,
|
|
@@ -1137,8 +1307,8 @@ def criar_mapa(
|
|
| 1137 |
if mostrar_indices and camada_indices is not None:
|
| 1138 |
add_indice_marker(
|
| 1139 |
camada_indices,
|
| 1140 |
-
lat=float(row[
|
| 1141 |
-
lon=float(row[
|
| 1142 |
indice=idx,
|
| 1143 |
)
|
| 1144 |
|
|
|
|
| 7 |
import pandas as pd
|
| 8 |
import plotly.graph_objects as go
|
| 9 |
from plotly.subplots import make_subplots
|
| 10 |
+
import math
|
| 11 |
from scipy import stats
|
| 12 |
from scipy.interpolate import griddata
|
| 13 |
from scipy.spatial import ConvexHull, QhullError
|
|
|
|
| 16 |
import branca.colormap as cm
|
| 17 |
from branca.element import Element
|
| 18 |
from html import escape
|
| 19 |
+
from typing import Any
|
| 20 |
from app.core.map_layers import add_bairros_layer, add_indice_marker, add_zoom_responsive_circle_markers
|
| 21 |
|
| 22 |
# ============================================================
|
|
|
|
| 608 |
return inside
|
| 609 |
|
| 610 |
|
| 611 |
+
def _aplicar_jitter_sobrepostos(
|
| 612 |
+
df_mapa: pd.DataFrame,
|
| 613 |
+
lat_col: str,
|
| 614 |
+
lon_col: str,
|
| 615 |
+
lat_plot_col: str,
|
| 616 |
+
lon_plot_col: str,
|
| 617 |
+
) -> pd.DataFrame:
|
| 618 |
+
"""
|
| 619 |
+
Aplica jitter visual apenas em pontos com coordenadas idênticas.
|
| 620 |
+
Mantém as coordenadas originais intactas para cálculos e filtros.
|
| 621 |
+
"""
|
| 622 |
+
df_plot = df_mapa.copy()
|
| 623 |
+
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 624 |
+
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
| 625 |
+
|
| 626 |
+
if len(df_plot) <= 1:
|
| 627 |
+
return df_plot
|
| 628 |
+
|
| 629 |
+
chave_lat = df_plot[lat_col].round(7)
|
| 630 |
+
chave_lon = df_plot[lon_col].round(7)
|
| 631 |
+
grupos = df_plot.groupby([chave_lat, chave_lon], sort=False)
|
| 632 |
+
|
| 633 |
+
passo_metros = 4.0
|
| 634 |
+
max_raio_metros = 22.0
|
| 635 |
+
metros_por_grau_lat = 111_320.0
|
| 636 |
+
|
| 637 |
+
for _, idx_labels in grupos.indices.items():
|
| 638 |
+
if len(idx_labels) <= 1:
|
| 639 |
+
continue
|
| 640 |
+
idx_list = list(idx_labels)
|
| 641 |
+
base_lat = float(df_plot.at[idx_list[0], lat_plot_col])
|
| 642 |
+
base_lon = float(df_plot.at[idx_list[0], lon_plot_col])
|
| 643 |
+
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 644 |
+
continue
|
| 645 |
+
|
| 646 |
+
seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360
|
| 647 |
+
angulo_base = math.radians(seed_val)
|
| 648 |
+
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 649 |
+
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 650 |
+
|
| 651 |
+
for pos, idx_label in enumerate(idx_list):
|
| 652 |
+
if pos == 0:
|
| 653 |
+
continue
|
| 654 |
+
|
| 655 |
+
pos_ring = pos - 1
|
| 656 |
+
ring = 1
|
| 657 |
+
while pos_ring >= (6 * ring):
|
| 658 |
+
pos_ring -= 6 * ring
|
| 659 |
+
ring += 1
|
| 660 |
+
|
| 661 |
+
slots_ring = max(6 * ring, 1)
|
| 662 |
+
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 663 |
+
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 664 |
+
|
| 665 |
+
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 666 |
+
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 667 |
+
|
| 668 |
+
df_plot.at[idx_label, lat_plot_col] = base_lat + delta_lat
|
| 669 |
+
df_plot.at[idx_label, lon_plot_col] = base_lon + delta_lon
|
| 670 |
+
|
| 671 |
+
return df_plot
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def _montar_popup_registro_em_colunas(
|
| 675 |
+
idx: Any,
|
| 676 |
+
row: pd.Series,
|
| 677 |
+
allowed_cols: list[str],
|
| 678 |
+
max_itens_coluna: int = 8,
|
| 679 |
+
popup_uid: str | None = None,
|
| 680 |
+
) -> tuple[str, int]:
|
| 681 |
+
itens: list[tuple[str, str]] = []
|
| 682 |
+
for col in allowed_cols:
|
| 683 |
+
if col not in row.index:
|
| 684 |
+
continue
|
| 685 |
+
col_txt = str(col)
|
| 686 |
+
col_low = col_txt.lower()
|
| 687 |
+
if col_txt.startswith("__mesa_"):
|
| 688 |
+
continue
|
| 689 |
+
if col_low in {"lat", "latitude", "lon", "longitude"}:
|
| 690 |
+
continue
|
| 691 |
+
val = row[col]
|
| 692 |
+
if isinstance(val, (int, float, np.floating, np.integer)) and np.isfinite(val):
|
| 693 |
+
val_fmt = f"{float(val):.2f}"
|
| 694 |
+
else:
|
| 695 |
+
val_fmt = str(val)
|
| 696 |
+
itens.append((col_txt, val_fmt))
|
| 697 |
+
|
| 698 |
+
if not itens:
|
| 699 |
+
return f"<b>Índice: {escape(str(idx))}</b>", 320
|
| 700 |
+
|
| 701 |
+
paginas = [itens[i:i + max_itens_coluna] for i in range(0, len(itens), max_itens_coluna)]
|
| 702 |
+
popup_uid = popup_uid or f"mesa-pop-{abs(hash(str(idx))) % 10_000_000}"
|
| 703 |
+
|
| 704 |
+
pages_html = []
|
| 705 |
+
botoes_html = []
|
| 706 |
+
for pagina_idx, pagina_itens in enumerate(paginas):
|
| 707 |
+
trs = "".join([
|
| 708 |
+
"<tr style='border-bottom:1px solid #e9ecef;'>"
|
| 709 |
+
f"<td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500;'>{escape(c)}</td>"
|
| 710 |
+
f"<td style='padding:4px 0; text-align:right; color:#495057;'>{escape(v)}</td>"
|
| 711 |
+
"</tr>"
|
| 712 |
+
for c, v in pagina_itens
|
| 713 |
+
])
|
| 714 |
+
display = "block" if pagina_idx == 0 else "none"
|
| 715 |
+
pages_html.append(
|
| 716 |
+
f"<div class='mesa-popup-page' data-page='{pagina_idx}' style='display:{display};'>"
|
| 717 |
+
f"<table style='border-collapse:collapse; font-size:12px; width:100%;'>{trs}</table>"
|
| 718 |
+
"</div>"
|
| 719 |
+
)
|
| 720 |
+
botao_style = (
|
| 721 |
+
"border:1px solid #9fb4c8; background:#eaf1f7; border-radius:6px; "
|
| 722 |
+
"padding:2px 7px; font-size:11px; cursor:pointer; color:#2f4b66;"
|
| 723 |
+
if pagina_idx == 0
|
| 724 |
+
else
|
| 725 |
+
"border:1px solid #ced8e2; background:#fff; border-radius:6px; "
|
| 726 |
+
"padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;"
|
| 727 |
+
)
|
| 728 |
+
onclick = (
|
| 729 |
+
f"var root=document.getElementById('{popup_uid}');"
|
| 730 |
+
"if(!root){return false;}"
|
| 731 |
+
"var pages=root.querySelectorAll('.mesa-popup-page');"
|
| 732 |
+
"for(var i=0;i<pages.length;i++){pages[i].style.display=(i==="
|
| 733 |
+
f"{pagina_idx}"
|
| 734 |
+
")?'block':'none';}"
|
| 735 |
+
"var btns=root.querySelectorAll('[data-page-btn]');"
|
| 736 |
+
"for(var j=0;j<btns.length;j++){btns[j].style.background='#fff';btns[j].style.borderColor='#ced8e2';btns[j].style.color='#4e6479';}"
|
| 737 |
+
"this.style.background='#eaf1f7';this.style.borderColor='#9fb4c8';this.style.color='#2f4b66';"
|
| 738 |
+
"return false;"
|
| 739 |
+
)
|
| 740 |
+
botoes_html.append(
|
| 741 |
+
f"<button type='button' data-page-btn='1' style=\"{botao_style}\" onclick=\"{onclick}\">"
|
| 742 |
+
f"{pagina_idx + 1}</button>"
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
controls_html = ""
|
| 746 |
+
if len(paginas) > 1:
|
| 747 |
+
controls_html = (
|
| 748 |
+
"<div class='mesa-popup-controls' style='display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; align-items:center;'>"
|
| 749 |
+
f"<span style='font-size:11px; color:#5d7388; margin-right:2px;'>Páginas:</span>{''.join(botoes_html)}"
|
| 750 |
+
"</div>"
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
popup_html = (
|
| 754 |
+
f"<div id='{popup_uid}' style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">"
|
| 755 |
+
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 756 |
+
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 757 |
+
f"<div class='mesa-popup-pages'>{''.join(pages_html)}</div>"
|
| 758 |
+
f"{controls_html}"
|
| 759 |
+
"</div></div>"
|
| 760 |
+
)
|
| 761 |
+
return popup_html, 430
|
| 762 |
+
|
| 763 |
+
|
| 764 |
def _normalizar_stops_cor(
|
| 765 |
cor_stops: list[float] | None,
|
| 766 |
colors: list[str],
|
|
|
|
| 1129 |
# Camada de índices (oculta por padrão, ativável pelo controle de camadas)
|
| 1130 |
mostrar_indices = (not modo_calor and not modo_superficie) and len(df_mapa) <= 800
|
| 1131 |
camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
|
| 1132 |
+
lat_plot_col = "__mesa_lat_plot__"
|
| 1133 |
+
lon_plot_col = "__mesa_lon_plot__"
|
| 1134 |
+
if not modo_calor and not modo_superficie:
|
| 1135 |
+
df_plot_pontos = _aplicar_jitter_sobrepostos(
|
| 1136 |
+
df_mapa,
|
| 1137 |
+
lat_col=lat_real,
|
| 1138 |
+
lon_col=lon_real,
|
| 1139 |
+
lat_plot_col=lat_plot_col,
|
| 1140 |
+
lon_plot_col=lon_plot_col,
|
| 1141 |
+
)
|
| 1142 |
+
else:
|
| 1143 |
+
df_plot_pontos = df_mapa.copy()
|
| 1144 |
+
df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
|
| 1145 |
+
df_plot_pontos[lon_plot_col] = df_plot_pontos[lon_real]
|
| 1146 |
|
| 1147 |
if modo_superficie:
|
| 1148 |
superficie_ok = _adicionar_superficie_continua(
|
|
|
|
| 1243 |
).add_to(m)
|
| 1244 |
elif not modo_superficie:
|
| 1245 |
# Adiciona pontos
|
| 1246 |
+
for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()):
|
| 1247 |
# Cor do ponto
|
| 1248 |
if colormap and cor_col:
|
| 1249 |
cor = colormap(row[cor_col])
|
|
|
|
| 1260 |
peso = 3 if idx == indice_destacado else 1
|
| 1261 |
|
| 1262 |
# Popup com informações
|
| 1263 |
+
popup_cols: list[str]
|
| 1264 |
+
if len(df_plot_pontos) <= 1200:
|
| 1265 |
+
popup_cols = [str(c) for c in df_plot_pontos.columns]
|
| 1266 |
+
elif tamanho_col and tamanho_col in df_plot_pontos.columns:
|
| 1267 |
+
popup_cols = [str(tamanho_col)]
|
| 1268 |
+
else:
|
| 1269 |
+
popup_cols = []
|
| 1270 |
+
popup_html, popup_width = _montar_popup_registro_em_colunas(
|
| 1271 |
+
idx,
|
| 1272 |
+
row,
|
| 1273 |
+
popup_cols,
|
| 1274 |
+
max_itens_coluna=8,
|
| 1275 |
+
popup_uid=f"mesa-pop-{marker_ordem}",
|
| 1276 |
+
)
|
| 1277 |
|
| 1278 |
# Tooltip (hover): índice + variável selecionada no dropdown
|
| 1279 |
tooltip_html = (
|
|
|
|
| 1291 |
tooltip_html += "</div>"
|
| 1292 |
|
| 1293 |
marcador = folium.CircleMarker(
|
| 1294 |
+
location=[row[lat_plot_col], row[lon_plot_col]],
|
| 1295 |
radius=raio,
|
| 1296 |
+
popup=folium.Popup(popup_html, max_width=popup_width, auto_pan=False),
|
| 1297 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 1298 |
color='black',
|
| 1299 |
weight=peso,
|
|
|
|
| 1307 |
if mostrar_indices and camada_indices is not None:
|
| 1308 |
add_indice_marker(
|
| 1309 |
camada_indices,
|
| 1310 |
+
lat=float(row[lat_plot_col]),
|
| 1311 |
+
lon=float(row[lon_plot_col]),
|
| 1312 |
indice=idx,
|
| 1313 |
)
|
| 1314 |
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -11,6 +11,7 @@ import os
|
|
| 11 |
import re
|
| 12 |
import traceback
|
| 13 |
from datetime import datetime
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
# Importações para gráficos (trazidas de graficos.py)
|
|
@@ -670,6 +671,135 @@ def formatar_escalas_html(escalas_raw):
|
|
| 670 |
|
| 671 |
return f"""<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">{cards_html}</div></div>"""
|
| 672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
# ============================================================
|
| 674 |
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
| 675 |
# ============================================================
|
|
@@ -786,6 +916,15 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 786 |
|
| 787 |
mostrar_indices = len(df_mapa) <= 800
|
| 788 |
camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
|
| 790 |
tooltip_col = None
|
| 791 |
tooltip_key = None
|
|
@@ -800,7 +939,7 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 800 |
df_mapa[tooltip_key] = serie_tooltip
|
| 801 |
|
| 802 |
# Adiciona pontos
|
| 803 |
-
for idx, row in
|
| 804 |
# Cor do ponto
|
| 805 |
if colormap and cor_key and pd.notna(row[cor_key]):
|
| 806 |
cor = colormap(row[cor_key])
|
|
@@ -830,19 +969,11 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 830 |
val_fmt = str(val)
|
| 831 |
itens.append((col, val_fmt))
|
| 832 |
|
| 833 |
-
MAX_ITENS = 8
|
| 834 |
-
colunas_html = []
|
| 835 |
-
for i in range(0, len(itens), MAX_ITENS):
|
| 836 |
-
chunk = itens[i:i+MAX_ITENS]
|
| 837 |
-
trs = "".join([f"<tr style='border-bottom: 1px solid #e9ecef;'><td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500;'>{c}</td><td style='padding:4px 0; text-align:right; color:#495057;'>{v}</td></tr>" for c, v in chunk])
|
| 838 |
-
style = "border-left: 2px solid #dee2e6; padding-left: 20px;" if i > 0 else ""
|
| 839 |
-
colunas_html.append(f"<div style='flex: 0 0 auto; {style}'><table style='border-collapse:collapse; font-size:12px;'>{trs}</table></div>")
|
| 840 |
-
|
| 841 |
-
popup_html = f"""<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;"><div style="display:flex; gap:20px;">{"".join(colunas_html)}</div></div></div>"""
|
| 842 |
-
|
| 843 |
# Tooltip (hover): índice + variável selecionada no dropdown (ou dependente como fallback)
|
| 844 |
# Usa coluna "index" (original, gerada pelo reset_index) quando disponível
|
| 845 |
idx_display = int(row["index"]) if "index" in row.index else idx
|
|
|
|
|
|
|
| 846 |
tooltip_html = (
|
| 847 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px;"
|
| 848 |
" line-height:1.7; padding:2px 4px;'>"
|
|
@@ -865,9 +996,9 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 865 |
tooltip_html += "</div>"
|
| 866 |
|
| 867 |
marcador = folium.CircleMarker(
|
| 868 |
-
location=[row[
|
| 869 |
radius=raio,
|
| 870 |
-
popup=folium.Popup(popup_html, max_width=
|
| 871 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 872 |
color='black',
|
| 873 |
weight=1,
|
|
@@ -880,8 +1011,8 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 880 |
if mostrar_indices and camada_indices is not None:
|
| 881 |
add_indice_marker(
|
| 882 |
camada_indices,
|
| 883 |
-
lat=float(row[
|
| 884 |
-
lon=float(row[
|
| 885 |
indice=idx_display,
|
| 886 |
)
|
| 887 |
|
|
|
|
| 11 |
import re
|
| 12 |
import traceback
|
| 13 |
from datetime import datetime
|
| 14 |
+
from html import escape
|
| 15 |
|
| 16 |
|
| 17 |
# Importações para gráficos (trazidas de graficos.py)
|
|
|
|
| 671 |
|
| 672 |
return f"""<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">{cards_html}</div></div>"""
|
| 673 |
|
| 674 |
+
|
| 675 |
+
def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plot_col):
|
| 676 |
+
"""
|
| 677 |
+
Aplica jitter visual mínimo para separar pontos com coordenadas idênticas.
|
| 678 |
+
Não altera as coordenadas originais da base.
|
| 679 |
+
"""
|
| 680 |
+
df_plot = df_mapa.copy()
|
| 681 |
+
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 682 |
+
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
| 683 |
+
|
| 684 |
+
if len(df_plot) <= 1:
|
| 685 |
+
return df_plot
|
| 686 |
+
|
| 687 |
+
chave_lat = df_plot[lat_col].round(7)
|
| 688 |
+
chave_lon = df_plot[lon_col].round(7)
|
| 689 |
+
grupos = df_plot.groupby([chave_lat, chave_lon], sort=False)
|
| 690 |
+
|
| 691 |
+
passo_metros = 4.0
|
| 692 |
+
max_raio_metros = 22.0
|
| 693 |
+
metros_por_grau_lat = 111_320.0
|
| 694 |
+
|
| 695 |
+
for _, idx_labels in grupos.indices.items():
|
| 696 |
+
if len(idx_labels) <= 1:
|
| 697 |
+
continue
|
| 698 |
+
idx_list = list(idx_labels)
|
| 699 |
+
base_lat = float(df_plot.at[idx_list[0], lat_plot_col])
|
| 700 |
+
base_lon = float(df_plot.at[idx_list[0], lon_plot_col])
|
| 701 |
+
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 702 |
+
continue
|
| 703 |
+
|
| 704 |
+
seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360
|
| 705 |
+
angulo_base = math.radians(seed_val)
|
| 706 |
+
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 707 |
+
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 708 |
+
|
| 709 |
+
for pos, idx_label in enumerate(idx_list):
|
| 710 |
+
if pos == 0:
|
| 711 |
+
continue
|
| 712 |
+
|
| 713 |
+
pos_ring = pos - 1
|
| 714 |
+
ring = 1
|
| 715 |
+
while pos_ring >= (6 * ring):
|
| 716 |
+
pos_ring -= 6 * ring
|
| 717 |
+
ring += 1
|
| 718 |
+
|
| 719 |
+
slots_ring = max(6 * ring, 1)
|
| 720 |
+
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 721 |
+
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 722 |
+
|
| 723 |
+
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 724 |
+
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 725 |
+
|
| 726 |
+
df_plot.at[idx_label, lat_plot_col] = base_lat + delta_lat
|
| 727 |
+
df_plot.at[idx_label, lon_plot_col] = base_lon + delta_lon
|
| 728 |
+
|
| 729 |
+
return df_plot
|
| 730 |
+
|
| 731 |
+
|
| 732 |
+
def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
| 733 |
+
if not itens:
|
| 734 |
+
html = (
|
| 735 |
+
"<div style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">"
|
| 736 |
+
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 737 |
+
"<div style=\"padding:12px 15px; background:#f8f9fa; color:#6c757d; font-size:12px;\">Sem variáveis para exibir.</div>"
|
| 738 |
+
"</div>"
|
| 739 |
+
)
|
| 740 |
+
return html, 360
|
| 741 |
+
|
| 742 |
+
paginas = [itens[i:i + max_itens_pagina] for i in range(0, len(itens), max_itens_pagina)]
|
| 743 |
+
pages_html = []
|
| 744 |
+
botoes_html = []
|
| 745 |
+
|
| 746 |
+
for page_idx, page_items in enumerate(paginas):
|
| 747 |
+
trs = "".join([
|
| 748 |
+
"<tr style='border-bottom:1px solid #e9ecef;'>"
|
| 749 |
+
f"<td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500;'>{escape(str(c))}</td>"
|
| 750 |
+
f"<td style='padding:4px 0; text-align:right; color:#495057;'>{escape(str(v))}</td>"
|
| 751 |
+
"</tr>"
|
| 752 |
+
for c, v in page_items
|
| 753 |
+
])
|
| 754 |
+
display = "block" if page_idx == 0 else "none"
|
| 755 |
+
pages_html.append(
|
| 756 |
+
f"<div class='mesa-popup-page' data-page='{page_idx}' style='display:{display};'>"
|
| 757 |
+
f"<table style='border-collapse:collapse; font-size:12px; width:100%;'>{trs}</table>"
|
| 758 |
+
"</div>"
|
| 759 |
+
)
|
| 760 |
+
botao_style = (
|
| 761 |
+
"border:1px solid #9fb4c8; background:#eaf1f7; border-radius:6px; "
|
| 762 |
+
"padding:2px 7px; font-size:11px; cursor:pointer; color:#2f4b66;"
|
| 763 |
+
if page_idx == 0
|
| 764 |
+
else
|
| 765 |
+
"border:1px solid #ced8e2; background:#fff; border-radius:6px; "
|
| 766 |
+
"padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;"
|
| 767 |
+
)
|
| 768 |
+
onclick = (
|
| 769 |
+
f"var root=document.getElementById('{popup_uid}');"
|
| 770 |
+
"if(!root){return false;}"
|
| 771 |
+
"var pages=root.querySelectorAll('.mesa-popup-page');"
|
| 772 |
+
"for(var i=0;i<pages.length;i++){pages[i].style.display=(i==="
|
| 773 |
+
f"{page_idx}"
|
| 774 |
+
")?'block':'none';}"
|
| 775 |
+
"var btns=root.querySelectorAll('[data-page-btn]');"
|
| 776 |
+
"for(var j=0;j<btns.length;j++){btns[j].style.background='#fff';btns[j].style.borderColor='#ced8e2';btns[j].style.color='#4e6479';}"
|
| 777 |
+
"this.style.background='#eaf1f7';this.style.borderColor='#9fb4c8';this.style.color='#2f4b66';"
|
| 778 |
+
"return false;"
|
| 779 |
+
)
|
| 780 |
+
botoes_html.append(
|
| 781 |
+
f"<button type='button' data-page-btn='1' style=\"{botao_style}\" onclick=\"{onclick}\">"
|
| 782 |
+
f"{page_idx + 1}</button>"
|
| 783 |
+
)
|
| 784 |
+
|
| 785 |
+
controls_html = ""
|
| 786 |
+
if len(paginas) > 1:
|
| 787 |
+
controls_html = (
|
| 788 |
+
"<div class='mesa-popup-controls' style='display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; align-items:center;'>"
|
| 789 |
+
f"<span style='font-size:11px; color:#5d7388; margin-right:2px;'>Páginas:</span>{''.join(botoes_html)}"
|
| 790 |
+
"</div>"
|
| 791 |
+
)
|
| 792 |
+
|
| 793 |
+
html = (
|
| 794 |
+
f"<div id='{popup_uid}' style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">"
|
| 795 |
+
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 796 |
+
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 797 |
+
f"<div class='mesa-popup-pages'>{''.join(pages_html)}</div>"
|
| 798 |
+
f"{controls_html}"
|
| 799 |
+
"</div></div>"
|
| 800 |
+
)
|
| 801 |
+
return html, 430
|
| 802 |
+
|
| 803 |
# ============================================================
|
| 804 |
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
| 805 |
# ============================================================
|
|
|
|
| 916 |
|
| 917 |
mostrar_indices = len(df_mapa) <= 800
|
| 918 |
camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
|
| 919 |
+
lat_plot_key = "__mesa_lat_plot__"
|
| 920 |
+
lon_plot_key = "__mesa_lon_plot__"
|
| 921 |
+
df_plot_pontos = _aplicar_jitter_sobrepostos(
|
| 922 |
+
df_mapa,
|
| 923 |
+
lat_col=lat_key,
|
| 924 |
+
lon_col=lon_key,
|
| 925 |
+
lat_plot_col=lat_plot_key,
|
| 926 |
+
lon_plot_col=lon_plot_key,
|
| 927 |
+
)
|
| 928 |
|
| 929 |
tooltip_col = None
|
| 930 |
tooltip_key = None
|
|
|
|
| 939 |
df_mapa[tooltip_key] = serie_tooltip
|
| 940 |
|
| 941 |
# Adiciona pontos
|
| 942 |
+
for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()):
|
| 943 |
# Cor do ponto
|
| 944 |
if colormap and cor_key and pd.notna(row[cor_key]):
|
| 945 |
cor = colormap(row[cor_key])
|
|
|
|
| 969 |
val_fmt = str(val)
|
| 970 |
itens.append((col, val_fmt))
|
| 971 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
# Tooltip (hover): índice + variável selecionada no dropdown (ou dependente como fallback)
|
| 973 |
# Usa coluna "index" (original, gerada pelo reset_index) quando disponível
|
| 974 |
idx_display = int(row["index"]) if "index" in row.index else idx
|
| 975 |
+
popup_uid = f"mesa-pop-{marker_ordem}"
|
| 976 |
+
popup_html, popup_width = _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8)
|
| 977 |
tooltip_html = (
|
| 978 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px;"
|
| 979 |
" line-height:1.7; padding:2px 4px;'>"
|
|
|
|
| 996 |
tooltip_html += "</div>"
|
| 997 |
|
| 998 |
marcador = folium.CircleMarker(
|
| 999 |
+
location=[row[lat_plot_key], row[lon_plot_key]],
|
| 1000 |
radius=raio,
|
| 1001 |
+
popup=folium.Popup(popup_html, max_width=popup_width),
|
| 1002 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 1003 |
color='black',
|
| 1004 |
weight=1,
|
|
|
|
| 1011 |
if mostrar_indices and camada_indices is not None:
|
| 1012 |
add_indice_marker(
|
| 1013 |
camada_indices,
|
| 1014 |
+
lat=float(row[lat_plot_key]),
|
| 1015 |
+
lon=float(row[lon_plot_key]),
|
| 1016 |
indice=idx_display,
|
| 1017 |
)
|
| 1018 |
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -2114,7 +2114,45 @@ def atualizar_mapa(session: SessionState, var_mapa: str | None, modo_mapa: str |
|
|
| 2114 |
return {"mapa_html": mapa_html}
|
| 2115 |
|
| 2116 |
|
| 2117 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2118 |
tabela_metricas = session.tabela_metricas_estado
|
| 2119 |
if tabela_metricas is None or tabela_metricas.empty:
|
| 2120 |
raise HTTPException(status_code=400, detail="Ajuste o modelo para gerar métricas de resíduos")
|
|
@@ -2127,23 +2165,35 @@ def atualizar_mapa_residuos(session: SessionState, var_mapa: str | None, modo_ma
|
|
| 2127 |
modo = str(modo_mapa or "pontos").strip().lower()
|
| 2128 |
if modo not in {"pontos", "calor", "superficie"}:
|
| 2129 |
modo = "pontos"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2130 |
|
| 2131 |
mapa_html = charts.criar_mapa(
|
| 2132 |
df,
|
| 2133 |
tamanho_col=var_escolhida,
|
| 2134 |
modo=modo,
|
| 2135 |
-
cor_vmin=
|
| 2136 |
-
cor_vmax=
|
| 2137 |
-
cor_caption=
|
| 2138 |
cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
|
| 2139 |
-
|
| 2140 |
-
|
| 2141 |
-
cor_tick_labels=["-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5"],
|
| 2142 |
)
|
| 2143 |
return {
|
| 2144 |
"mapa_html": mapa_html,
|
| 2145 |
"variavel_mapa": var_escolhida,
|
| 2146 |
"modo_mapa": modo,
|
|
|
|
| 2147 |
}
|
| 2148 |
|
| 2149 |
|
|
|
|
| 2114 |
return {"mapa_html": mapa_html}
|
| 2115 |
|
| 2116 |
|
| 2117 |
+
def _normalizar_extremo_abs_residuos(valor: float | None) -> float | None:
|
| 2118 |
+
if valor is None:
|
| 2119 |
+
return None
|
| 2120 |
+
try:
|
| 2121 |
+
extremo = float(valor)
|
| 2122 |
+
except (TypeError, ValueError):
|
| 2123 |
+
return None
|
| 2124 |
+
if not np.isfinite(extremo):
|
| 2125 |
+
return None
|
| 2126 |
+
if abs(extremo) <= 0:
|
| 2127 |
+
return None
|
| 2128 |
+
return float(np.clip(abs(extremo), 0.5, 20.0))
|
| 2129 |
+
|
| 2130 |
+
|
| 2131 |
+
def _montar_ticks_residuos(extremo_abs: float) -> tuple[list[float], list[str]]:
|
| 2132 |
+
inteiro = int(round(extremo_abs))
|
| 2133 |
+
if np.isclose(extremo_abs, inteiro) and inteiro <= 12:
|
| 2134 |
+
valores = [float(v) for v in range(-inteiro, inteiro + 1)] if inteiro > 0 else [-1.0, 0.0, 1.0]
|
| 2135 |
+
else:
|
| 2136 |
+
valores = [float(v) for v in np.linspace(-extremo_abs, extremo_abs, 11)]
|
| 2137 |
+
|
| 2138 |
+
def _label(v: float) -> str:
|
| 2139 |
+
if np.isclose(v, round(v)):
|
| 2140 |
+
return str(int(round(v)))
|
| 2141 |
+
return f"{v:.2f}".rstrip("0").rstrip(".")
|
| 2142 |
+
|
| 2143 |
+
labels = [_label(v) for v in valores]
|
| 2144 |
+
if labels:
|
| 2145 |
+
labels[0] = f"<= {labels[0]}"
|
| 2146 |
+
labels[-1] = f">= {labels[-1]}"
|
| 2147 |
+
return valores, labels
|
| 2148 |
+
|
| 2149 |
+
|
| 2150 |
+
def atualizar_mapa_residuos(
|
| 2151 |
+
session: SessionState,
|
| 2152 |
+
var_mapa: str | None,
|
| 2153 |
+
modo_mapa: str | None = None,
|
| 2154 |
+
escala_extremo_abs: float | None = None,
|
| 2155 |
+
) -> dict[str, Any]:
|
| 2156 |
tabela_metricas = session.tabela_metricas_estado
|
| 2157 |
if tabela_metricas is None or tabela_metricas.empty:
|
| 2158 |
raise HTTPException(status_code=400, detail="Ajuste o modelo para gerar métricas de resíduos")
|
|
|
|
| 2165 |
modo = str(modo_mapa or "pontos").strip().lower()
|
| 2166 |
if modo not in {"pontos", "calor", "superficie"}:
|
| 2167 |
modo = "pontos"
|
| 2168 |
+
extremo_abs = _normalizar_extremo_abs_residuos(escala_extremo_abs)
|
| 2169 |
+
cor_vmin = None if extremo_abs is None else -extremo_abs
|
| 2170 |
+
cor_vmax = None if extremo_abs is None else extremo_abs
|
| 2171 |
+
ticks_valores, ticks_labels = (None, None)
|
| 2172 |
+
if extremo_abs is not None:
|
| 2173 |
+
ticks_valores, ticks_labels = _montar_ticks_residuos(extremo_abs)
|
| 2174 |
+
extremo_txt = None if extremo_abs is None else f"{extremo_abs:.2f}".rstrip("0").rstrip(".")
|
| 2175 |
+
caption = (
|
| 2176 |
+
"Resíduo Pad. (escala livre conforme limites dos dados)"
|
| 2177 |
+
if extremo_abs is None
|
| 2178 |
+
else f"Resíduo Pad. (escala fixa -{extremo_txt} a +{extremo_txt})"
|
| 2179 |
+
)
|
| 2180 |
|
| 2181 |
mapa_html = charts.criar_mapa(
|
| 2182 |
df,
|
| 2183 |
tamanho_col=var_escolhida,
|
| 2184 |
modo=modo,
|
| 2185 |
+
cor_vmin=cor_vmin,
|
| 2186 |
+
cor_vmax=cor_vmax,
|
| 2187 |
+
cor_caption=caption,
|
| 2188 |
cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
|
| 2189 |
+
cor_tick_values=ticks_valores,
|
| 2190 |
+
cor_tick_labels=ticks_labels,
|
|
|
|
| 2191 |
)
|
| 2192 |
return {
|
| 2193 |
"mapa_html": mapa_html,
|
| 2194 |
"variavel_mapa": var_escolhida,
|
| 2195 |
"modo_mapa": modo,
|
| 2196 |
+
"escala_extremo_abs": extremo_abs,
|
| 2197 |
}
|
| 2198 |
|
| 2199 |
|
frontend/src/App.jsx
CHANGED
|
@@ -18,7 +18,7 @@ const TABS = [
|
|
| 18 |
]
|
| 19 |
|
| 20 |
export default function App() {
|
| 21 |
-
const [activeTab, setActiveTab] = useState(
|
| 22 |
const [showStartupIntro, setShowStartupIntro] = useState(true)
|
| 23 |
const [sessionId, setSessionId] = useState('')
|
| 24 |
const [bootError, setBootError] = useState('')
|
|
@@ -61,7 +61,7 @@ export default function App() {
|
|
| 61 |
setLogsEvents([])
|
| 62 |
setLogsError('')
|
| 63 |
setLogsPage(1)
|
| 64 |
-
setActiveTab(
|
| 65 |
setShowStartupIntro(true)
|
| 66 |
setAuthError(message)
|
| 67 |
}
|
|
|
|
| 18 |
]
|
| 19 |
|
| 20 |
export default function App() {
|
| 21 |
+
const [activeTab, setActiveTab] = useState('')
|
| 22 |
const [showStartupIntro, setShowStartupIntro] = useState(true)
|
| 23 |
const [sessionId, setSessionId] = useState('')
|
| 24 |
const [bootError, setBootError] = useState('')
|
|
|
|
| 61 |
setLogsEvents([])
|
| 62 |
setLogsError('')
|
| 63 |
setLogsPage(1)
|
| 64 |
+
setActiveTab('')
|
| 65 |
setShowStartupIntro(true)
|
| 66 |
setAuthError(message)
|
| 67 |
}
|
frontend/src/api.js
CHANGED
|
@@ -230,10 +230,11 @@ export const api = {
|
|
| 230 |
variavel_mapa: variavelMapa,
|
| 231 |
modo_mapa: modoMapa,
|
| 232 |
}),
|
| 233 |
-
updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos') => postJson('/api/elaboracao/residuos/map/update', {
|
| 234 |
session_id: sessionId,
|
| 235 |
variavel_mapa: variavelMapa,
|
| 236 |
modo_mapa: modoMapa,
|
|
|
|
| 237 |
}),
|
| 238 |
previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }),
|
| 239 |
applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }),
|
|
|
|
| 230 |
variavel_mapa: variavelMapa,
|
| 231 |
modo_mapa: modoMapa,
|
| 232 |
}),
|
| 233 |
+
updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos', escalaExtremoAbs = null) => postJson('/api/elaboracao/residuos/map/update', {
|
| 234 |
session_id: sessionId,
|
| 235 |
variavel_mapa: variavelMapa,
|
| 236 |
modo_mapa: modoMapa,
|
| 237 |
+
escala_extremo_abs: escalaExtremoAbs,
|
| 238 |
}),
|
| 239 |
previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }),
|
| 240 |
applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }),
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -33,6 +33,9 @@ const MAPA_MODO_PONTOS = 'pontos'
|
|
| 33 |
const MAPA_MODO_CALOR = 'calor'
|
| 34 |
const MAPA_MODO_SUPERFICIE = 'superficie'
|
| 35 |
const MAPA_RESIDUOS_VARIAVEL = 'Resíduo Pad.'
|
|
|
|
|
|
|
|
|
|
| 36 |
const OUTLIER_RECURSIVO_TOOLTIP = 'Aplicar com recursividade executa os mesmos filtros em ciclos sucessivos: nos bastidores, simula a exclusão dos índices encontrados, recalcula o ajuste do modelo e as métricas de outlier e reaplica os filtros, repetindo até não surgir nenhum índice novo. Para você, o resultado prático é que o campo "A excluir" é preenchido automaticamente com o conjunto total de índices encontrados nessa simulação recursiva.'
|
| 37 |
|
| 38 |
function grauBadgeClass(value) {
|
|
@@ -166,6 +169,36 @@ function formatMetric4(value) {
|
|
| 166 |
return Number.isFinite(num) ? num.toFixed(4) : '-'
|
| 167 |
}
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
function sleep(ms) {
|
| 170 |
return new Promise((resolve) => {
|
| 171 |
window.setTimeout(resolve, ms)
|
|
@@ -704,6 +737,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 704 |
const [mapaGerado, setMapaGerado] = useState(false)
|
| 705 |
const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
|
| 706 |
const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
|
|
|
|
| 707 |
const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
|
| 708 |
|
| 709 |
const [coordsInfo, setCoordsInfo] = useState(null)
|
|
@@ -992,6 +1026,46 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 992 |
|
| 993 |
return Array.from(indices)
|
| 994 |
}, [fit?.tabela_metricas, filtros, outlierHighlightIndexColumn])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 995 |
const transformacaoAplicadaYBadge = useMemo(
|
| 996 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 997 |
[transformacoesAplicadas],
|
|
@@ -1675,6 +1749,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1675 |
setSecao13InterativoEixoYResiduo('residuo_pad')
|
| 1676 |
setSecao13InterativoEixoYColuna('')
|
| 1677 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
|
|
|
| 1678 |
setMapaResiduosHtml('')
|
| 1679 |
setMapaResiduosGerado(false)
|
| 1680 |
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
|
@@ -1824,6 +1899,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1824 |
setMapaResiduosGerado(false)
|
| 1825 |
setMapaResiduosHtml('')
|
| 1826 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
|
|
|
| 1827 |
setGeoAuto200(true)
|
| 1828 |
setSelectedSheet('')
|
| 1829 |
setRequiresSheet(false)
|
|
@@ -1854,6 +1930,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1854 |
setMapaResiduosGerado(false)
|
| 1855 |
setMapaResiduosHtml('')
|
| 1856 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
|
|
|
| 1857 |
setGeoAuto200(true)
|
| 1858 |
setSelectedSheet('')
|
| 1859 |
setRequiresSheet(false)
|
|
@@ -1913,6 +1990,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1913 |
setMapaResiduosGerado(false)
|
| 1914 |
setMapaResiduosHtml('')
|
| 1915 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
|
|
|
| 1916 |
setGeoAuto200(true)
|
| 1917 |
const resp = await api.confirmSheet(sessionId, selectedSheet)
|
| 1918 |
setTipoFonteDados('tabular')
|
|
@@ -2677,17 +2755,48 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 2677 |
async function onMapaResiduosModoChange(value) {
|
| 2678 |
setMapaResiduosModo(value)
|
| 2679 |
if (!sessionId || !mapaResiduosGerado) return
|
|
|
|
| 2680 |
await withBusy(async () => {
|
| 2681 |
-
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value)
|
| 2682 |
setMapaResiduosHtml(resp.mapa_html || '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2683 |
})
|
| 2684 |
}
|
| 2685 |
|
| 2686 |
async function onGerarMapaResiduos() {
|
| 2687 |
if (!sessionId) return
|
|
|
|
| 2688 |
await withBusy(async () => {
|
| 2689 |
-
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo)
|
| 2690 |
setMapaResiduosHtml(resp.mapa_html || '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2691 |
setMapaResiduosGerado(true)
|
| 2692 |
})
|
| 2693 |
}
|
|
@@ -4693,62 +4802,122 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 4693 |
</SectionBlock>
|
| 4694 |
|
| 4695 |
<SectionBlock step="16" title="Analisar Resíduos" subtitle="Métricas para identificação de observações influentes.">
|
| 4696 |
-
<
|
| 4697 |
-
<
|
| 4698 |
-
{
|
| 4699 |
-
<div className="
|
| 4700 |
-
<div className="
|
| 4701 |
-
<
|
| 4702 |
-
|
| 4703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4704 |
</div>
|
| 4705 |
-
<div className="section1-empty-hint">O mapa de resíduos padronizados será carregado somente após solicitação explícita.</div>
|
| 4706 |
</div>
|
| 4707 |
) : (
|
| 4708 |
-
<>
|
| 4709 |
-
|
| 4710 |
-
|
| 4711 |
-
|
| 4712 |
-
|
| 4713 |
-
|
| 4714 |
-
|
| 4715 |
-
|
| 4716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4717 |
</div>
|
|
|
|
| 4718 |
</div>
|
| 4719 |
-
|
| 4720 |
-
|
| 4721 |
-
|
| 4722 |
-
className="
|
| 4723 |
-
|
| 4724 |
-
|
| 4725 |
-
|
| 4726 |
-
|
| 4727 |
-
|
| 4728 |
-
|
| 4729 |
-
|
| 4730 |
-
|
| 4731 |
-
|
| 4732 |
-
|
| 4733 |
-
|
| 4734 |
-
|
| 4735 |
-
|
| 4736 |
-
|
| 4737 |
-
|
| 4738 |
-
|
| 4739 |
-
|
| 4740 |
-
|
| 4741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4742 |
</div>
|
| 4743 |
-
|
| 4744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4745 |
</div>
|
| 4746 |
-
<DataTable
|
| 4747 |
-
table={fit.tabela_metricas}
|
| 4748 |
-
maxHeight={320}
|
| 4749 |
-
highlightedRowIndices={outlierRowsHighlight}
|
| 4750 |
-
highlightIndexColumn={outlierHighlightIndexColumn}
|
| 4751 |
-
/>
|
| 4752 |
</SectionBlock>
|
| 4753 |
|
| 4754 |
<SectionBlock step="17" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
|
|
|
|
| 33 |
const MAPA_MODO_CALOR = 'calor'
|
| 34 |
const MAPA_MODO_SUPERFICIE = 'superficie'
|
| 35 |
const MAPA_RESIDUOS_VARIAVEL = 'Resíduo Pad.'
|
| 36 |
+
const MAPA_RESIDUOS_EXTREMO_LIVRE = 'livre'
|
| 37 |
+
const MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT = MAPA_RESIDUOS_EXTREMO_LIVRE
|
| 38 |
+
const MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS = [2, 3, 4, 5, 6, 7, 8, 10]
|
| 39 |
const OUTLIER_RECURSIVO_TOOLTIP = 'Aplicar com recursividade executa os mesmos filtros em ciclos sucessivos: nos bastidores, simula a exclusão dos índices encontrados, recalcula o ajuste do modelo e as métricas de outlier e reaplica os filtros, repetindo até não surgir nenhum índice novo. Para você, o resultado prático é que o campo "A excluir" é preenchido automaticamente com o conjunto total de índices encontrados nessa simulação recursiva.'
|
| 40 |
|
| 41 |
function grauBadgeClass(value) {
|
|
|
|
| 169 |
return Number.isFinite(num) ? num.toFixed(4) : '-'
|
| 170 |
}
|
| 171 |
|
| 172 |
+
function formatNumberBr(value, maximumFractionDigits = 4) {
|
| 173 |
+
const num = Number(value)
|
| 174 |
+
if (!Number.isFinite(num)) return '-'
|
| 175 |
+
return num.toLocaleString('pt-BR', {
|
| 176 |
+
minimumFractionDigits: 0,
|
| 177 |
+
maximumFractionDigits,
|
| 178 |
+
})
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function quantileLinear(sortedValues, q) {
|
| 182 |
+
if (!Array.isArray(sortedValues) || sortedValues.length === 0) return null
|
| 183 |
+
const clampedQ = Math.min(1, Math.max(0, Number(q)))
|
| 184 |
+
const pos = (sortedValues.length - 1) * clampedQ
|
| 185 |
+
const low = Math.floor(pos)
|
| 186 |
+
const high = Math.ceil(pos)
|
| 187 |
+
const lowValue = Number(sortedValues[low])
|
| 188 |
+
const highValue = Number(sortedValues[high])
|
| 189 |
+
if (!Number.isFinite(lowValue) || !Number.isFinite(highValue)) return null
|
| 190 |
+
if (low === high) return lowValue
|
| 191 |
+
return lowValue + ((highValue - lowValue) * (pos - low))
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function parseMapaResiduosExtremoAbs(value) {
|
| 195 |
+
const raw = String(value ?? '').trim().toLowerCase()
|
| 196 |
+
if (!raw || raw === MAPA_RESIDUOS_EXTREMO_LIVRE) return null
|
| 197 |
+
const parsed = Number(raw)
|
| 198 |
+
if (!Number.isFinite(parsed) || parsed <= 0) return null
|
| 199 |
+
return parsed
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
function sleep(ms) {
|
| 203 |
return new Promise((resolve) => {
|
| 204 |
window.setTimeout(resolve, ms)
|
|
|
|
| 737 |
const [mapaGerado, setMapaGerado] = useState(false)
|
| 738 |
const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
|
| 739 |
const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
|
| 740 |
+
const [mapaResiduosExtremoAbs, setMapaResiduosExtremoAbs] = useState(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 741 |
const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
|
| 742 |
|
| 743 |
const [coordsInfo, setCoordsInfo] = useState(null)
|
|
|
|
| 1026 |
|
| 1027 |
return Array.from(indices)
|
| 1028 |
}, [fit?.tabela_metricas, filtros, outlierHighlightIndexColumn])
|
| 1029 |
+
const resumoResiduoPadStats = useMemo(() => {
|
| 1030 |
+
const rows = fit?.tabela_metricas?.rows
|
| 1031 |
+
if (!Array.isArray(rows) || rows.length === 0) return null
|
| 1032 |
+
|
| 1033 |
+
const colunasCandidatas = ['Resíduo Pad.', 'Residuo Pad.', 'Resíduo Padronizado', 'Residuo Padronizado']
|
| 1034 |
+
let colunaResiduo = ''
|
| 1035 |
+
for (let i = 0; i < colunasCandidatas.length; i += 1) {
|
| 1036 |
+
const candidata = colunasCandidatas[i]
|
| 1037 |
+
if (rows.some((row) => row && typeof row === 'object' && Object.prototype.hasOwnProperty.call(row, candidata))) {
|
| 1038 |
+
colunaResiduo = candidata
|
| 1039 |
+
break
|
| 1040 |
+
}
|
| 1041 |
+
}
|
| 1042 |
+
if (!colunaResiduo) return null
|
| 1043 |
+
|
| 1044 |
+
const valores = rows
|
| 1045 |
+
.map((row) => toFiniteNumber(row?.[colunaResiduo]))
|
| 1046 |
+
.filter((item) => item !== null)
|
| 1047 |
+
.map((item) => Number(item))
|
| 1048 |
+
|
| 1049 |
+
if (valores.length === 0) return null
|
| 1050 |
+
const ordenados = [...valores].sort((a, b) => a - b)
|
| 1051 |
+
const min = ordenados[0]
|
| 1052 |
+
const max = ordenados[ordenados.length - 1]
|
| 1053 |
+
const q1 = quantileLinear(ordenados, 0.25)
|
| 1054 |
+
const mediana = quantileLinear(ordenados, 0.5)
|
| 1055 |
+
const q3 = quantileLinear(ordenados, 0.75)
|
| 1056 |
+
|
| 1057 |
+
return {
|
| 1058 |
+
minimo: min,
|
| 1059 |
+
q1,
|
| 1060 |
+
mediana,
|
| 1061 |
+
q3,
|
| 1062 |
+
maximo: max,
|
| 1063 |
+
}
|
| 1064 |
+
}, [fit?.tabela_metricas?.rows])
|
| 1065 |
+
const mapaResiduosExtremoAbsAtivo = useMemo(
|
| 1066 |
+
() => parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs),
|
| 1067 |
+
[mapaResiduosExtremoAbs],
|
| 1068 |
+
)
|
| 1069 |
const transformacaoAplicadaYBadge = useMemo(
|
| 1070 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 1071 |
[transformacoesAplicadas],
|
|
|
|
| 1749 |
setSecao13InterativoEixoYResiduo('residuo_pad')
|
| 1750 |
setSecao13InterativoEixoYColuna('')
|
| 1751 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 1752 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 1753 |
setMapaResiduosHtml('')
|
| 1754 |
setMapaResiduosGerado(false)
|
| 1755 |
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
|
|
|
| 1899 |
setMapaResiduosGerado(false)
|
| 1900 |
setMapaResiduosHtml('')
|
| 1901 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 1902 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 1903 |
setGeoAuto200(true)
|
| 1904 |
setSelectedSheet('')
|
| 1905 |
setRequiresSheet(false)
|
|
|
|
| 1930 |
setMapaResiduosGerado(false)
|
| 1931 |
setMapaResiduosHtml('')
|
| 1932 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 1933 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 1934 |
setGeoAuto200(true)
|
| 1935 |
setSelectedSheet('')
|
| 1936 |
setRequiresSheet(false)
|
|
|
|
| 1990 |
setMapaResiduosGerado(false)
|
| 1991 |
setMapaResiduosHtml('')
|
| 1992 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 1993 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 1994 |
setGeoAuto200(true)
|
| 1995 |
const resp = await api.confirmSheet(sessionId, selectedSheet)
|
| 1996 |
setTipoFonteDados('tabular')
|
|
|
|
| 2755 |
async function onMapaResiduosModoChange(value) {
|
| 2756 |
setMapaResiduosModo(value)
|
| 2757 |
if (!sessionId || !mapaResiduosGerado) return
|
| 2758 |
+
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 2759 |
await withBusy(async () => {
|
| 2760 |
+
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value, extremoAbs)
|
| 2761 |
setMapaResiduosHtml(resp.mapa_html || '')
|
| 2762 |
+
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 2763 |
+
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 2764 |
+
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
| 2765 |
+
} else {
|
| 2766 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_LIVRE)
|
| 2767 |
+
}
|
| 2768 |
+
})
|
| 2769 |
+
}
|
| 2770 |
+
|
| 2771 |
+
async function onMapaResiduosExtremoAbsChange(value) {
|
| 2772 |
+
const raw = String(value || MAPA_RESIDUOS_EXTREMO_LIVRE)
|
| 2773 |
+
setMapaResiduosExtremoAbs(raw)
|
| 2774 |
+
const extremoAbs = parseMapaResiduosExtremoAbs(raw)
|
| 2775 |
+
if (!sessionId || !mapaResiduosGerado) return
|
| 2776 |
+
await withBusy(async () => {
|
| 2777 |
+
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 2778 |
+
setMapaResiduosHtml(resp.mapa_html || '')
|
| 2779 |
+
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 2780 |
+
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 2781 |
+
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
| 2782 |
+
} else {
|
| 2783 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_LIVRE)
|
| 2784 |
+
}
|
| 2785 |
})
|
| 2786 |
}
|
| 2787 |
|
| 2788 |
async function onGerarMapaResiduos() {
|
| 2789 |
if (!sessionId) return
|
| 2790 |
+
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 2791 |
await withBusy(async () => {
|
| 2792 |
+
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 2793 |
setMapaResiduosHtml(resp.mapa_html || '')
|
| 2794 |
+
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 2795 |
+
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 2796 |
+
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
| 2797 |
+
} else {
|
| 2798 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_LIVRE)
|
| 2799 |
+
}
|
| 2800 |
setMapaResiduosGerado(true)
|
| 2801 |
})
|
| 2802 |
}
|
|
|
|
| 4802 |
</SectionBlock>
|
| 4803 |
|
| 4804 |
<SectionBlock step="16" title="Analisar Resíduos" subtitle="Métricas para identificação de observações influentes.">
|
| 4805 |
+
<div className="sec16-subsection">
|
| 4806 |
+
<h5 className="sec16-subtitle">Resumo Estatístico dos Resíduos Padronizados</h5>
|
| 4807 |
+
{resumoResiduoPadStats ? (
|
| 4808 |
+
<div className="residuos-stats-box">
|
| 4809 |
+
<div className="residuos-stats-grid">
|
| 4810 |
+
<div className="residuos-stats-item">
|
| 4811 |
+
<span>Mínimo</span>
|
| 4812 |
+
<strong>{formatNumberBr(resumoResiduoPadStats.minimo)}</strong>
|
| 4813 |
+
</div>
|
| 4814 |
+
<div className="residuos-stats-item">
|
| 4815 |
+
<span>1º Quartil</span>
|
| 4816 |
+
<strong>{formatNumberBr(resumoResiduoPadStats.q1)}</strong>
|
| 4817 |
+
</div>
|
| 4818 |
+
<div className="residuos-stats-item">
|
| 4819 |
+
<span>Mediana</span>
|
| 4820 |
+
<strong>{formatNumberBr(resumoResiduoPadStats.mediana)}</strong>
|
| 4821 |
+
</div>
|
| 4822 |
+
<div className="residuos-stats-item">
|
| 4823 |
+
<span>3º Quartil</span>
|
| 4824 |
+
<strong>{formatNumberBr(resumoResiduoPadStats.q3)}</strong>
|
| 4825 |
+
</div>
|
| 4826 |
+
<div className="residuos-stats-item">
|
| 4827 |
+
<span>Máximo</span>
|
| 4828 |
+
<strong>{formatNumberBr(resumoResiduoPadStats.maximo)}</strong>
|
| 4829 |
+
</div>
|
| 4830 |
</div>
|
|
|
|
| 4831 |
</div>
|
| 4832 |
) : (
|
| 4833 |
+
<div className="section1-empty-hint">Resumo indisponível para os resíduos padronizados.</div>
|
| 4834 |
+
)}
|
| 4835 |
+
</div>
|
| 4836 |
+
|
| 4837 |
+
<div className="sec16-subsection">
|
| 4838 |
+
<h5 className="sec16-subtitle">Mapa de Resíduos Padronizados</h5>
|
| 4839 |
+
<details className="dados-mapa-details" open>
|
| 4840 |
+
<summary>Mostrar/Ocultar mapa</summary>
|
| 4841 |
+
{!mapaResiduosGerado ? (
|
| 4842 |
+
<div className="empty-box">
|
| 4843 |
+
<div className="row">
|
| 4844 |
+
<button type="button" className="btn-gerar-mapa" onClick={onGerarMapaResiduos} disabled={loading}>
|
| 4845 |
+
Gerar Mapa de Resíduos Padronizados
|
| 4846 |
+
</button>
|
| 4847 |
</div>
|
| 4848 |
+
<div className="section1-empty-hint">O mapa de resíduos padronizados será carregado somente após solicitação explícita.</div>
|
| 4849 |
</div>
|
| 4850 |
+
) : (
|
| 4851 |
+
<>
|
| 4852 |
+
<div className="dados-mapa-controls">
|
| 4853 |
+
<div className="dados-mapa-control-field">
|
| 4854 |
+
<label>Visualização</label>
|
| 4855 |
+
<select value={mapaResiduosModo} onChange={(e) => onMapaResiduosModoChange(e.target.value)}>
|
| 4856 |
+
<option value={MAPA_MODO_PONTOS}>Pontos</option>
|
| 4857 |
+
<option value={MAPA_MODO_CALOR}>Mapa de calor</option>
|
| 4858 |
+
<option value={MAPA_MODO_SUPERFICIE}>Superfície contínua</option>
|
| 4859 |
+
</select>
|
| 4860 |
+
</div>
|
| 4861 |
+
<div className="dados-mapa-control-field">
|
| 4862 |
+
<label>Extremos da escala (abs.)</label>
|
| 4863 |
+
<select
|
| 4864 |
+
value={String(mapaResiduosExtremoAbs)}
|
| 4865 |
+
onChange={(e) => {
|
| 4866 |
+
void onMapaResiduosExtremoAbsChange(e.target.value)
|
| 4867 |
+
}}
|
| 4868 |
+
>
|
| 4869 |
+
<option value={MAPA_RESIDUOS_EXTREMO_LIVRE}>Livre (limites dos dados)</option>
|
| 4870 |
+
{MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS.map((valor) => (
|
| 4871 |
+
<option key={`mapa-res-ext-${valor}`} value={String(valor)}>
|
| 4872 |
+
±{formatNumberBr(valor, 1)}
|
| 4873 |
+
</option>
|
| 4874 |
+
))}
|
| 4875 |
+
</select>
|
| 4876 |
+
</div>
|
| 4877 |
+
</div>
|
| 4878 |
+
<div className="residuos-map-scale-hint">
|
| 4879 |
+
{mapaResiduosExtremoAbsAtivo === null
|
| 4880 |
+
? 'Escala livre: os extremos seguem os limites observados dos resíduos padronizados.'
|
| 4881 |
+
: `Escala fixa: valores ≤ -${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} e ≥ ${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} usam as cores máximas.`}
|
| 4882 |
+
</div>
|
| 4883 |
+
<div className="download-actions-bar">
|
| 4884 |
+
<button
|
| 4885 |
+
type="button"
|
| 4886 |
+
className="btn-download-subtle"
|
| 4887 |
+
onClick={onDownloadMapaSecao16}
|
| 4888 |
+
disabled={loading || downloadingAssets || !mapaResiduosHtml}
|
| 4889 |
+
>
|
| 4890 |
+
Fazer download
|
| 4891 |
+
</button>
|
| 4892 |
+
</div>
|
| 4893 |
+
<MapFrame html={mapaResiduosHtml} />
|
| 4894 |
+
</>
|
| 4895 |
+
)}
|
| 4896 |
+
</details>
|
| 4897 |
</div>
|
| 4898 |
+
|
| 4899 |
+
<div className="sec16-subsection">
|
| 4900 |
+
<h5 className="sec16-subtitle">Dados de Resíduos</h5>
|
| 4901 |
+
<div className="download-actions-bar">
|
| 4902 |
+
<button
|
| 4903 |
+
type="button"
|
| 4904 |
+
className="btn-download-subtle"
|
| 4905 |
+
onClick={() => onDownloadTableCsv(fit.tabela_metricas, 'secao16_tabela_metricas')}
|
| 4906 |
+
disabled={loading || downloadingAssets || !fit.tabela_metricas}
|
| 4907 |
+
>
|
| 4908 |
+
Fazer download
|
| 4909 |
+
</button>
|
| 4910 |
+
</div>
|
| 4911 |
+
<div className="outlier-highlight-note">
|
| 4912 |
+
Linhas amarelas indicam observações que atendem aos filtros definidos na seção 17.
|
| 4913 |
+
</div>
|
| 4914 |
+
<DataTable
|
| 4915 |
+
table={fit.tabela_metricas}
|
| 4916 |
+
maxHeight={320}
|
| 4917 |
+
highlightedRowIndices={outlierRowsHighlight}
|
| 4918 |
+
highlightIndexColumn={outlierHighlightIndexColumn}
|
| 4919 |
+
/>
|
| 4920 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4921 |
</SectionBlock>
|
| 4922 |
|
| 4923 |
<SectionBlock step="17" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
|
frontend/src/styles.css
CHANGED
|
@@ -927,6 +927,57 @@ textarea {
|
|
| 927 |
margin-top: 6px;
|
| 928 |
}
|
| 929 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
.dispersao-config-row {
|
| 931 |
align-items: flex-end;
|
| 932 |
gap: 14px;
|
|
|
|
| 927 |
margin-top: 6px;
|
| 928 |
}
|
| 929 |
|
| 930 |
+
.sec16-subsection {
|
| 931 |
+
border: 1px solid #dfe7ef;
|
| 932 |
+
border-radius: 10px;
|
| 933 |
+
background: #fbfdff;
|
| 934 |
+
padding: 10px;
|
| 935 |
+
margin-bottom: 10px;
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
.sec16-subtitle {
|
| 939 |
+
margin: 0 0 8px;
|
| 940 |
+
font-family: 'Sora', sans-serif;
|
| 941 |
+
font-size: 0.86rem;
|
| 942 |
+
color: #2e4459;
|
| 943 |
+
}
|
| 944 |
+
|
| 945 |
+
.residuos-stats-box {
|
| 946 |
+
margin-bottom: 0;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.residuos-stats-grid {
|
| 950 |
+
display: grid;
|
| 951 |
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
| 952 |
+
gap: 8px;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
.residuos-stats-item {
|
| 956 |
+
border: 1px solid #dce6ef;
|
| 957 |
+
background: #f7fafc;
|
| 958 |
+
border-radius: 8px;
|
| 959 |
+
padding: 6px 8px;
|
| 960 |
+
display: grid;
|
| 961 |
+
gap: 1px;
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
.residuos-stats-item span {
|
| 965 |
+
font-size: 0.72rem;
|
| 966 |
+
color: #667d92;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.residuos-stats-item strong {
|
| 970 |
+
font-family: 'Sora', sans-serif;
|
| 971 |
+
font-size: 0.84rem;
|
| 972 |
+
color: #2a3f53;
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
.residuos-map-scale-hint {
|
| 976 |
+
font-size: 0.74rem;
|
| 977 |
+
color: #5b7085;
|
| 978 |
+
margin: -4px 0 6px;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
.dispersao-config-row {
|
| 982 |
align-items: flex-end;
|
| 983 |
gap: 14px;
|