Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
0a8b8ba
1
Parent(s): 2e13456
otimizacoes de funcoes
Browse files- backend/app/core/elaboracao/geocodificacao.py +123 -106
- backend/app/services/elaboracao_service.py +6 -5
- backend/app/services/pesquisa_service.py +25 -24
- backend/app/services/serializers.py +10 -7
- frontend/src/App.jsx +3 -5
- frontend/src/components/ElaboracaoTab.jsx +960 -26
- frontend/src/components/PlotFigure.jsx +50 -12
- frontend/src/components/VisualizacaoTab.jsx +9 -12
- frontend/src/styles.css +102 -5
backend/app/core/elaboracao/geocodificacao.py
CHANGED
|
@@ -8,6 +8,7 @@ e, caso contrário, oferece geocodificação por interpolação em eixos de logr
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
import os
|
|
|
|
| 11 |
import pandas as pd
|
| 12 |
|
| 13 |
from .core import NOMES_LAT, NOMES_LON
|
|
@@ -214,20 +215,23 @@ def geocodificar(df, col_cdlog, col_num, auto_200=False):
|
|
| 214 |
df["__geo_cdlog"] = obter_serie_coluna(df, col_cdlog)
|
| 215 |
df["__geo_num"] = pd.to_numeric(obter_serie_coluna(df, col_num), errors="coerce").fillna(0).astype(int)
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
lats = []
|
| 218 |
lons = []
|
| 219 |
falhas = []
|
| 220 |
ajustados = []
|
| 221 |
|
| 222 |
-
for
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
numero = int(row["__geo_num"])
|
| 226 |
-
|
| 227 |
-
# --- Passo 1: buscar segmentos do CDLOG ---
|
| 228 |
-
segmentos = gdf_eixos[gdf_eixos["CDLOG"] == cdlog]
|
| 229 |
|
| 230 |
-
if segmentos.empty:
|
| 231 |
lats.append(None)
|
| 232 |
lons.append(None)
|
| 233 |
falhas.append({
|
|
@@ -240,106 +244,116 @@ def geocodificar(df, col_cdlog, col_num, auto_200=False):
|
|
| 240 |
})
|
| 241 |
continue
|
| 242 |
|
| 243 |
-
# --- Passo 2: determinar lado par/ímpar ---
|
| 244 |
lado = "Par" if numero % 2 == 0 else "Ímpar"
|
| 245 |
ini_col, fim_col = (
|
| 246 |
("NRPARINI", "NRPARFIN") if lado == "Par" else ("NRIMPINI", "NRIMPFIN")
|
| 247 |
)
|
| 248 |
|
| 249 |
-
segmentos
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
# --- Passo 3: encontrar segmento com intervalo válido ---
|
| 255 |
-
cond = (
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
min_index = diffs.idxmin()
|
| 266 |
-
linha_proxima = segmentos.loc[min_index]
|
| 267 |
-
|
| 268 |
-
ini = linha_proxima[ini_col]
|
| 269 |
-
fim = linha_proxima[fim_col]
|
| 270 |
-
|
| 271 |
-
if pd.notna(ini) and pd.notna(fim):
|
| 272 |
-
ini_i, fim_i = int(ini), int(fim)
|
| 273 |
-
todos = list(range(ini_i, fim_i + 1))
|
| 274 |
-
pares = sorted([n for n in todos if n % 2 == 0], key=lambda x: abs(x - numero))
|
| 275 |
-
impares = sorted([n for n in todos if n % 2 != 0], key=lambda x: abs(x - numero))
|
| 276 |
-
sugestoes = sorted(pares[:5] + impares[:5], key=lambda x: abs(x - numero))
|
| 277 |
-
sugestoes_str = ", ".join(map(str, sugestoes))
|
| 278 |
-
|
| 279 |
-
# Auto-correção ≤ 200
|
| 280 |
-
if sugestoes and auto_200:
|
| 281 |
-
melhor = sugestoes[0]
|
| 282 |
-
diferenca = abs(numero - melhor)
|
| 283 |
-
if diferenca <= 200:
|
| 284 |
-
numero_para_interpolar = melhor
|
| 285 |
-
ajustados.append({
|
| 286 |
-
"idx": idx,
|
| 287 |
-
"numero_original": numero,
|
| 288 |
-
"numero_usado": melhor,
|
| 289 |
-
})
|
| 290 |
-
|
| 291 |
-
if numero_para_interpolar is not None:
|
| 292 |
-
# Interpola com o número corrigido automaticamente
|
| 293 |
-
cond2 = (segmentos[ini_col] <= numero_para_interpolar) & \
|
| 294 |
-
(segmentos[fim_col] >= numero_para_interpolar)
|
| 295 |
-
seg2 = segmentos[cond2]
|
| 296 |
-
if seg2.empty:
|
| 297 |
-
# Usa segmento mais próximo mesmo assim
|
| 298 |
-
seg2 = segmentos.loc[[min_index]]
|
| 299 |
-
|
| 300 |
-
linha = seg2.iloc[0]
|
| 301 |
-
geom = linha.geometry
|
| 302 |
-
ini_v = linha[ini_col]
|
| 303 |
-
fim_v = linha[fim_col]
|
| 304 |
-
|
| 305 |
-
if fim_v == ini_v:
|
| 306 |
-
lats.append(None)
|
| 307 |
-
lons.append(None)
|
| 308 |
-
else:
|
| 309 |
-
frac = (numero_para_interpolar - ini_v) / (fim_v - ini_v)
|
| 310 |
-
frac = max(0.0, min(1.0, frac))
|
| 311 |
-
ponto = geom.interpolate(geom.length * frac)
|
| 312 |
-
lons.append(ponto.x)
|
| 313 |
-
lats.append(ponto.y)
|
| 314 |
-
else:
|
| 315 |
lats.append(None)
|
| 316 |
lons.append(None)
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
})
|
| 325 |
continue
|
| 326 |
|
| 327 |
-
# --- Passo
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
continue
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
df["lat"] = lats
|
| 345 |
df["lon"] = lons
|
|
@@ -408,18 +422,21 @@ def aplicar_correcoes_e_regeodificar(df_original, df_falhas, col_cdlog, col_num,
|
|
| 408 |
if "_idx" not in df.columns:
|
| 409 |
df["_idx"] = range(len(df))
|
| 410 |
|
| 411 |
-
# Aplica correções
|
| 412 |
-
manuais = []
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
df_resultado, df_falhas_novas, ajustados = geocodificar(df, col_cdlog, col_num, auto_200)
|
| 425 |
return df_resultado, df_falhas_novas, ajustados, manuais
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
import os
|
| 11 |
+
import numpy as np
|
| 12 |
import pandas as pd
|
| 13 |
|
| 14 |
from .core import NOMES_LAT, NOMES_LON
|
|
|
|
| 215 |
df["__geo_cdlog"] = obter_serie_coluna(df, col_cdlog)
|
| 216 |
df["__geo_num"] = pd.to_numeric(obter_serie_coluna(df, col_num), errors="coerce").fillna(0).astype(int)
|
| 217 |
|
| 218 |
+
# Pré-processa segmentos dos eixos apenas uma vez por execução.
|
| 219 |
+
eixos = gdf_eixos.copy()
|
| 220 |
+
for col in ("NRPARINI", "NRPARFIN", "NRIMPINI", "NRIMPFIN"):
|
| 221 |
+
if col in eixos.columns:
|
| 222 |
+
eixos[col] = pd.to_numeric(eixos[col], errors="coerce")
|
| 223 |
+
segmentos_por_cdlog = {cdlog: grupo.reset_index(drop=True) for cdlog, grupo in eixos.groupby("CDLOG", sort=False)}
|
| 224 |
+
|
| 225 |
lats = []
|
| 226 |
lons = []
|
| 227 |
falhas = []
|
| 228 |
ajustados = []
|
| 229 |
|
| 230 |
+
for idx, cdlog, numero_raw in df[["_idx", "__geo_cdlog", "__geo_num"]].itertuples(index=False, name=None):
|
| 231 |
+
numero = int(numero_raw)
|
| 232 |
+
segmentos = segmentos_por_cdlog.get(cdlog)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
if segmentos is None or segmentos.empty:
|
| 235 |
lats.append(None)
|
| 236 |
lons.append(None)
|
| 237 |
falhas.append({
|
|
|
|
| 244 |
})
|
| 245 |
continue
|
| 246 |
|
|
|
|
| 247 |
lado = "Par" if numero % 2 == 0 else "Ímpar"
|
| 248 |
ini_col, fim_col = (
|
| 249 |
("NRPARINI", "NRPARFIN") if lado == "Par" else ("NRIMPINI", "NRIMPFIN")
|
| 250 |
)
|
| 251 |
|
| 252 |
+
if ini_col not in segmentos.columns or fim_col not in segmentos.columns:
|
| 253 |
+
lats.append(None)
|
| 254 |
+
lons.append(None)
|
| 255 |
+
falhas.append({
|
| 256 |
+
"_idx": idx,
|
| 257 |
+
"cdlog": cdlog,
|
| 258 |
+
"numero_atual": numero,
|
| 259 |
+
"motivo": "Numeração fora do intervalo",
|
| 260 |
+
"sugestoes": "",
|
| 261 |
+
"numero_corrigido": "",
|
| 262 |
+
})
|
| 263 |
+
continue
|
| 264 |
+
|
| 265 |
+
ini_vals = segmentos[ini_col].to_numpy(dtype=float, copy=False)
|
| 266 |
+
fim_vals = segmentos[fim_col].to_numpy(dtype=float, copy=False)
|
| 267 |
+
valid_mask = np.isfinite(ini_vals) & np.isfinite(fim_vals)
|
| 268 |
|
| 269 |
# --- Passo 3: encontrar segmento com intervalo válido ---
|
| 270 |
+
cond = valid_mask & (ini_vals <= numero) & (fim_vals >= numero)
|
| 271 |
+
|
| 272 |
+
if cond.any():
|
| 273 |
+
pos = int(np.flatnonzero(cond)[0])
|
| 274 |
+
linha = segmentos.iloc[pos]
|
| 275 |
+
geom = linha.geometry
|
| 276 |
+
ini = ini_vals[pos]
|
| 277 |
+
fim = fim_vals[pos]
|
| 278 |
+
|
| 279 |
+
if fim == ini:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
lats.append(None)
|
| 281 |
lons.append(None)
|
| 282 |
+
continue
|
| 283 |
+
|
| 284 |
+
frac = (numero - ini) / (fim - ini)
|
| 285 |
+
frac = max(0.0, min(1.0, frac))
|
| 286 |
+
ponto = geom.interpolate(geom.length * frac)
|
| 287 |
+
lons.append(ponto.x)
|
| 288 |
+
lats.append(ponto.y)
|
|
|
|
| 289 |
continue
|
| 290 |
|
| 291 |
+
# --- Passo 5: intervalo não encontrado — gera sugestões ---
|
| 292 |
+
sugestoes_str = ""
|
| 293 |
+
numero_para_interpolar = None
|
| 294 |
+
min_pos = None
|
| 295 |
+
valid_positions = np.flatnonzero(valid_mask)
|
| 296 |
+
|
| 297 |
+
if valid_positions.size > 0:
|
| 298 |
+
ini_valid = ini_vals[valid_positions]
|
| 299 |
+
diffs = np.abs(ini_valid - numero)
|
| 300 |
+
min_pos = int(valid_positions[int(np.argmin(diffs))])
|
| 301 |
+
|
| 302 |
+
ini = ini_vals[min_pos]
|
| 303 |
+
fim = fim_vals[min_pos]
|
| 304 |
+
|
| 305 |
+
if pd.notna(ini) and pd.notna(fim):
|
| 306 |
+
ini_i, fim_i = int(ini), int(fim)
|
| 307 |
+
todos = list(range(ini_i, fim_i + 1))
|
| 308 |
+
pares = sorted([n for n in todos if n % 2 == 0], key=lambda x: abs(x - numero))
|
| 309 |
+
impares = sorted([n for n in todos if n % 2 != 0], key=lambda x: abs(x - numero))
|
| 310 |
+
sugestoes = sorted(pares[:5] + impares[:5], key=lambda x: abs(x - numero))
|
| 311 |
+
sugestoes_str = ", ".join(map(str, sugestoes))
|
| 312 |
+
|
| 313 |
+
if sugestoes and auto_200:
|
| 314 |
+
melhor = sugestoes[0]
|
| 315 |
+
diferenca = abs(numero - melhor)
|
| 316 |
+
if diferenca <= 200:
|
| 317 |
+
numero_para_interpolar = melhor
|
| 318 |
+
ajustados.append({
|
| 319 |
+
"idx": idx,
|
| 320 |
+
"numero_original": numero,
|
| 321 |
+
"numero_usado": melhor,
|
| 322 |
+
})
|
| 323 |
+
|
| 324 |
+
if numero_para_interpolar is not None and min_pos is not None:
|
| 325 |
+
cond2 = valid_mask & (ini_vals <= numero_para_interpolar) & (fim_vals >= numero_para_interpolar)
|
| 326 |
+
if cond2.any():
|
| 327 |
+
pos2 = int(np.flatnonzero(cond2)[0])
|
| 328 |
+
else:
|
| 329 |
+
pos2 = min_pos
|
| 330 |
|
| 331 |
+
linha = segmentos.iloc[pos2]
|
| 332 |
+
geom = linha.geometry
|
| 333 |
+
ini_v = ini_vals[pos2]
|
| 334 |
+
fim_v = fim_vals[pos2]
|
| 335 |
+
|
| 336 |
+
if fim_v == ini_v:
|
| 337 |
+
lats.append(None)
|
| 338 |
+
lons.append(None)
|
| 339 |
+
else:
|
| 340 |
+
frac = (numero_para_interpolar - ini_v) / (fim_v - ini_v)
|
| 341 |
+
frac = max(0.0, min(1.0, frac))
|
| 342 |
+
ponto = geom.interpolate(geom.length * frac)
|
| 343 |
+
lons.append(ponto.x)
|
| 344 |
+
lats.append(ponto.y)
|
| 345 |
continue
|
| 346 |
|
| 347 |
+
lats.append(None)
|
| 348 |
+
lons.append(None)
|
| 349 |
+
falhas.append({
|
| 350 |
+
"_idx": idx,
|
| 351 |
+
"cdlog": cdlog,
|
| 352 |
+
"numero_atual": numero,
|
| 353 |
+
"motivo": "Numeração fora do intervalo",
|
| 354 |
+
"sugestoes": sugestoes_str,
|
| 355 |
+
"numero_corrigido": "",
|
| 356 |
+
})
|
| 357 |
|
| 358 |
df["lat"] = lats
|
| 359 |
df["lon"] = lons
|
|
|
|
| 422 |
if "_idx" not in df.columns:
|
| 423 |
df["_idx"] = range(len(df))
|
| 424 |
|
| 425 |
+
# Aplica correções de forma vetorizada por _idx.
|
| 426 |
+
manuais: list[int] = []
|
| 427 |
+
if isinstance(df_falhas, pd.DataFrame) and not df_falhas.empty and "_idx" in df_falhas.columns:
|
| 428 |
+
correcoes = df_falhas.loc[:, ["_idx", "numero_corrigido"]].copy()
|
| 429 |
+
correcoes["__novo_num"] = pd.to_numeric(correcoes["numero_corrigido"], errors="coerce")
|
| 430 |
+
correcoes = correcoes.dropna(subset=["_idx", "__novo_num"])
|
| 431 |
+
if not correcoes.empty:
|
| 432 |
+
correcoes["__novo_num"] = correcoes["__novo_num"].astype(int)
|
| 433 |
+
manuais = correcoes["_idx"].tolist()
|
| 434 |
+
|
| 435 |
+
mapa = correcoes.drop_duplicates(subset=["_idx"], keep="last").set_index("_idx")["__novo_num"]
|
| 436 |
+
novos = df["_idx"].map(mapa)
|
| 437 |
+
mask = novos.notna()
|
| 438 |
+
if mask.any():
|
| 439 |
+
df.loc[mask, col_num] = novos[mask].astype(int).to_numpy()
|
| 440 |
|
| 441 |
df_resultado, df_falhas_novas, ajustados = geocodificar(df, col_cdlog, col_num, auto_200)
|
| 442 |
return df_resultado, df_falhas_novas, ajustados, manuais
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -1408,11 +1408,12 @@ def aplicar_correcoes_geocodificacao(
|
|
| 1408 |
|
| 1409 |
if "numero_corrigido" not in df_falhas.columns:
|
| 1410 |
df_falhas["numero_corrigido"] = ""
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
|
|
|
| 1416 |
|
| 1417 |
try:
|
| 1418 |
df_resultado, df_falhas_novas, ajustados, manuais = geocodificacao.aplicar_correcoes_e_regeodificar(
|
|
|
|
| 1408 |
|
| 1409 |
if "numero_corrigido" not in df_falhas.columns:
|
| 1410 |
df_falhas["numero_corrigido"] = ""
|
| 1411 |
+
if mapa_correcao:
|
| 1412 |
+
linhas_ref = pd.to_numeric(df_falhas["_idx"], errors="coerce")
|
| 1413 |
+
valores_corrigidos = linhas_ref.map(mapa_correcao).fillna("")
|
| 1414 |
+
df_falhas["numero_corrigido"] = valores_corrigidos.astype(str)
|
| 1415 |
+
else:
|
| 1416 |
+
df_falhas["numero_corrigido"] = ""
|
| 1417 |
|
| 1418 |
try:
|
| 1419 |
df_resultado, df_falhas_novas, ajustados, manuais = geocodificacao.aplicar_correcoes_e_regeodificar(
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -11,6 +11,7 @@ from threading import Lock
|
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
import folium
|
|
|
|
| 14 |
import pandas as pd
|
| 15 |
from fastapi import HTTPException
|
| 16 |
from joblib import load
|
|
@@ -741,10 +742,9 @@ def _coletar_pontos_modelo(df_modelo: pd.DataFrame, limite_pontos: int) -> list[
|
|
| 741 |
passo = max(1, math.ceil(len(base) / limite_pontos))
|
| 742 |
base = base.iloc[::passo].head(limite_pontos)
|
| 743 |
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
return pontos
|
| 748 |
|
| 749 |
|
| 750 |
def _identificar_coluna_por_alias(colunas: Any, aliases: list[str]) -> str | None:
|
|
@@ -873,19 +873,17 @@ def _extrair_faixa_por_alias(estat_df: pd.DataFrame | None, aliases: list[str])
|
|
| 873 |
if min_col is None or max_col is None:
|
| 874 |
return None
|
| 875 |
|
| 876 |
-
|
| 877 |
-
|
|
|
|
| 878 |
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
continue
|
| 887 |
-
mins.append(min_val)
|
| 888 |
-
maxs.append(max_val)
|
| 889 |
|
| 890 |
if not mins and not maxs:
|
| 891 |
return None
|
|
@@ -912,14 +910,15 @@ def _resumo_variaveis(estat_df: pd.DataFrame | None, limite: int = 12) -> list[d
|
|
| 912 |
return []
|
| 913 |
|
| 914 |
linhas: list[dict[str, Any]] = []
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
|
|
|
| 918 |
if _is_empty(min_val) and _is_empty(max_val):
|
| 919 |
continue
|
| 920 |
linhas.append(
|
| 921 |
{
|
| 922 |
-
"variavel":
|
| 923 |
"min": sanitize_value(min_val),
|
| 924 |
"max": sanitize_value(max_val),
|
| 925 |
}
|
|
@@ -1605,13 +1604,15 @@ def _indexar_faixas_variaveis(estat_df: pd.DataFrame | None, variaveis_modelo: l
|
|
| 1605 |
variaveis_norm = {_normalize(item) for item in (variaveis_modelo or []) if _str_or_none(item)}
|
| 1606 |
indice: dict[str, dict[str, Any]] = {}
|
| 1607 |
|
| 1608 |
-
|
| 1609 |
-
|
|
|
|
|
|
|
| 1610 |
if variaveis_norm and _normalize(nome) not in variaveis_norm:
|
| 1611 |
continue
|
| 1612 |
|
| 1613 |
-
min_val = sanitize_value(
|
| 1614 |
-
max_val = sanitize_value(
|
| 1615 |
if _is_empty(min_val) and _is_empty(max_val):
|
| 1616 |
continue
|
| 1617 |
indice[nome] = {
|
|
|
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
import folium
|
| 14 |
+
import numpy as np
|
| 15 |
import pandas as pd
|
| 16 |
from fastapi import HTTPException
|
| 17 |
from joblib import load
|
|
|
|
| 742 |
passo = max(1, math.ceil(len(base) / limite_pontos))
|
| 743 |
base = base.iloc[::passo].head(limite_pontos)
|
| 744 |
|
| 745 |
+
lat_vals = base[col_lat].to_numpy(dtype=float, copy=False)
|
| 746 |
+
lon_vals = base[col_lon].to_numpy(dtype=float, copy=False)
|
| 747 |
+
return [{"lat": float(lat), "lon": float(lon)} for lat, lon in zip(lat_vals, lon_vals)]
|
|
|
|
| 748 |
|
| 749 |
|
| 750 |
def _identificar_coluna_por_alias(colunas: Any, aliases: list[str]) -> str | None:
|
|
|
|
| 873 |
if min_col is None or max_col is None:
|
| 874 |
return None
|
| 875 |
|
| 876 |
+
nomes = estat_indexado.index.astype(str).tolist()
|
| 877 |
+
if not nomes:
|
| 878 |
+
return None
|
| 879 |
|
| 880 |
+
mask_alias = np.fromiter((_has_alias(nome, aliases) for nome in nomes), dtype=bool, count=len(nomes))
|
| 881 |
+
if not mask_alias.any():
|
| 882 |
+
return None
|
| 883 |
+
|
| 884 |
+
trabalho = estat_indexado.loc[mask_alias, [min_col, max_col]]
|
| 885 |
+
mins = [valor for valor in trabalho[min_col].tolist() if not _is_empty(valor)]
|
| 886 |
+
maxs = [valor for valor in trabalho[max_col].tolist() if not _is_empty(valor)]
|
|
|
|
|
|
|
|
|
|
| 887 |
|
| 888 |
if not mins and not maxs:
|
| 889 |
return None
|
|
|
|
| 910 |
return []
|
| 911 |
|
| 912 |
linhas: list[dict[str, Any]] = []
|
| 913 |
+
nomes = trabalho.index.astype(str).tolist()
|
| 914 |
+
mins = trabalho[min_col].tolist()
|
| 915 |
+
maxs = trabalho[max_col].tolist()
|
| 916 |
+
for var, min_val, max_val in zip(nomes, mins, maxs):
|
| 917 |
if _is_empty(min_val) and _is_empty(max_val):
|
| 918 |
continue
|
| 919 |
linhas.append(
|
| 920 |
{
|
| 921 |
+
"variavel": var,
|
| 922 |
"min": sanitize_value(min_val),
|
| 923 |
"max": sanitize_value(max_val),
|
| 924 |
}
|
|
|
|
| 1604 |
variaveis_norm = {_normalize(item) for item in (variaveis_modelo or []) if _str_or_none(item)}
|
| 1605 |
indice: dict[str, dict[str, Any]] = {}
|
| 1606 |
|
| 1607 |
+
nomes = trabalho.index.astype(str).tolist()
|
| 1608 |
+
mins = trabalho[min_col].tolist()
|
| 1609 |
+
maxs = trabalho[max_col].tolist()
|
| 1610 |
+
for nome, min_bruto, max_bruto in zip(nomes, mins, maxs):
|
| 1611 |
if variaveis_norm and _normalize(nome) not in variaveis_norm:
|
| 1612 |
continue
|
| 1613 |
|
| 1614 |
+
min_val = sanitize_value(min_bruto)
|
| 1615 |
+
max_val = sanitize_value(max_bruto)
|
| 1616 |
if _is_empty(min_val) and _is_empty(max_val):
|
| 1617 |
continue
|
| 1618 |
indice[nome] = {
|
backend/app/services/serializers.py
CHANGED
|
@@ -67,7 +67,8 @@ def dataframe_to_payload(df: pd.DataFrame | None, decimals: int | None = None, m
|
|
| 67 |
if df is None:
|
| 68 |
return None
|
| 69 |
|
| 70 |
-
|
|
|
|
| 71 |
if decimals is not None:
|
| 72 |
numeric_cols = [
|
| 73 |
col
|
|
@@ -82,16 +83,18 @@ def dataframe_to_payload(df: pd.DataFrame | None, decimals: int | None = None, m
|
|
| 82 |
if truncated:
|
| 83 |
df_work = df_work.head(max_rows)
|
| 84 |
|
|
|
|
| 85 |
rows: list[dict[str, Any]] = []
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
rows.append(payload_row)
|
| 91 |
|
| 92 |
-
|
| 93 |
return {
|
| 94 |
-
"columns":
|
| 95 |
"rows": rows,
|
| 96 |
"total_rows": total_rows,
|
| 97 |
"returned_rows": len(rows),
|
|
|
|
| 67 |
if df is None:
|
| 68 |
return None
|
| 69 |
|
| 70 |
+
# Evita cópia quando não há transformação necessária.
|
| 71 |
+
df_work = df if decimals is None else df.copy()
|
| 72 |
if decimals is not None:
|
| 73 |
numeric_cols = [
|
| 74 |
col
|
|
|
|
| 83 |
if truncated:
|
| 84 |
df_work = df_work.head(max_rows)
|
| 85 |
|
| 86 |
+
columns = [str(c) for c in df_work.columns]
|
| 87 |
rows: list[dict[str, Any]] = []
|
| 88 |
+
# itertuples é significativamente mais leve que iterrows para payloads grandes.
|
| 89 |
+
for tuple_row in df_work.itertuples(index=True, name=None):
|
| 90 |
+
payload_row = {"_index": sanitize_value(tuple_row[0])}
|
| 91 |
+
for col, value in zip(columns, tuple_row[1:]):
|
| 92 |
+
payload_row[col] = sanitize_value(value)
|
| 93 |
rows.append(payload_row)
|
| 94 |
|
| 95 |
+
columns_payload = ["_index"] + columns
|
| 96 |
return {
|
| 97 |
+
"columns": columns_payload,
|
| 98 |
"rows": rows,
|
| 99 |
"total_rows": total_rows,
|
| 100 |
"returned_rows": len(rows),
|
frontend/src/App.jsx
CHANGED
|
@@ -64,11 +64,9 @@ export default function App() {
|
|
| 64 |
<InicioTab />
|
| 65 |
</div>
|
| 66 |
|
| 67 |
-
{activeTab ==
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
</div>
|
| 71 |
-
) : null}
|
| 72 |
|
| 73 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
| 74 |
<ElaboracaoTab sessionId={sessionId} />
|
|
|
|
| 64 |
<InicioTab />
|
| 65 |
</div>
|
| 66 |
|
| 67 |
+
<div className="tab-pane" hidden={activeTab !== 'Pesquisa'}>
|
| 68 |
+
<PesquisaTab />
|
| 69 |
+
</div>
|
|
|
|
|
|
|
| 70 |
|
| 71 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
| 72 |
<ElaboracaoTab sessionId={sessionId} />
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
|
|
|
| 3 |
import DataTable from './DataTable'
|
| 4 |
import LoadingOverlay from './LoadingOverlay'
|
| 5 |
import MapFrame from './MapFrame'
|
|
@@ -45,6 +46,355 @@ function joinSelection(values) {
|
|
| 45 |
return values.join(', ')
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
function formatConselhoRegistro(elaborador) {
|
| 49 |
if (!elaborador) return ''
|
| 50 |
const conselho = String(elaborador.conselho || '').trim()
|
|
@@ -144,6 +494,7 @@ function obterLabelGrau(listaGraus, valor) {
|
|
| 144 |
|
| 145 |
export default function ElaboracaoTab({ sessionId }) {
|
| 146 |
const [loading, setLoading] = useState(false)
|
|
|
|
| 147 |
const [error, setError] = useState('')
|
| 148 |
const [status, setStatus] = useState('')
|
| 149 |
|
|
@@ -175,6 +526,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 175 |
|
| 176 |
const [colunasNumericas, setColunasNumericas] = useState([])
|
| 177 |
const [colunaY, setColunaY] = useState('')
|
|
|
|
| 178 |
const [colunasX, setColunasX] = useState([])
|
| 179 |
const [dicotomicas, setDicotomicas] = useState([])
|
| 180 |
const [codigoAlocado, setCodigoAlocado] = useState([])
|
|
@@ -182,6 +534,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 182 |
const [colunasDataMercado, setColunasDataMercado] = useState([])
|
| 183 |
const [colunaDataMercadoSugerida, setColunaDataMercadoSugerida] = useState('')
|
| 184 |
const [colunaDataMercado, setColunaDataMercado] = useState('')
|
|
|
|
| 185 |
const [periodoDadosMercado, setPeriodoDadosMercado] = useState(null)
|
| 186 |
const [periodoDadosMercadoPreview, setPeriodoDadosMercadoPreview] = useState(null)
|
| 187 |
const [dataMercadoError, setDataMercadoError] = useState('')
|
|
@@ -214,6 +567,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 214 |
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 215 |
const valoresAvaliacaoRef = useRef({})
|
| 216 |
const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
|
|
|
|
| 217 |
const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
|
| 218 |
const [baseChoices, setBaseChoices] = useState([])
|
| 219 |
const [baseValue, setBaseValue] = useState('')
|
|
@@ -222,6 +576,11 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 222 |
const [avaliadores, setAvaliadores] = useState([])
|
| 223 |
const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
|
| 224 |
const [tipoFonteDados, setTipoFonteDados] = useState('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
const marcarTodasXRef = useRef(null)
|
| 226 |
const classificarXReqRef = useRef(0)
|
| 227 |
const deleteConfirmTimersRef = useRef({})
|
|
@@ -229,7 +588,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 229 |
|
| 230 |
const mapaChoices = useMemo(() => ['Visualização Padrão', ...colunasNumericas], [colunasNumericas])
|
| 231 |
const colunasXDisponiveis = useMemo(
|
| 232 |
-
() => colunasNumericas.filter((coluna) => coluna !== colunaY),
|
| 233 |
[colunasNumericas, colunaY],
|
| 234 |
)
|
| 235 |
const todasXMarcadas = useMemo(
|
|
@@ -269,6 +628,65 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 269 |
() => formatPeriodoDadosMercado(periodoDadosMercadoPreview),
|
| 270 |
[periodoDadosMercadoPreview],
|
| 271 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
const transformacaoAplicadaYBadge = useMemo(
|
| 273 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 274 |
[transformacoesAplicadas],
|
|
@@ -297,6 +715,32 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 297 |
}
|
| 298 |
return ''
|
| 299 |
}, [origemTransformacoes])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
const showCoordsPanel = Boolean(
|
| 301 |
coordsInfo && (
|
| 302 |
!coordsInfo.tem_coords ||
|
|
@@ -408,6 +852,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 408 |
|
| 409 |
function applyBaseResponse(resp, options = {}) {
|
| 410 |
const resetXSelection = Boolean(options.resetXSelection)
|
|
|
|
| 411 |
setDataMercadoError('')
|
| 412 |
if (resp.status) setStatus(resp.status)
|
| 413 |
if (resp.dados) setDados(resp.dados)
|
|
@@ -420,44 +865,74 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 420 |
setColunaDataMercadoSugerida(String(resp.coluna_data_mercado_sugerida || ''))
|
| 421 |
}
|
| 422 |
if (Object.prototype.hasOwnProperty.call(resp, 'coluna_data_mercado')) {
|
| 423 |
-
|
|
|
|
|
|
|
| 424 |
}
|
| 425 |
if (Object.prototype.hasOwnProperty.call(resp, 'periodo_dados_mercado')) {
|
| 426 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 427 |
setPeriodoDadosMercado(periodo)
|
| 428 |
setPeriodoDadosMercadoPreview(periodo)
|
| 429 |
}
|
| 430 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
if (resp.contexto && !resetXSelection) {
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
setOutliersAnteriores(resp.contexto.outliers_anteriores || [])
|
| 439 |
setIteracao(resp.contexto.iteracao || 1)
|
| 440 |
} else if (resetXSelection) {
|
|
|
|
|
|
|
| 441 |
setColunasX([])
|
| 442 |
setDicotomicas([])
|
| 443 |
setCodigoAlocado([])
|
| 444 |
setPercentuais([])
|
|
|
|
| 445 |
setTransformacaoY('(x)')
|
| 446 |
setTransformacoesX({})
|
| 447 |
setTransformacoesAplicadas(null)
|
| 448 |
setOrigemTransformacoes(null)
|
|
|
|
|
|
|
| 449 |
setOutliersAnteriores([])
|
| 450 |
setIteracao(1)
|
| 451 |
setColunaDataMercadoSugerida('')
|
| 452 |
setColunaDataMercado('')
|
|
|
|
| 453 |
setPeriodoDadosMercado(null)
|
| 454 |
setPeriodoDadosMercadoPreview(null)
|
| 455 |
setDataMercadoError('')
|
| 456 |
setSelection(null)
|
| 457 |
setFit(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
setCamposAvaliacao([])
|
| 459 |
valoresAvaliacaoRef.current = {}
|
| 460 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 461 |
setResultadoAvaliacaoHtml('')
|
| 462 |
setOutliersHtml('')
|
| 463 |
}
|
|
@@ -488,6 +963,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 488 |
setSection10ManualOpen(false)
|
| 489 |
setTransformacoesAplicadas(null)
|
| 490 |
setOrigemTransformacoes(null)
|
|
|
|
| 491 |
if (resp.transformacao_y) {
|
| 492 |
setTransformacaoY(resp.transformacao_y)
|
| 493 |
}
|
|
@@ -497,11 +973,15 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 497 |
map[field.coluna] = field.valor || '(x)'
|
| 498 |
})
|
| 499 |
setTransformacoesX(map)
|
|
|
|
| 500 |
|
|
|
|
|
|
|
| 501 |
if (resp.busca) {
|
| 502 |
-
setGrauCoef(
|
| 503 |
-
setGrauF(
|
| 504 |
}
|
|
|
|
| 505 |
|
| 506 |
if (resp.resumo_outliers) {
|
| 507 |
setResumoOutliers(resp.resumo_outliers)
|
|
@@ -518,6 +998,8 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 518 |
function applyFitResponse(resp, origemMeta = null) {
|
| 519 |
setFit(resp)
|
| 520 |
setSection11Open(false)
|
|
|
|
|
|
|
| 521 |
if (resp.transformacao_y) {
|
| 522 |
setTransformacaoY(resp.transformacao_y)
|
| 523 |
}
|
|
@@ -533,12 +1015,15 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 533 |
transformacoes_x: resp.transformacoes_x || transformacoesX,
|
| 534 |
})
|
| 535 |
}
|
|
|
|
| 536 |
if (origemMeta && origemMeta.origem) {
|
| 537 |
setOrigemTransformacoes(origemMeta)
|
| 538 |
} else if (resp.origem_transformacoes?.origem) {
|
| 539 |
setOrigemTransformacoes(resp.origem_transformacoes)
|
| 540 |
}
|
| 541 |
setResumoOutliers(resp.resumo_outliers || resumoOutliers)
|
|
|
|
|
|
|
| 542 |
setCamposAvaliacao(resp.avaliacao_campos || [])
|
| 543 |
const init = {}
|
| 544 |
;(resp.avaliacao_campos || []).forEach((campo) => {
|
|
@@ -546,6 +1031,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 546 |
})
|
| 547 |
valoresAvaliacaoRef.current = init
|
| 548 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 549 |
setResultadoAvaliacaoHtml('')
|
| 550 |
setBaseChoices([])
|
| 551 |
setBaseValue('')
|
|
@@ -580,8 +1066,11 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 580 |
applyBaseResponse(resp, { resetXSelection })
|
| 581 |
} else if (resp.status) {
|
| 582 |
setTipoFonteDados('tabular')
|
|
|
|
|
|
|
| 583 |
setSelection(null)
|
| 584 |
setFit(null)
|
|
|
|
| 585 |
setColunasX([])
|
| 586 |
setDicotomicas([])
|
| 587 |
setCodigoAlocado([])
|
|
@@ -590,15 +1079,24 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 590 |
setTransformacoesX({})
|
| 591 |
setTransformacoesAplicadas(null)
|
| 592 |
setOrigemTransformacoes(null)
|
|
|
|
|
|
|
| 593 |
setColunasDataMercado([])
|
| 594 |
setColunaDataMercadoSugerida('')
|
| 595 |
setColunaDataMercado('')
|
|
|
|
| 596 |
setPeriodoDadosMercado(null)
|
| 597 |
setPeriodoDadosMercadoPreview(null)
|
| 598 |
setDataMercadoError('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
setCamposAvaliacao([])
|
| 600 |
valoresAvaliacaoRef.current = {}
|
| 601 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 602 |
setResultadoAvaliacaoHtml('')
|
| 603 |
setStatus(resp.status)
|
| 604 |
}
|
|
@@ -661,6 +1159,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 661 |
const coluna = String(value || '')
|
| 662 |
setColunaDataMercado(coluna)
|
| 663 |
setDataMercadoError('')
|
|
|
|
| 664 |
|
| 665 |
if (!sessionId || !coluna) {
|
| 666 |
setPeriodoDadosMercadoPreview(null)
|
|
@@ -672,7 +1171,6 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 672 |
const resp = await api.previewMarketDateColumn(sessionId, coluna)
|
| 673 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 674 |
setPeriodoDadosMercadoPreview(periodo)
|
| 675 |
-
if (resp.status) setStatus(resp.status)
|
| 676 |
} catch (err) {
|
| 677 |
setPeriodoDadosMercadoPreview(null)
|
| 678 |
setDataMercadoError(err.message || 'Falha ao identificar período da coluna selecionada.')
|
|
@@ -686,7 +1184,9 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 686 |
await withBusy(async () => {
|
| 687 |
const resp = await api.applyMarketDateColumn(sessionId, colunaDataMercado)
|
| 688 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 689 |
-
|
|
|
|
|
|
|
| 690 |
setPeriodoDadosMercado(periodo)
|
| 691 |
setPeriodoDadosMercadoPreview(periodo)
|
| 692 |
setDataMercadoError('')
|
|
@@ -701,6 +1201,46 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 701 |
})
|
| 702 |
}
|
| 703 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
async function onMapCoords() {
|
| 705 |
if (!manualLat || !manualLon || !sessionId) return
|
| 706 |
setLoading(true)
|
|
@@ -802,7 +1342,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 802 |
async function onApplySelection() {
|
| 803 |
if (!sessionId || !colunaY || colunasX.length === 0) return
|
| 804 |
await withBusy(async () => {
|
| 805 |
-
const
|
| 806 |
session_id: sessionId,
|
| 807 |
coluna_y: colunaY,
|
| 808 |
colunas_x: colunasX,
|
|
@@ -812,10 +1352,17 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 812 |
outliers_anteriores: outliersAnteriores,
|
| 813 |
grau_min_coef: grauCoef,
|
| 814 |
grau_min_f: grauF,
|
| 815 |
-
}
|
|
|
|
| 816 |
applySelectionResponse(resp)
|
|
|
|
| 817 |
setFit(null)
|
|
|
|
|
|
|
| 818 |
setCamposAvaliacao([])
|
|
|
|
|
|
|
|
|
|
| 819 |
setResultadoAvaliacaoHtml('')
|
| 820 |
})
|
| 821 |
}
|
|
@@ -824,9 +1371,12 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 824 |
if (!sessionId) return
|
| 825 |
await withBusy(async () => {
|
| 826 |
const busca = await api.searchTransformations(sessionId, grauCoef, grauF)
|
|
|
|
|
|
|
| 827 |
setSelection((prev) => ({ ...prev, busca }))
|
| 828 |
-
setGrauCoef(
|
| 829 |
-
setGrauF(
|
|
|
|
| 830 |
})
|
| 831 |
}
|
| 832 |
|
|
@@ -888,6 +1438,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 888 |
const filtrosValidos = filtros.filter((f) => f.variavel && f.operador)
|
| 889 |
const resp = await api.applyOutlierFilters(sessionId, filtrosValidos)
|
| 890 |
setOutliersTexto(resp.texto || '')
|
|
|
|
| 891 |
})
|
| 892 |
}
|
| 893 |
|
|
@@ -920,6 +1471,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 920 |
setIteracao(resp.iteracao || iteracao)
|
| 921 |
setOutliersTexto('')
|
| 922 |
setReincluirTexto('')
|
|
|
|
| 923 |
setFit(null)
|
| 924 |
setTransformacoesAplicadas(null)
|
| 925 |
setOrigemTransformacoes(null)
|
|
@@ -945,10 +1497,16 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 945 |
setTransformacoesAplicadas(null)
|
| 946 |
setOrigemTransformacoes(null)
|
| 947 |
setCamposAvaliacao([])
|
|
|
|
|
|
|
|
|
|
| 948 |
valoresAvaliacaoRef.current = {}
|
| 949 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 950 |
setResultadoAvaliacaoHtml('')
|
| 951 |
setOutliersHtml(resp.outliers_html || '')
|
|
|
|
|
|
|
| 952 |
})
|
| 953 |
}
|
| 954 |
|
|
@@ -959,6 +1517,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 959 |
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 960 |
setBaseChoices(resp.base_choices || [])
|
| 961 |
setBaseValue(resp.base_value || '')
|
|
|
|
| 962 |
})
|
| 963 |
}
|
| 964 |
|
|
@@ -975,6 +1534,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 975 |
})
|
| 976 |
valoresAvaliacaoRef.current = limpo
|
| 977 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 978 |
})
|
| 979 |
}
|
| 980 |
|
|
@@ -1078,6 +1638,132 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1078 |
})
|
| 1079 |
}
|
| 1080 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
function toggleSelection(setter, value) {
|
| 1082 |
setter((prev) => {
|
| 1083 |
if (prev.includes(value)) return prev.filter((item) => item !== value)
|
|
@@ -1473,6 +2159,16 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1473 |
))}
|
| 1474 |
</select>
|
| 1475 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
<MapFrame html={mapaHtml} />
|
| 1477 |
</details>
|
| 1478 |
)}
|
|
@@ -1490,6 +2186,16 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1490 |
<div className="resumo-outliers-box">Índices excluídos: {joinSelection(outliersAnteriores)}</div>
|
| 1491 |
) : null}
|
| 1492 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
<DataTable table={dados} maxHeight={540} />
|
| 1494 |
</div>
|
| 1495 |
</div>
|
|
@@ -1530,6 +2236,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1530 |
>
|
| 1531 |
Aplicar período
|
| 1532 |
</button>
|
|
|
|
| 1533 |
</div>
|
| 1534 |
{dataMercadoError ? <div className="error-line inline-error">{dataMercadoError}</div> : null}
|
| 1535 |
</div>
|
|
@@ -1538,16 +2245,29 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1538 |
<SectionBlock step="5" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
|
| 1539 |
<div className="row">
|
| 1540 |
<label>Variável Dependente (Y)</label>
|
| 1541 |
-
<select value={
|
| 1542 |
<option value="">Selecione</option>
|
| 1543 |
{colunasNumericas.map((col) => (
|
| 1544 |
<option key={col} value={col}>{col}</option>
|
| 1545 |
))}
|
| 1546 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
</div>
|
| 1548 |
</SectionBlock>
|
| 1549 |
|
| 1550 |
<SectionBlock step="6" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1551 |
<div className="compact-option-group compact-option-group-x">
|
| 1552 |
<h4>Variáveis Independentes (X)</h4>
|
| 1553 |
<div className="checkbox-inline-wrap checkbox-inline-wrap-tools">
|
|
@@ -1616,7 +2336,8 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1616 |
</div>
|
| 1617 |
|
| 1618 |
<div className="row">
|
| 1619 |
-
<button onClick={onApplySelection} disabled={loading || !colunaY || colunasX.length === 0}>Aplicar seleção</button>
|
|
|
|
| 1620 |
</div>
|
| 1621 |
{selection?.aviso_multicolinearidade?.visible ? (
|
| 1622 |
<div dangerouslySetInnerHTML={{ __html: selection.aviso_multicolinearidade.html }} />
|
|
@@ -1631,6 +2352,16 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1631 |
{selection ? (
|
| 1632 |
<>
|
| 1633 |
<SectionBlock step="7" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1634 |
<DataTable table={selection.estatisticas} />
|
| 1635 |
</SectionBlock>
|
| 1636 |
|
|
@@ -1646,12 +2377,59 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1646 |
>
|
| 1647 |
<summary>Mostrar/Ocultar gráficos da seção</summary>
|
| 1648 |
{section8Open ? (
|
| 1649 |
-
<
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
| 1653 |
-
|
| 1654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1655 |
) : null}
|
| 1656 |
</details>
|
| 1657 |
</SectionBlock>
|
|
@@ -1671,6 +2449,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1671 |
))}
|
| 1672 |
</select>
|
| 1673 |
<button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
|
|
|
|
| 1674 |
</div>
|
| 1675 |
{(selection.busca?.resultados || []).length > 0 ? (
|
| 1676 |
<div className="transform-suggestions-grid">
|
|
@@ -1752,7 +2531,10 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1752 |
))}
|
| 1753 |
</div>
|
| 1754 |
|
| 1755 |
-
<
|
|
|
|
|
|
|
|
|
|
| 1756 |
</>
|
| 1757 |
) : null}
|
| 1758 |
{transformacoesAplicadas?.coluna_y ? (
|
|
@@ -1814,7 +2596,57 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1814 |
<option value="Variáveis Independentes Não Transformadas X Resíduo Padronizado">X não transformado x Resíduo</option>
|
| 1815 |
</select>
|
| 1816 |
</div>
|
| 1817 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1818 |
</>
|
| 1819 |
) : null}
|
| 1820 |
</details>
|
|
@@ -1822,6 +2654,36 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1822 |
|
| 1823 |
<SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 1824 |
<div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1825 |
<div className="two-col diagnostic-tables">
|
| 1826 |
<div className="pane">
|
| 1827 |
<h4>Tabela de Coeficientes</h4>
|
|
@@ -1835,6 +2697,63 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1835 |
</SectionBlock>
|
| 1836 |
|
| 1837 |
<SectionBlock step="14" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1838 |
<div className="plot-grid-2-fixed">
|
| 1839 |
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 1840 |
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
|
@@ -1847,6 +2766,16 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1847 |
</SectionBlock>
|
| 1848 |
|
| 1849 |
<SectionBlock step="15" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1850 |
<DataTable table={fit.tabela_metricas} maxHeight={320} />
|
| 1851 |
</SectionBlock>
|
| 1852 |
|
|
@@ -1908,6 +2837,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1908 |
<div className="outlier-actions-row">
|
| 1909 |
<button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
|
| 1910 |
<button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
|
|
|
|
| 1911 |
</div>
|
| 1912 |
</div>
|
| 1913 |
|
|
@@ -1928,6 +2858,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1928 |
<button onClick={onRestartIteration} disabled={loading} className="btn-reiniciar-iteracao">
|
| 1929 |
Atualizar Modelo (Excluir/Reincluir Outliers)
|
| 1930 |
</button>
|
|
|
|
| 1931 |
</div>
|
| 1932 |
</div>
|
| 1933 |
<div className="resumo-outliers-box">Iteração: {iteracao} | {resumoOutliers}</div>
|
|
@@ -1944,6 +2875,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1944 |
defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
|
| 1945 |
onChange={(e) => {
|
| 1946 |
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
|
|
|
|
| 1947 |
}}
|
| 1948 |
>
|
| 1949 |
<option value="">Selecione</option>
|
|
@@ -1960,6 +2892,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1960 |
placeholder={campo.placeholder || ''}
|
| 1961 |
onChange={(e) => {
|
| 1962 |
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
|
|
|
|
| 1963 |
}}
|
| 1964 |
/>
|
| 1965 |
)}
|
|
@@ -1970,6 +2903,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1970 |
<button onClick={onCalculateAvaliacao} disabled={loading}>Calcular</button>
|
| 1971 |
<button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
|
| 1972 |
<button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações (Excel)</button>
|
|
|
|
| 1973 |
</div>
|
| 1974 |
<div className="row avaliacao-base-row">
|
| 1975 |
<label>Base comparação</label>
|
|
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
+
import Plotly from 'plotly.js-dist-min'
|
| 4 |
import DataTable from './DataTable'
|
| 5 |
import LoadingOverlay from './LoadingOverlay'
|
| 6 |
import MapFrame from './MapFrame'
|
|
|
|
| 46 |
return values.join(', ')
|
| 47 |
}
|
| 48 |
|
| 49 |
+
function normalizeSnapshotArray(values) {
|
| 50 |
+
if (!Array.isArray(values)) return []
|
| 51 |
+
return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))].sort()
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function buildSelectionSnapshot(payload = {}) {
|
| 55 |
+
return JSON.stringify({
|
| 56 |
+
coluna_y: String(payload.coluna_y || '').trim(),
|
| 57 |
+
colunas_x: normalizeSnapshotArray(payload.colunas_x),
|
| 58 |
+
dicotomicas: normalizeSnapshotArray(payload.dicotomicas),
|
| 59 |
+
codigo_alocado: normalizeSnapshotArray(payload.codigo_alocado),
|
| 60 |
+
percentuais: normalizeSnapshotArray(payload.percentuais),
|
| 61 |
+
})
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function buildGrauSnapshot(grauCoef, grauF) {
|
| 65 |
+
return `${Number(grauCoef) || 0}|${Number(grauF) || 0}`
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function buildTransformacoesSnapshot(transformacaoY, transformacoesX) {
|
| 69 |
+
const transformacoesOrdenadas = Object.entries(transformacoesX || {})
|
| 70 |
+
.map(([coluna, valor]) => [String(coluna), String(valor || '(x)')])
|
| 71 |
+
.sort(([a], [b]) => a.localeCompare(b, 'pt-BR'))
|
| 72 |
+
return JSON.stringify({
|
| 73 |
+
transformacao_y: String(transformacaoY || '(x)'),
|
| 74 |
+
transformacoes_x: transformacoesOrdenadas,
|
| 75 |
+
})
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function buildFiltrosSnapshot(filtros) {
|
| 79 |
+
if (!Array.isArray(filtros)) return '[]'
|
| 80 |
+
return JSON.stringify(
|
| 81 |
+
filtros.map((item) => ({
|
| 82 |
+
variavel: String(item?.variavel || ''),
|
| 83 |
+
operador: String(item?.operador || ''),
|
| 84 |
+
valor: Number(item?.valor ?? 0),
|
| 85 |
+
})),
|
| 86 |
+
)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function buildOutlierTextSnapshot(outliersTexto, reincluirTexto) {
|
| 90 |
+
return JSON.stringify({
|
| 91 |
+
outliers: String(outliersTexto || '').trim(),
|
| 92 |
+
reincluir: String(reincluirTexto || '').trim(),
|
| 93 |
+
})
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function sleep(ms) {
|
| 97 |
+
return new Promise((resolve) => {
|
| 98 |
+
window.setTimeout(resolve, ms)
|
| 99 |
+
})
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function toCsvCell(value, separator = ';') {
|
| 103 |
+
const text = String(value ?? '')
|
| 104 |
+
const escaped = text.replace(/"/g, '""')
|
| 105 |
+
if (escaped.includes('"') || escaped.includes('\n') || escaped.includes('\r') || escaped.includes(separator)) {
|
| 106 |
+
return `"${escaped}"`
|
| 107 |
+
}
|
| 108 |
+
return escaped
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function tableToCsvBlob(table) {
|
| 112 |
+
if (!table || !Array.isArray(table.columns) || !Array.isArray(table.rows) || table.columns.length === 0) return null
|
| 113 |
+
|
| 114 |
+
const separator = ';'
|
| 115 |
+
const header = table.columns.map((col) => toCsvCell(col, separator)).join(separator)
|
| 116 |
+
const lines = [header]
|
| 117 |
+
|
| 118 |
+
table.rows.forEach((row) => {
|
| 119 |
+
const values = table.columns.map((col) => toCsvCell(row?.[col], separator))
|
| 120 |
+
lines.push(values.join(separator))
|
| 121 |
+
})
|
| 122 |
+
|
| 123 |
+
const csv = `\uFEFF${lines.join('\n')}`
|
| 124 |
+
return new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function sanitizeFileName(name, fallback = 'arquivo') {
|
| 128 |
+
const safe = String(name || '')
|
| 129 |
+
.normalize('NFD')
|
| 130 |
+
.replace(/[\u0300-\u036f]/g, '')
|
| 131 |
+
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
| 132 |
+
.replace(/^_+|_+$/g, '')
|
| 133 |
+
return safe || fallback
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
function buildFigureForExport(figure, forceHideLegend = false) {
|
| 137 |
+
if (!figure || typeof figure !== 'object') return null
|
| 138 |
+
|
| 139 |
+
const data = (figure.data || []).map((trace) => {
|
| 140 |
+
if (!forceHideLegend) return trace
|
| 141 |
+
return { ...trace, showlegend: false }
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
const baseLayout = figure.layout || {}
|
| 145 |
+
const { width: _ignoreWidth, ...layoutNoWidth } = baseLayout
|
| 146 |
+
const safeAnnotations = Array.isArray(baseLayout.annotations)
|
| 147 |
+
? baseLayout.annotations.map((annotation) => {
|
| 148 |
+
const {
|
| 149 |
+
ax,
|
| 150 |
+
ay,
|
| 151 |
+
axref,
|
| 152 |
+
ayref,
|
| 153 |
+
arrowhead,
|
| 154 |
+
arrowsize,
|
| 155 |
+
arrowwidth,
|
| 156 |
+
arrowcolor,
|
| 157 |
+
standoff,
|
| 158 |
+
...clean
|
| 159 |
+
} = annotation || {}
|
| 160 |
+
return { ...clean, showarrow: false }
|
| 161 |
+
})
|
| 162 |
+
: baseLayout.annotations
|
| 163 |
+
|
| 164 |
+
const width = Math.max(1100, Number(baseLayout.width) || 1100)
|
| 165 |
+
const height = Math.max(640, Number(baseLayout.height) || 640)
|
| 166 |
+
const layout = {
|
| 167 |
+
...layoutNoWidth,
|
| 168 |
+
autosize: false,
|
| 169 |
+
width,
|
| 170 |
+
height,
|
| 171 |
+
annotations: safeAnnotations,
|
| 172 |
+
margin: baseLayout.margin || { t: 40, r: 20, b: 50, l: 50 },
|
| 173 |
+
}
|
| 174 |
+
if (forceHideLegend) {
|
| 175 |
+
layout.showlegend = false
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return { data, layout, width, height }
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function normalizeAxisRef(axisRef, axisType) {
|
| 182 |
+
const raw = String(axisRef || '').trim().toLowerCase()
|
| 183 |
+
if (!raw || raw === axisType) return `${axisType}axis`
|
| 184 |
+
if (raw.startsWith(`${axisType}axis`)) return raw
|
| 185 |
+
if (raw.startsWith(axisType)) return `${axisType}axis${raw.slice(axisType.length)}`
|
| 186 |
+
return `${axisType}axis`
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
function axisKeyToToken(axisKey, axisType) {
|
| 190 |
+
const normalized = normalizeAxisRef(axisKey, axisType)
|
| 191 |
+
if (normalized === `${axisType}axis`) return axisType
|
| 192 |
+
return `${axisType}${normalized.replace(`${axisType}axis`, '')}`
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function getAxisTitleText(axisObj) {
|
| 196 |
+
if (!axisObj || typeof axisObj !== 'object') return ''
|
| 197 |
+
const title = axisObj.title
|
| 198 |
+
if (typeof title === 'string') return String(title).trim()
|
| 199 |
+
if (title && typeof title === 'object') {
|
| 200 |
+
return String(title.text || '').trim()
|
| 201 |
+
}
|
| 202 |
+
return ''
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function stripHtmlText(value) {
|
| 206 |
+
return String(value || '')
|
| 207 |
+
.replace(/<br\s*\/?>/gi, ' ')
|
| 208 |
+
.replace(/<[^>]*>/g, ' ')
|
| 209 |
+
.replace(/\s+/g, ' ')
|
| 210 |
+
.trim()
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
function parsePanelHeading(tituloHtml, fallbackTitle) {
|
| 214 |
+
const raw = String(tituloHtml || '').trim()
|
| 215 |
+
const fallback = String(fallbackTitle || '').trim() || 'Gráfico'
|
| 216 |
+
if (!raw) {
|
| 217 |
+
return { title: fallback, subtitle: '' }
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
const parts = raw
|
| 221 |
+
.split(/<br\s*\/?>|\n/gi)
|
| 222 |
+
.map((part) => stripHtmlText(part))
|
| 223 |
+
.filter(Boolean)
|
| 224 |
+
|
| 225 |
+
if (parts.length === 0) {
|
| 226 |
+
return { title: fallback, subtitle: '' }
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const [first, ...rest] = parts
|
| 230 |
+
const subtitleRaw = rest.join(' • ').trim()
|
| 231 |
+
const subtitle = /^r\s*=/i.test(subtitleRaw)
|
| 232 |
+
? `Correlação (${subtitleRaw.replace(/\s+/g, ' ').replace(/^r\s*=/i, 'r =').trim()})`
|
| 233 |
+
: subtitleRaw
|
| 234 |
+
|
| 235 |
+
return {
|
| 236 |
+
title: first || fallback,
|
| 237 |
+
subtitle,
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
function matchesShapeAxisRef(ref, token) {
|
| 242 |
+
const text = String(ref || '').trim().toLowerCase()
|
| 243 |
+
if (!text) return false
|
| 244 |
+
return text === token || text === `${token} domain`
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function filterShapesForPanel(layoutShapes, xToken, yToken) {
|
| 248 |
+
if (!Array.isArray(layoutShapes)) return []
|
| 249 |
+
return layoutShapes.filter((shape) => {
|
| 250 |
+
const xref = String(shape?.xref || '').trim().toLowerCase()
|
| 251 |
+
const yref = String(shape?.yref || '').trim().toLowerCase()
|
| 252 |
+
const hasX = matchesShapeAxisRef(xref, xToken)
|
| 253 |
+
const hasY = matchesShapeAxisRef(yref, yToken)
|
| 254 |
+
|
| 255 |
+
if (hasX && hasY) return true
|
| 256 |
+
if (hasX && !yref) return true
|
| 257 |
+
if (hasY && !xref) return true
|
| 258 |
+
return false
|
| 259 |
+
})
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function stylePanelTrace(trace) {
|
| 263 |
+
const mode = String(trace?.mode || '')
|
| 264 |
+
const traceName = String(trace?.name || '').toLowerCase()
|
| 265 |
+
const hasMarkers = mode.includes('markers')
|
| 266 |
+
const hasLines = mode.includes('lines')
|
| 267 |
+
const next = { ...trace, showlegend: false }
|
| 268 |
+
|
| 269 |
+
if (hasMarkers) {
|
| 270 |
+
next.marker = {
|
| 271 |
+
...(trace?.marker || {}),
|
| 272 |
+
color: '#FF8C00',
|
| 273 |
+
size: Number(trace?.marker?.size) || 8,
|
| 274 |
+
opacity: trace?.marker?.opacity ?? 0.84,
|
| 275 |
+
line: {
|
| 276 |
+
color: '#1f2933',
|
| 277 |
+
width: 1,
|
| 278 |
+
...(trace?.marker?.line || {}),
|
| 279 |
+
},
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
if (hasLines) {
|
| 284 |
+
const currentDash = String(trace?.line?.dash || '').trim()
|
| 285 |
+
next.line = {
|
| 286 |
+
...(trace?.line || {}),
|
| 287 |
+
color: trace?.line?.color || '#dc3545',
|
| 288 |
+
width: Number(trace?.line?.width) || 2,
|
| 289 |
+
dash: currentDash || (traceName.includes('regress') ? 'solid' : 'dash'),
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
return next
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function buildScatterPanels(figure, options = {}) {
|
| 297 |
+
if (!figure || typeof figure !== 'object') return []
|
| 298 |
+
const data = Array.isArray(figure.data) ? figure.data : []
|
| 299 |
+
const rawLayout = figure.layout || {}
|
| 300 |
+
const { title: _ignoreTitle, ...layout } = rawLayout
|
| 301 |
+
if (data.length === 0) return []
|
| 302 |
+
|
| 303 |
+
const grouped = new Map()
|
| 304 |
+
data.forEach((trace, index) => {
|
| 305 |
+
const xKey = normalizeAxisRef(trace?.xaxis, 'x')
|
| 306 |
+
const yKey = normalizeAxisRef(trace?.yaxis, 'y')
|
| 307 |
+
const groupKey = `${xKey}|${yKey}`
|
| 308 |
+
const current = grouped.get(groupKey) || []
|
| 309 |
+
current.push({ trace, index, xKey, yKey })
|
| 310 |
+
grouped.set(groupKey, current)
|
| 311 |
+
})
|
| 312 |
+
|
| 313 |
+
const keys = Array.from(grouped.keys())
|
| 314 |
+
return keys.map((groupKey, idx) => {
|
| 315 |
+
const traces = grouped.get(groupKey) || []
|
| 316 |
+
const markerTrace = traces.find((item) => String(item?.trace?.mode || '').includes('markers'))?.trace || {}
|
| 317 |
+
const xKey = traces[0]?.xKey || 'xaxis'
|
| 318 |
+
const yKey = traces[0]?.yKey || 'yaxis'
|
| 319 |
+
const xToken = axisKeyToToken(xKey, 'x')
|
| 320 |
+
const yToken = axisKeyToToken(yKey, 'y')
|
| 321 |
+
const axisX = layout[xKey] || layout.xaxis || {}
|
| 322 |
+
const axisY = layout[yKey] || layout.yaxis || {}
|
| 323 |
+
const xTitle = getAxisTitleText(axisX)
|
| 324 |
+
const yTitle = getAxisTitleText(axisY)
|
| 325 |
+
const markerName = String(markerTrace?.name || '').replace(/^Regress[aã]o\s+/i, '').trim()
|
| 326 |
+
const xName = xTitle || markerName || `Gráfico ${idx + 1}`
|
| 327 |
+
const yName = String(options.yLabel || yTitle || 'Y').trim()
|
| 328 |
+
const annotationTitleHtml = String(layout?.annotations?.[idx]?.text || '').trim()
|
| 329 |
+
const tituloHtml = annotationTitleHtml || `${xName} × ${yName}`
|
| 330 |
+
const heading = parsePanelHeading(tituloHtml, `${xName} × ${yName}`)
|
| 331 |
+
|
| 332 |
+
const label = xName
|
| 333 |
+
const panelTraces = traces.map((item) => stylePanelTrace({
|
| 334 |
+
...item.trace,
|
| 335 |
+
xaxis: 'x',
|
| 336 |
+
yaxis: 'y',
|
| 337 |
+
}))
|
| 338 |
+
|
| 339 |
+
const panelFigure = {
|
| 340 |
+
data: panelTraces,
|
| 341 |
+
layout: {
|
| 342 |
+
...layout,
|
| 343 |
+
showlegend: false,
|
| 344 |
+
autosize: true,
|
| 345 |
+
height: options.height || 360,
|
| 346 |
+
plot_bgcolor: 'white',
|
| 347 |
+
paper_bgcolor: 'white',
|
| 348 |
+
margin: { t: 18, r: 18, b: 44, l: 56 },
|
| 349 |
+
annotations: [],
|
| 350 |
+
shapes: filterShapesForPanel(layout.shapes, xToken, yToken),
|
| 351 |
+
xaxis: {
|
| 352 |
+
...axisX,
|
| 353 |
+
domain: undefined,
|
| 354 |
+
anchor: undefined,
|
| 355 |
+
matches: undefined,
|
| 356 |
+
scaleanchor: undefined,
|
| 357 |
+
scaleratio: undefined,
|
| 358 |
+
overlaying: undefined,
|
| 359 |
+
position: undefined,
|
| 360 |
+
side: axisX.side || 'bottom',
|
| 361 |
+
showgrid: true,
|
| 362 |
+
gridcolor: '#d7dde3',
|
| 363 |
+
showline: true,
|
| 364 |
+
linecolor: '#1f2933',
|
| 365 |
+
zeroline: false,
|
| 366 |
+
},
|
| 367 |
+
yaxis: {
|
| 368 |
+
...axisY,
|
| 369 |
+
domain: undefined,
|
| 370 |
+
anchor: undefined,
|
| 371 |
+
matches: undefined,
|
| 372 |
+
scaleanchor: undefined,
|
| 373 |
+
scaleratio: undefined,
|
| 374 |
+
overlaying: undefined,
|
| 375 |
+
position: undefined,
|
| 376 |
+
side: axisY.side || 'left',
|
| 377 |
+
showgrid: true,
|
| 378 |
+
gridcolor: '#d7dde3',
|
| 379 |
+
showline: true,
|
| 380 |
+
linecolor: '#1f2933',
|
| 381 |
+
zeroline: false,
|
| 382 |
+
},
|
| 383 |
+
},
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
const legenda = [heading.title, heading.subtitle].filter(Boolean).join(' - ')
|
| 387 |
+
return {
|
| 388 |
+
id: `${groupKey}-${idx}`,
|
| 389 |
+
label,
|
| 390 |
+
title: heading.title,
|
| 391 |
+
subtitle: heading.subtitle,
|
| 392 |
+
legenda,
|
| 393 |
+
figure: panelFigure,
|
| 394 |
+
}
|
| 395 |
+
})
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
function formatConselhoRegistro(elaborador) {
|
| 399 |
if (!elaborador) return ''
|
| 400 |
const conselho = String(elaborador.conselho || '').trim()
|
|
|
|
| 494 |
|
| 495 |
export default function ElaboracaoTab({ sessionId }) {
|
| 496 |
const [loading, setLoading] = useState(false)
|
| 497 |
+
const [downloadingAssets, setDownloadingAssets] = useState(false)
|
| 498 |
const [error, setError] = useState('')
|
| 499 |
const [status, setStatus] = useState('')
|
| 500 |
|
|
|
|
| 526 |
|
| 527 |
const [colunasNumericas, setColunasNumericas] = useState([])
|
| 528 |
const [colunaY, setColunaY] = useState('')
|
| 529 |
+
const [colunaYDraft, setColunaYDraft] = useState('')
|
| 530 |
const [colunasX, setColunasX] = useState([])
|
| 531 |
const [dicotomicas, setDicotomicas] = useState([])
|
| 532 |
const [codigoAlocado, setCodigoAlocado] = useState([])
|
|
|
|
| 534 |
const [colunasDataMercado, setColunasDataMercado] = useState([])
|
| 535 |
const [colunaDataMercadoSugerida, setColunaDataMercadoSugerida] = useState('')
|
| 536 |
const [colunaDataMercado, setColunaDataMercado] = useState('')
|
| 537 |
+
const [colunaDataMercadoAplicada, setColunaDataMercadoAplicada] = useState('')
|
| 538 |
const [periodoDadosMercado, setPeriodoDadosMercado] = useState(null)
|
| 539 |
const [periodoDadosMercadoPreview, setPeriodoDadosMercadoPreview] = useState(null)
|
| 540 |
const [dataMercadoError, setDataMercadoError] = useState('')
|
|
|
|
| 567 |
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 568 |
const valoresAvaliacaoRef = useRef({})
|
| 569 |
const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
|
| 570 |
+
const [avaliacaoPendente, setAvaliacaoPendente] = useState(false)
|
| 571 |
const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
|
| 572 |
const [baseChoices, setBaseChoices] = useState([])
|
| 573 |
const [baseValue, setBaseValue] = useState('')
|
|
|
|
| 576 |
const [avaliadores, setAvaliadores] = useState([])
|
| 577 |
const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
|
| 578 |
const [tipoFonteDados, setTipoFonteDados] = useState('')
|
| 579 |
+
const [selectionAppliedSnapshot, setSelectionAppliedSnapshot] = useState(() => buildSelectionSnapshot())
|
| 580 |
+
const [buscaTransformAppliedSnapshot, setBuscaTransformAppliedSnapshot] = useState(() => buildGrauSnapshot(0, 0))
|
| 581 |
+
const [manualTransformAppliedSnapshot, setManualTransformAppliedSnapshot] = useState(() => buildTransformacoesSnapshot('(x)', {}))
|
| 582 |
+
const [outlierFiltrosAplicadosSnapshot, setOutlierFiltrosAplicadosSnapshot] = useState(() => buildFiltrosSnapshot(defaultFiltros()))
|
| 583 |
+
const [outlierTextosAplicadosSnapshot, setOutlierTextosAplicadosSnapshot] = useState(() => buildOutlierTextSnapshot('', ''))
|
| 584 |
const marcarTodasXRef = useRef(null)
|
| 585 |
const classificarXReqRef = useRef(0)
|
| 586 |
const deleteConfirmTimersRef = useRef({})
|
|
|
|
| 588 |
|
| 589 |
const mapaChoices = useMemo(() => ['Visualização Padrão', ...colunasNumericas], [colunasNumericas])
|
| 590 |
const colunasXDisponiveis = useMemo(
|
| 591 |
+
() => (colunaY ? colunasNumericas.filter((coluna) => coluna !== colunaY) : []),
|
| 592 |
[colunasNumericas, colunaY],
|
| 593 |
)
|
| 594 |
const todasXMarcadas = useMemo(
|
|
|
|
| 628 |
() => formatPeriodoDadosMercado(periodoDadosMercadoPreview),
|
| 629 |
[periodoDadosMercadoPreview],
|
| 630 |
)
|
| 631 |
+
const pendingWarningText = 'Há alterações em campos que ainda não foram aplicadas.'
|
| 632 |
+
const dataMercadoPendente = useMemo(
|
| 633 |
+
() => String(colunaDataMercado || '') !== String(colunaDataMercadoAplicada || ''),
|
| 634 |
+
[colunaDataMercado, colunaDataMercadoAplicada],
|
| 635 |
+
)
|
| 636 |
+
const dependentePendente = useMemo(
|
| 637 |
+
() => String(colunaYDraft || '') !== String(colunaY || ''),
|
| 638 |
+
[colunaYDraft, colunaY],
|
| 639 |
+
)
|
| 640 |
+
const selectionSnapshotAtual = useMemo(
|
| 641 |
+
() => buildSelectionSnapshot({
|
| 642 |
+
coluna_y: colunaY,
|
| 643 |
+
colunas_x: colunasX,
|
| 644 |
+
dicotomicas,
|
| 645 |
+
codigo_alocado: codigoAlocado,
|
| 646 |
+
percentuais,
|
| 647 |
+
}),
|
| 648 |
+
[colunaY, colunasX, dicotomicas, codigoAlocado, percentuais],
|
| 649 |
+
)
|
| 650 |
+
const selecaoTemDados = useMemo(
|
| 651 |
+
() => Boolean(colunaY) && colunasX.length > 0,
|
| 652 |
+
[colunaY, colunasX],
|
| 653 |
+
)
|
| 654 |
+
const selecaoPendente = useMemo(
|
| 655 |
+
() => selecaoTemDados && selectionSnapshotAtual !== selectionAppliedSnapshot,
|
| 656 |
+
[selecaoTemDados, selectionSnapshotAtual, selectionAppliedSnapshot],
|
| 657 |
+
)
|
| 658 |
+
const buscaTransformSnapshotAtual = useMemo(
|
| 659 |
+
() => buildGrauSnapshot(grauCoef, grauF),
|
| 660 |
+
[grauCoef, grauF],
|
| 661 |
+
)
|
| 662 |
+
const buscaTransformPendente = useMemo(
|
| 663 |
+
() => Boolean(selection) && buscaTransformSnapshotAtual !== buscaTransformAppliedSnapshot,
|
| 664 |
+
[selection, buscaTransformSnapshotAtual, buscaTransformAppliedSnapshot],
|
| 665 |
+
)
|
| 666 |
+
const manualTransformSnapshotAtual = useMemo(
|
| 667 |
+
() => buildTransformacoesSnapshot(transformacaoY, transformacoesX),
|
| 668 |
+
[transformacaoY, transformacoesX],
|
| 669 |
+
)
|
| 670 |
+
const manualTransformPendente = useMemo(
|
| 671 |
+
() => Boolean(selection) && Boolean(section10ManualOpen) && manualTransformSnapshotAtual !== manualTransformAppliedSnapshot,
|
| 672 |
+
[selection, section10ManualOpen, manualTransformSnapshotAtual, manualTransformAppliedSnapshot],
|
| 673 |
+
)
|
| 674 |
+
const outlierFiltrosSnapshotAtual = useMemo(
|
| 675 |
+
() => buildFiltrosSnapshot(filtros),
|
| 676 |
+
[filtros],
|
| 677 |
+
)
|
| 678 |
+
const outlierFiltrosPendentes = useMemo(
|
| 679 |
+
() => Boolean(fit) && outlierFiltrosSnapshotAtual !== outlierFiltrosAplicadosSnapshot,
|
| 680 |
+
[fit, outlierFiltrosSnapshotAtual, outlierFiltrosAplicadosSnapshot],
|
| 681 |
+
)
|
| 682 |
+
const outlierTextosSnapshotAtual = useMemo(
|
| 683 |
+
() => buildOutlierTextSnapshot(outliersTexto, reincluirTexto),
|
| 684 |
+
[outliersTexto, reincluirTexto],
|
| 685 |
+
)
|
| 686 |
+
const outlierTextosPendentes = useMemo(
|
| 687 |
+
() => Boolean(fit) && outlierTextosSnapshotAtual !== outlierTextosAplicadosSnapshot,
|
| 688 |
+
[fit, outlierTextosSnapshotAtual, outlierTextosAplicadosSnapshot],
|
| 689 |
+
)
|
| 690 |
const transformacaoAplicadaYBadge = useMemo(
|
| 691 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 692 |
[transformacoesAplicadas],
|
|
|
|
| 715 |
}
|
| 716 |
return ''
|
| 717 |
}, [origemTransformacoes])
|
| 718 |
+
const graficosSecao9 = useMemo(
|
| 719 |
+
() => buildScatterPanels(selection?.grafico_dispersao, {
|
| 720 |
+
singleLabel: 'Dispersão',
|
| 721 |
+
height: 360,
|
| 722 |
+
yLabel: colunaY || 'Y',
|
| 723 |
+
}),
|
| 724 |
+
[selection?.grafico_dispersao, colunaY],
|
| 725 |
+
)
|
| 726 |
+
const colunasGraficosSecao9 = useMemo(
|
| 727 |
+
() => Math.max(1, Math.min(3, graficosSecao9.length || 1)),
|
| 728 |
+
[graficosSecao9.length],
|
| 729 |
+
)
|
| 730 |
+
const graficosSecao12 = useMemo(
|
| 731 |
+
() => buildScatterPanels(fit?.grafico_dispersao_modelo, {
|
| 732 |
+
singleLabel: 'Dispersão do modelo',
|
| 733 |
+
height: 360,
|
| 734 |
+
yLabel: tipoDispersao.includes('Resíduo')
|
| 735 |
+
? 'Resíduo Padronizado'
|
| 736 |
+
: `${colunaY || 'Y'} (transformada)`,
|
| 737 |
+
}),
|
| 738 |
+
[fit?.grafico_dispersao_modelo, tipoDispersao, colunaY],
|
| 739 |
+
)
|
| 740 |
+
const colunasGraficosSecao12 = useMemo(
|
| 741 |
+
() => Math.max(1, Math.min(3, graficosSecao12.length || 1)),
|
| 742 |
+
[graficosSecao12.length],
|
| 743 |
+
)
|
| 744 |
const showCoordsPanel = Boolean(
|
| 745 |
coordsInfo && (
|
| 746 |
!coordsInfo.tem_coords ||
|
|
|
|
| 852 |
|
| 853 |
function applyBaseResponse(resp, options = {}) {
|
| 854 |
const resetXSelection = Boolean(options.resetXSelection)
|
| 855 |
+
const colunaYPadrao = String(resp.coluna_y_padrao || '')
|
| 856 |
setDataMercadoError('')
|
| 857 |
if (resp.status) setStatus(resp.status)
|
| 858 |
if (resp.dados) setDados(resp.dados)
|
|
|
|
| 865 |
setColunaDataMercadoSugerida(String(resp.coluna_data_mercado_sugerida || ''))
|
| 866 |
}
|
| 867 |
if (Object.prototype.hasOwnProperty.call(resp, 'coluna_data_mercado')) {
|
| 868 |
+
const colunaData = String(resp.coluna_data_mercado || '')
|
| 869 |
+
setColunaDataMercado(colunaData)
|
| 870 |
+
setColunaDataMercadoAplicada(colunaData)
|
| 871 |
}
|
| 872 |
if (Object.prototype.hasOwnProperty.call(resp, 'periodo_dados_mercado')) {
|
| 873 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 874 |
setPeriodoDadosMercado(periodo)
|
| 875 |
setPeriodoDadosMercadoPreview(periodo)
|
| 876 |
}
|
| 877 |
+
if (Object.prototype.hasOwnProperty.call(resp, 'coluna_y_padrao')) {
|
| 878 |
+
setColunaY(colunaYPadrao)
|
| 879 |
+
setColunaYDraft(colunaYPadrao)
|
| 880 |
+
}
|
| 881 |
|
| 882 |
if (resp.contexto && !resetXSelection) {
|
| 883 |
+
const colunaYContexto = String(resp.contexto.coluna_y || colunaYPadrao || '')
|
| 884 |
+
const colunasXContexto = resp.contexto.colunas_x || []
|
| 885 |
+
const dicotomicasContexto = resp.contexto.dicotomicas || []
|
| 886 |
+
const codigoAlocadoContexto = resp.contexto.codigo_alocado || []
|
| 887 |
+
const percentuaisContexto = resp.contexto.percentuais || []
|
| 888 |
+
setColunaY(colunaYContexto)
|
| 889 |
+
setColunaYDraft(colunaYContexto)
|
| 890 |
+
setColunasX(colunasXContexto)
|
| 891 |
+
setDicotomicas(dicotomicasContexto)
|
| 892 |
+
setCodigoAlocado(codigoAlocadoContexto)
|
| 893 |
+
setPercentuais(percentuaisContexto)
|
| 894 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot({
|
| 895 |
+
coluna_y: colunaYContexto,
|
| 896 |
+
colunas_x: colunasXContexto,
|
| 897 |
+
dicotomicas: dicotomicasContexto,
|
| 898 |
+
codigo_alocado: codigoAlocadoContexto,
|
| 899 |
+
percentuais: percentuaisContexto,
|
| 900 |
+
}))
|
| 901 |
setOutliersAnteriores(resp.contexto.outliers_anteriores || [])
|
| 902 |
setIteracao(resp.contexto.iteracao || 1)
|
| 903 |
} else if (resetXSelection) {
|
| 904 |
+
setColunaY('')
|
| 905 |
+
setColunaYDraft(colunaYPadrao)
|
| 906 |
setColunasX([])
|
| 907 |
setDicotomicas([])
|
| 908 |
setCodigoAlocado([])
|
| 909 |
setPercentuais([])
|
| 910 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: '' }))
|
| 911 |
setTransformacaoY('(x)')
|
| 912 |
setTransformacoesX({})
|
| 913 |
setTransformacoesAplicadas(null)
|
| 914 |
setOrigemTransformacoes(null)
|
| 915 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
|
| 916 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
|
| 917 |
setOutliersAnteriores([])
|
| 918 |
setIteracao(1)
|
| 919 |
setColunaDataMercadoSugerida('')
|
| 920 |
setColunaDataMercado('')
|
| 921 |
+
setColunaDataMercadoAplicada('')
|
| 922 |
setPeriodoDadosMercado(null)
|
| 923 |
setPeriodoDadosMercadoPreview(null)
|
| 924 |
setDataMercadoError('')
|
| 925 |
setSelection(null)
|
| 926 |
setFit(null)
|
| 927 |
+
setFiltros(defaultFiltros())
|
| 928 |
+
setOutliersTexto('')
|
| 929 |
+
setReincluirTexto('')
|
| 930 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 931 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 932 |
setCamposAvaliacao([])
|
| 933 |
valoresAvaliacaoRef.current = {}
|
| 934 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 935 |
+
setAvaliacaoPendente(false)
|
| 936 |
setResultadoAvaliacaoHtml('')
|
| 937 |
setOutliersHtml('')
|
| 938 |
}
|
|
|
|
| 963 |
setSection10ManualOpen(false)
|
| 964 |
setTransformacoesAplicadas(null)
|
| 965 |
setOrigemTransformacoes(null)
|
| 966 |
+
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
| 967 |
if (resp.transformacao_y) {
|
| 968 |
setTransformacaoY(resp.transformacao_y)
|
| 969 |
}
|
|
|
|
| 973 |
map[field.coluna] = field.valor || '(x)'
|
| 974 |
})
|
| 975 |
setTransformacoesX(map)
|
| 976 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot(transformacaoYAplicada, map))
|
| 977 |
|
| 978 |
+
const grauCoefAplicado = resp.busca?.grau_coef ?? grauCoef
|
| 979 |
+
const grauFAplicado = resp.busca?.grau_f ?? grauF
|
| 980 |
if (resp.busca) {
|
| 981 |
+
setGrauCoef(grauCoefAplicado)
|
| 982 |
+
setGrauF(grauFAplicado)
|
| 983 |
}
|
| 984 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoefAplicado, grauFAplicado))
|
| 985 |
|
| 986 |
if (resp.resumo_outliers) {
|
| 987 |
setResumoOutliers(resp.resumo_outliers)
|
|
|
|
| 998 |
function applyFitResponse(resp, origemMeta = null) {
|
| 999 |
setFit(resp)
|
| 1000 |
setSection11Open(false)
|
| 1001 |
+
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
| 1002 |
+
const transformacoesXAplicadas = resp.transformacoes_x || transformacoesX
|
| 1003 |
if (resp.transformacao_y) {
|
| 1004 |
setTransformacaoY(resp.transformacao_y)
|
| 1005 |
}
|
|
|
|
| 1015 |
transformacoes_x: resp.transformacoes_x || transformacoesX,
|
| 1016 |
})
|
| 1017 |
}
|
| 1018 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot(transformacaoYAplicada, transformacoesXAplicadas))
|
| 1019 |
if (origemMeta && origemMeta.origem) {
|
| 1020 |
setOrigemTransformacoes(origemMeta)
|
| 1021 |
} else if (resp.origem_transformacoes?.origem) {
|
| 1022 |
setOrigemTransformacoes(resp.origem_transformacoes)
|
| 1023 |
}
|
| 1024 |
setResumoOutliers(resp.resumo_outliers || resumoOutliers)
|
| 1025 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(filtros))
|
| 1026 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot(outliersTexto, reincluirTexto))
|
| 1027 |
setCamposAvaliacao(resp.avaliacao_campos || [])
|
| 1028 |
const init = {}
|
| 1029 |
;(resp.avaliacao_campos || []).forEach((campo) => {
|
|
|
|
| 1031 |
})
|
| 1032 |
valoresAvaliacaoRef.current = init
|
| 1033 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1034 |
+
setAvaliacaoPendente(false)
|
| 1035 |
setResultadoAvaliacaoHtml('')
|
| 1036 |
setBaseChoices([])
|
| 1037 |
setBaseValue('')
|
|
|
|
| 1066 |
applyBaseResponse(resp, { resetXSelection })
|
| 1067 |
} else if (resp.status) {
|
| 1068 |
setTipoFonteDados('tabular')
|
| 1069 |
+
setColunaY('')
|
| 1070 |
+
setColunaYDraft('')
|
| 1071 |
setSelection(null)
|
| 1072 |
setFit(null)
|
| 1073 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot())
|
| 1074 |
setColunasX([])
|
| 1075 |
setDicotomicas([])
|
| 1076 |
setCodigoAlocado([])
|
|
|
|
| 1079 |
setTransformacoesX({})
|
| 1080 |
setTransformacoesAplicadas(null)
|
| 1081 |
setOrigemTransformacoes(null)
|
| 1082 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
|
| 1083 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
|
| 1084 |
setColunasDataMercado([])
|
| 1085 |
setColunaDataMercadoSugerida('')
|
| 1086 |
setColunaDataMercado('')
|
| 1087 |
+
setColunaDataMercadoAplicada('')
|
| 1088 |
setPeriodoDadosMercado(null)
|
| 1089 |
setPeriodoDadosMercadoPreview(null)
|
| 1090 |
setDataMercadoError('')
|
| 1091 |
+
setFiltros(defaultFiltros())
|
| 1092 |
+
setOutliersTexto('')
|
| 1093 |
+
setReincluirTexto('')
|
| 1094 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 1095 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 1096 |
setCamposAvaliacao([])
|
| 1097 |
valoresAvaliacaoRef.current = {}
|
| 1098 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1099 |
+
setAvaliacaoPendente(false)
|
| 1100 |
setResultadoAvaliacaoHtml('')
|
| 1101 |
setStatus(resp.status)
|
| 1102 |
}
|
|
|
|
| 1159 |
const coluna = String(value || '')
|
| 1160 |
setColunaDataMercado(coluna)
|
| 1161 |
setDataMercadoError('')
|
| 1162 |
+
setStatus((prev) => (String(prev || '').startsWith('Prévia do período') ? '' : prev))
|
| 1163 |
|
| 1164 |
if (!sessionId || !coluna) {
|
| 1165 |
setPeriodoDadosMercadoPreview(null)
|
|
|
|
| 1171 |
const resp = await api.previewMarketDateColumn(sessionId, coluna)
|
| 1172 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 1173 |
setPeriodoDadosMercadoPreview(periodo)
|
|
|
|
| 1174 |
} catch (err) {
|
| 1175 |
setPeriodoDadosMercadoPreview(null)
|
| 1176 |
setDataMercadoError(err.message || 'Falha ao identificar período da coluna selecionada.')
|
|
|
|
| 1184 |
await withBusy(async () => {
|
| 1185 |
const resp = await api.applyMarketDateColumn(sessionId, colunaDataMercado)
|
| 1186 |
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 1187 |
+
const colunaAplicada = String(resp.coluna_data_mercado || colunaDataMercado)
|
| 1188 |
+
setColunaDataMercado(colunaAplicada)
|
| 1189 |
+
setColunaDataMercadoAplicada(colunaAplicada)
|
| 1190 |
setPeriodoDadosMercado(periodo)
|
| 1191 |
setPeriodoDadosMercadoPreview(periodo)
|
| 1192 |
setDataMercadoError('')
|
|
|
|
| 1201 |
})
|
| 1202 |
}
|
| 1203 |
|
| 1204 |
+
function onAplicarVariavelDependente() {
|
| 1205 |
+
const proximaColunaY = String(colunaYDraft || '')
|
| 1206 |
+
const colunaYAtual = String(colunaY || '')
|
| 1207 |
+
if (proximaColunaY === colunaYAtual) return
|
| 1208 |
+
|
| 1209 |
+
const proximasDisponiveis = colunasNumericas.filter((coluna) => coluna !== proximaColunaY)
|
| 1210 |
+
const proximasX = colunasX.filter((coluna) => proximasDisponiveis.includes(coluna))
|
| 1211 |
+
const proximasDicotomicas = dicotomicas.filter((coluna) => proximasX.includes(coluna))
|
| 1212 |
+
const proximoCodigoAlocado = codigoAlocado.filter((coluna) => proximasX.includes(coluna))
|
| 1213 |
+
const proximosPercentuais = percentuais.filter((coluna) => proximasX.includes(coluna))
|
| 1214 |
+
|
| 1215 |
+
setColunaY(proximaColunaY)
|
| 1216 |
+
setColunasX(proximasX)
|
| 1217 |
+
setDicotomicas(proximasDicotomicas)
|
| 1218 |
+
setCodigoAlocado(proximoCodigoAlocado)
|
| 1219 |
+
setPercentuais(proximosPercentuais)
|
| 1220 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: proximaColunaY }))
|
| 1221 |
+
setSelection(null)
|
| 1222 |
+
setFit(null)
|
| 1223 |
+
setTransformacaoY('(x)')
|
| 1224 |
+
setTransformacoesX({})
|
| 1225 |
+
setTransformacoesAplicadas(null)
|
| 1226 |
+
setOrigemTransformacoes(null)
|
| 1227 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
|
| 1228 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
|
| 1229 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 1230 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 1231 |
+
setFiltros(defaultFiltros())
|
| 1232 |
+
setCamposAvaliacao([])
|
| 1233 |
+
valoresAvaliacaoRef.current = {}
|
| 1234 |
+
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1235 |
+
setAvaliacaoPendente(false)
|
| 1236 |
+
setResultadoAvaliacaoHtml('')
|
| 1237 |
+
setOutliersHtml('')
|
| 1238 |
+
setOutliersTexto('')
|
| 1239 |
+
setReincluirTexto('')
|
| 1240 |
+
setBaseChoices([])
|
| 1241 |
+
setBaseValue('')
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
async function onMapCoords() {
|
| 1245 |
if (!manualLat || !manualLon || !sessionId) return
|
| 1246 |
setLoading(true)
|
|
|
|
| 1342 |
async function onApplySelection() {
|
| 1343 |
if (!sessionId || !colunaY || colunasX.length === 0) return
|
| 1344 |
await withBusy(async () => {
|
| 1345 |
+
const payload = {
|
| 1346 |
session_id: sessionId,
|
| 1347 |
coluna_y: colunaY,
|
| 1348 |
colunas_x: colunasX,
|
|
|
|
| 1352 |
outliers_anteriores: outliersAnteriores,
|
| 1353 |
grau_min_coef: grauCoef,
|
| 1354 |
grau_min_f: grauF,
|
| 1355 |
+
}
|
| 1356 |
+
const resp = await api.applySelection(payload)
|
| 1357 |
applySelectionResponse(resp)
|
| 1358 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot(payload))
|
| 1359 |
setFit(null)
|
| 1360 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 1361 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 1362 |
setCamposAvaliacao([])
|
| 1363 |
+
valoresAvaliacaoRef.current = {}
|
| 1364 |
+
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1365 |
+
setAvaliacaoPendente(false)
|
| 1366 |
setResultadoAvaliacaoHtml('')
|
| 1367 |
})
|
| 1368 |
}
|
|
|
|
| 1371 |
if (!sessionId) return
|
| 1372 |
await withBusy(async () => {
|
| 1373 |
const busca = await api.searchTransformations(sessionId, grauCoef, grauF)
|
| 1374 |
+
const grauCoefAplicado = busca.grau_coef ?? grauCoef
|
| 1375 |
+
const grauFAplicado = busca.grau_f ?? grauF
|
| 1376 |
setSelection((prev) => ({ ...prev, busca }))
|
| 1377 |
+
setGrauCoef(grauCoefAplicado)
|
| 1378 |
+
setGrauF(grauFAplicado)
|
| 1379 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoefAplicado, grauFAplicado))
|
| 1380 |
})
|
| 1381 |
}
|
| 1382 |
|
|
|
|
| 1438 |
const filtrosValidos = filtros.filter((f) => f.variavel && f.operador)
|
| 1439 |
const resp = await api.applyOutlierFilters(sessionId, filtrosValidos)
|
| 1440 |
setOutliersTexto(resp.texto || '')
|
| 1441 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(filtros))
|
| 1442 |
})
|
| 1443 |
}
|
| 1444 |
|
|
|
|
| 1471 |
setIteracao(resp.iteracao || iteracao)
|
| 1472 |
setOutliersTexto('')
|
| 1473 |
setReincluirTexto('')
|
| 1474 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 1475 |
setFit(null)
|
| 1476 |
setTransformacoesAplicadas(null)
|
| 1477 |
setOrigemTransformacoes(null)
|
|
|
|
| 1497 |
setTransformacoesAplicadas(null)
|
| 1498 |
setOrigemTransformacoes(null)
|
| 1499 |
setCamposAvaliacao([])
|
| 1500 |
+
setOutliersTexto('')
|
| 1501 |
+
setReincluirTexto('')
|
| 1502 |
+
setFiltros(defaultFiltros())
|
| 1503 |
valoresAvaliacaoRef.current = {}
|
| 1504 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1505 |
+
setAvaliacaoPendente(false)
|
| 1506 |
setResultadoAvaliacaoHtml('')
|
| 1507 |
setOutliersHtml(resp.outliers_html || '')
|
| 1508 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 1509 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 1510 |
})
|
| 1511 |
}
|
| 1512 |
|
|
|
|
| 1517 |
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 1518 |
setBaseChoices(resp.base_choices || [])
|
| 1519 |
setBaseValue(resp.base_value || '')
|
| 1520 |
+
setAvaliacaoPendente(false)
|
| 1521 |
})
|
| 1522 |
}
|
| 1523 |
|
|
|
|
| 1534 |
})
|
| 1535 |
valoresAvaliacaoRef.current = limpo
|
| 1536 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 1537 |
+
setAvaliacaoPendente(false)
|
| 1538 |
})
|
| 1539 |
}
|
| 1540 |
|
|
|
|
| 1638 |
})
|
| 1639 |
}
|
| 1640 |
|
| 1641 |
+
function onDownloadTableCsv(table, fileNameBase) {
|
| 1642 |
+
const blob = tableToCsvBlob(table)
|
| 1643 |
+
if (!blob) {
|
| 1644 |
+
setError('Tabela indisponível para download.')
|
| 1645 |
+
return
|
| 1646 |
+
}
|
| 1647 |
+
downloadBlob(blob, `${sanitizeFileName(fileNameBase, 'tabela')}.csv`)
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
async function onDownloadTablesCsvBatch(items) {
|
| 1651 |
+
const validItems = (items || []).filter((item) => item?.table)
|
| 1652 |
+
if (validItems.length === 0) {
|
| 1653 |
+
setError('Não há tabelas disponíveis para download.')
|
| 1654 |
+
return
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
setDownloadingAssets(true)
|
| 1658 |
+
setError('')
|
| 1659 |
+
try {
|
| 1660 |
+
for (let i = 0; i < validItems.length; i += 1) {
|
| 1661 |
+
const item = validItems[i]
|
| 1662 |
+
const blob = tableToCsvBlob(item.table)
|
| 1663 |
+
if (!blob) continue
|
| 1664 |
+
downloadBlob(blob, `${sanitizeFileName(item.fileNameBase, `tabela_${i + 1}`)}.csv`)
|
| 1665 |
+
if (i < validItems.length - 1) {
|
| 1666 |
+
await sleep(180)
|
| 1667 |
+
}
|
| 1668 |
+
}
|
| 1669 |
+
} catch (err) {
|
| 1670 |
+
setError(err.message || 'Falha ao baixar tabelas.')
|
| 1671 |
+
} finally {
|
| 1672 |
+
setDownloadingAssets(false)
|
| 1673 |
+
}
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
function onDownloadMapaSecao3() {
|
| 1677 |
+
if (!mapaHtml) {
|
| 1678 |
+
setError('Mapa indisponível para download.')
|
| 1679 |
+
return
|
| 1680 |
+
}
|
| 1681 |
+
const blob = new Blob([mapaHtml], { type: 'text/html;charset=utf-8;' })
|
| 1682 |
+
downloadBlob(blob, 'secao3_mapa_dados_mercado.html')
|
| 1683 |
+
}
|
| 1684 |
+
|
| 1685 |
+
async function exportFigureAsPng(figure, fileNameBase, options = {}) {
|
| 1686 |
+
const payload = buildFigureForExport(figure, Boolean(options.forceHideLegend))
|
| 1687 |
+
if (!payload) {
|
| 1688 |
+
throw new Error('Gráfico indisponível para download.')
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
const container = document.createElement('div')
|
| 1692 |
+
container.style.position = 'fixed'
|
| 1693 |
+
container.style.left = '-10000px'
|
| 1694 |
+
container.style.top = '-10000px'
|
| 1695 |
+
container.style.width = `${payload.width}px`
|
| 1696 |
+
container.style.height = `${payload.height}px`
|
| 1697 |
+
container.style.pointerEvents = 'none'
|
| 1698 |
+
container.style.opacity = '0'
|
| 1699 |
+
document.body.appendChild(container)
|
| 1700 |
+
|
| 1701 |
+
try {
|
| 1702 |
+
await Plotly.newPlot(
|
| 1703 |
+
container,
|
| 1704 |
+
payload.data,
|
| 1705 |
+
payload.layout,
|
| 1706 |
+
{ displaylogo: false, responsive: false, staticPlot: true },
|
| 1707 |
+
)
|
| 1708 |
+
const dataUrl = await Plotly.toImage(container, {
|
| 1709 |
+
format: 'png',
|
| 1710 |
+
width: payload.width,
|
| 1711 |
+
height: payload.height,
|
| 1712 |
+
scale: 2,
|
| 1713 |
+
})
|
| 1714 |
+
const response = await fetch(dataUrl)
|
| 1715 |
+
const blob = await response.blob()
|
| 1716 |
+
downloadBlob(blob, `${sanitizeFileName(fileNameBase, 'grafico')}.png`)
|
| 1717 |
+
} finally {
|
| 1718 |
+
try {
|
| 1719 |
+
Plotly.purge(container)
|
| 1720 |
+
} catch {
|
| 1721 |
+
// no-op
|
| 1722 |
+
}
|
| 1723 |
+
container.remove()
|
| 1724 |
+
}
|
| 1725 |
+
}
|
| 1726 |
+
|
| 1727 |
+
async function onDownloadFigurePng(figure, fileNameBase, options = {}) {
|
| 1728 |
+
if (!figure) {
|
| 1729 |
+
setError('Gráfico indisponível para download.')
|
| 1730 |
+
return
|
| 1731 |
+
}
|
| 1732 |
+
setDownloadingAssets(true)
|
| 1733 |
+
setError('')
|
| 1734 |
+
try {
|
| 1735 |
+
await exportFigureAsPng(figure, fileNameBase, options)
|
| 1736 |
+
} catch (err) {
|
| 1737 |
+
setError(err.message || 'Falha ao baixar gráfico.')
|
| 1738 |
+
} finally {
|
| 1739 |
+
setDownloadingAssets(false)
|
| 1740 |
+
}
|
| 1741 |
+
}
|
| 1742 |
+
|
| 1743 |
+
async function onDownloadFiguresPngBatch(items) {
|
| 1744 |
+
const validItems = (items || []).filter((item) => item?.figure)
|
| 1745 |
+
if (validItems.length === 0) {
|
| 1746 |
+
setError('Não há gráficos disponíveis para download.')
|
| 1747 |
+
return
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
setDownloadingAssets(true)
|
| 1751 |
+
setError('')
|
| 1752 |
+
try {
|
| 1753 |
+
for (let i = 0; i < validItems.length; i += 1) {
|
| 1754 |
+
const item = validItems[i]
|
| 1755 |
+
await exportFigureAsPng(item.figure, item.fileNameBase, { forceHideLegend: item.forceHideLegend })
|
| 1756 |
+
if (i < validItems.length - 1) {
|
| 1757 |
+
await sleep(220)
|
| 1758 |
+
}
|
| 1759 |
+
}
|
| 1760 |
+
} catch (err) {
|
| 1761 |
+
setError(err.message || 'Falha ao baixar gráficos.')
|
| 1762 |
+
} finally {
|
| 1763 |
+
setDownloadingAssets(false)
|
| 1764 |
+
}
|
| 1765 |
+
}
|
| 1766 |
+
|
| 1767 |
function toggleSelection(setter, value) {
|
| 1768 |
setter((prev) => {
|
| 1769 |
if (prev.includes(value)) return prev.filter((item) => item !== value)
|
|
|
|
| 2159 |
))}
|
| 2160 |
</select>
|
| 2161 |
</div>
|
| 2162 |
+
<div className="download-actions-bar">
|
| 2163 |
+
<button
|
| 2164 |
+
type="button"
|
| 2165 |
+
className="btn-download-subtle"
|
| 2166 |
+
onClick={onDownloadMapaSecao3}
|
| 2167 |
+
disabled={loading || downloadingAssets || !mapaHtml}
|
| 2168 |
+
>
|
| 2169 |
+
Fazer download
|
| 2170 |
+
</button>
|
| 2171 |
+
</div>
|
| 2172 |
<MapFrame html={mapaHtml} />
|
| 2173 |
</details>
|
| 2174 |
)}
|
|
|
|
| 2186 |
<div className="resumo-outliers-box">Índices excluídos: {joinSelection(outliersAnteriores)}</div>
|
| 2187 |
) : null}
|
| 2188 |
</div>
|
| 2189 |
+
<div className="download-actions-bar">
|
| 2190 |
+
<button
|
| 2191 |
+
type="button"
|
| 2192 |
+
className="btn-download-subtle"
|
| 2193 |
+
onClick={() => onDownloadTableCsv(dados, 'secao3_dados_mercado')}
|
| 2194 |
+
disabled={loading || downloadingAssets || !dados}
|
| 2195 |
+
>
|
| 2196 |
+
Fazer download
|
| 2197 |
+
</button>
|
| 2198 |
+
</div>
|
| 2199 |
<DataTable table={dados} maxHeight={540} />
|
| 2200 |
</div>
|
| 2201 |
</div>
|
|
|
|
| 2236 |
>
|
| 2237 |
Aplicar período
|
| 2238 |
</button>
|
| 2239 |
+
{dataMercadoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2240 |
</div>
|
| 2241 |
{dataMercadoError ? <div className="error-line inline-error">{dataMercadoError}</div> : null}
|
| 2242 |
</div>
|
|
|
|
| 2245 |
<SectionBlock step="5" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
|
| 2246 |
<div className="row">
|
| 2247 |
<label>Variável Dependente (Y)</label>
|
| 2248 |
+
<select value={colunaYDraft} onChange={(e) => setColunaYDraft(e.target.value)}>
|
| 2249 |
<option value="">Selecione</option>
|
| 2250 |
{colunasNumericas.map((col) => (
|
| 2251 |
<option key={col} value={col}>{col}</option>
|
| 2252 |
))}
|
| 2253 |
</select>
|
| 2254 |
+
<button
|
| 2255 |
+
type="button"
|
| 2256 |
+
onClick={onAplicarVariavelDependente}
|
| 2257 |
+
disabled={loading || !dependentePendente}
|
| 2258 |
+
>
|
| 2259 |
+
Aplicar variável dependente
|
| 2260 |
+
</button>
|
| 2261 |
+
{dependentePendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2262 |
</div>
|
| 2263 |
</SectionBlock>
|
| 2264 |
|
| 2265 |
<SectionBlock step="6" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
|
| 2266 |
+
{!colunaY ? (
|
| 2267 |
+
<div className="section1-empty-hint">
|
| 2268 |
+
Aplique a variável dependente na etapa anterior para liberar as opções de variáveis independentes.
|
| 2269 |
+
</div>
|
| 2270 |
+
) : null}
|
| 2271 |
<div className="compact-option-group compact-option-group-x">
|
| 2272 |
<h4>Variáveis Independentes (X)</h4>
|
| 2273 |
<div className="checkbox-inline-wrap checkbox-inline-wrap-tools">
|
|
|
|
| 2336 |
</div>
|
| 2337 |
|
| 2338 |
<div className="row">
|
| 2339 |
+
<button onClick={onApplySelection} disabled={loading || dependentePendente || !colunaY || colunasX.length === 0}>Aplicar seleção</button>
|
| 2340 |
+
{selecaoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2341 |
</div>
|
| 2342 |
{selection?.aviso_multicolinearidade?.visible ? (
|
| 2343 |
<div dangerouslySetInnerHTML={{ __html: selection.aviso_multicolinearidade.html }} />
|
|
|
|
| 2352 |
{selection ? (
|
| 2353 |
<>
|
| 2354 |
<SectionBlock step="7" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
|
| 2355 |
+
<div className="download-actions-bar">
|
| 2356 |
+
<button
|
| 2357 |
+
type="button"
|
| 2358 |
+
className="btn-download-subtle"
|
| 2359 |
+
onClick={() => onDownloadTableCsv(selection.estatisticas, 'secao7_estatisticas')}
|
| 2360 |
+
disabled={loading || downloadingAssets || !selection.estatisticas}
|
| 2361 |
+
>
|
| 2362 |
+
Fazer download
|
| 2363 |
+
</button>
|
| 2364 |
+
</div>
|
| 2365 |
<DataTable table={selection.estatisticas} />
|
| 2366 |
</SectionBlock>
|
| 2367 |
|
|
|
|
| 2377 |
>
|
| 2378 |
<summary>Mostrar/Ocultar gráficos da seção</summary>
|
| 2379 |
{section8Open ? (
|
| 2380 |
+
<>
|
| 2381 |
+
<div className="download-actions-bar">
|
| 2382 |
+
{graficosSecao9.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
|
| 2383 |
+
{graficosSecao9.map((item, idx) => {
|
| 2384 |
+
const fileBase = `secao9_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
|
| 2385 |
+
return (
|
| 2386 |
+
<button
|
| 2387 |
+
key={`s9-dl-${item.id}`}
|
| 2388 |
+
type="button"
|
| 2389 |
+
className="btn-download-subtle"
|
| 2390 |
+
title={item.legenda}
|
| 2391 |
+
onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
|
| 2392 |
+
disabled={loading || downloadingAssets || !item.figure}
|
| 2393 |
+
>
|
| 2394 |
+
{graficosSecao9.length > 1 ? item.label : 'Fazer download'}
|
| 2395 |
+
</button>
|
| 2396 |
+
)
|
| 2397 |
+
})}
|
| 2398 |
+
{graficosSecao9.length > 1 ? (
|
| 2399 |
+
<button
|
| 2400 |
+
type="button"
|
| 2401 |
+
className="btn-download-subtle"
|
| 2402 |
+
onClick={() => onDownloadFiguresPngBatch(
|
| 2403 |
+
graficosSecao9.map((item, idx) => ({
|
| 2404 |
+
figure: item.figure,
|
| 2405 |
+
fileNameBase: `secao9_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
|
| 2406 |
+
forceHideLegend: true,
|
| 2407 |
+
})),
|
| 2408 |
+
)}
|
| 2409 |
+
disabled={loading || downloadingAssets || graficosSecao9.length === 0}
|
| 2410 |
+
>
|
| 2411 |
+
Todos
|
| 2412 |
+
</button>
|
| 2413 |
+
) : null}
|
| 2414 |
+
</div>
|
| 2415 |
+
{graficosSecao9.length > 0 ? (
|
| 2416 |
+
<div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao9 }}>
|
| 2417 |
+
{graficosSecao9.map((item) => (
|
| 2418 |
+
<PlotFigure
|
| 2419 |
+
key={`s9-plot-${item.id}`}
|
| 2420 |
+
figure={item.figure}
|
| 2421 |
+
title={item.title}
|
| 2422 |
+
subtitle={item.subtitle}
|
| 2423 |
+
forceHideLegend
|
| 2424 |
+
className="plot-stretch"
|
| 2425 |
+
lazy
|
| 2426 |
+
/>
|
| 2427 |
+
))}
|
| 2428 |
+
</div>
|
| 2429 |
+
) : (
|
| 2430 |
+
<div className="empty-box">Grafico indisponivel.</div>
|
| 2431 |
+
)}
|
| 2432 |
+
</>
|
| 2433 |
) : null}
|
| 2434 |
</details>
|
| 2435 |
</SectionBlock>
|
|
|
|
| 2449 |
))}
|
| 2450 |
</select>
|
| 2451 |
<button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
|
| 2452 |
+
{buscaTransformPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2453 |
</div>
|
| 2454 |
{(selection.busca?.resultados || []).length > 0 ? (
|
| 2455 |
<div className="transform-suggestions-grid">
|
|
|
|
| 2531 |
))}
|
| 2532 |
</div>
|
| 2533 |
|
| 2534 |
+
<div className="row row-fit-transformacoes">
|
| 2535 |
+
<button className="btn-fit-model" onClick={onFitModel} disabled={loading}>Aplicar transformações e ajustar modelo</button>
|
| 2536 |
+
{manualTransformPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2537 |
+
</div>
|
| 2538 |
</>
|
| 2539 |
) : null}
|
| 2540 |
{transformacoesAplicadas?.coluna_y ? (
|
|
|
|
| 2596 |
<option value="Variáveis Independentes Não Transformadas X Resíduo Padronizado">X não transformado x Resíduo</option>
|
| 2597 |
</select>
|
| 2598 |
</div>
|
| 2599 |
+
<div className="download-actions-bar">
|
| 2600 |
+
{graficosSecao12.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
|
| 2601 |
+
{graficosSecao12.map((item, idx) => {
|
| 2602 |
+
const fileBase = `secao12_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
|
| 2603 |
+
return (
|
| 2604 |
+
<button
|
| 2605 |
+
key={`s12-dl-${item.id}`}
|
| 2606 |
+
type="button"
|
| 2607 |
+
className="btn-download-subtle"
|
| 2608 |
+
title={item.legenda}
|
| 2609 |
+
onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
|
| 2610 |
+
disabled={loading || downloadingAssets || !item.figure}
|
| 2611 |
+
>
|
| 2612 |
+
{graficosSecao12.length > 1 ? item.label : 'Fazer download'}
|
| 2613 |
+
</button>
|
| 2614 |
+
)
|
| 2615 |
+
})}
|
| 2616 |
+
{graficosSecao12.length > 1 ? (
|
| 2617 |
+
<button
|
| 2618 |
+
type="button"
|
| 2619 |
+
className="btn-download-subtle"
|
| 2620 |
+
onClick={() => onDownloadFiguresPngBatch(
|
| 2621 |
+
graficosSecao12.map((item, idx) => ({
|
| 2622 |
+
figure: item.figure,
|
| 2623 |
+
fileNameBase: `secao12_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
|
| 2624 |
+
forceHideLegend: true,
|
| 2625 |
+
})),
|
| 2626 |
+
)}
|
| 2627 |
+
disabled={loading || downloadingAssets || graficosSecao12.length === 0}
|
| 2628 |
+
>
|
| 2629 |
+
Todos
|
| 2630 |
+
</button>
|
| 2631 |
+
) : null}
|
| 2632 |
+
</div>
|
| 2633 |
+
{graficosSecao12.length > 0 ? (
|
| 2634 |
+
<div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao12 }}>
|
| 2635 |
+
{graficosSecao12.map((item) => (
|
| 2636 |
+
<PlotFigure
|
| 2637 |
+
key={`s12-plot-${item.id}`}
|
| 2638 |
+
figure={item.figure}
|
| 2639 |
+
title={item.title}
|
| 2640 |
+
subtitle={item.subtitle}
|
| 2641 |
+
forceHideLegend
|
| 2642 |
+
className="plot-stretch"
|
| 2643 |
+
lazy
|
| 2644 |
+
/>
|
| 2645 |
+
))}
|
| 2646 |
+
</div>
|
| 2647 |
+
) : (
|
| 2648 |
+
<div className="empty-box">Grafico indisponivel.</div>
|
| 2649 |
+
)}
|
| 2650 |
</>
|
| 2651 |
) : null}
|
| 2652 |
</details>
|
|
|
|
| 2654 |
|
| 2655 |
<SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 2656 |
<div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
|
| 2657 |
+
<div className="download-actions-bar">
|
| 2658 |
+
<span className="download-actions-label">Fazer download:</span>
|
| 2659 |
+
<button
|
| 2660 |
+
type="button"
|
| 2661 |
+
className="btn-download-subtle"
|
| 2662 |
+
onClick={() => onDownloadTableCsv(fit.tabela_coef, 'secao13_coeficientes')}
|
| 2663 |
+
disabled={loading || downloadingAssets || !fit.tabela_coef}
|
| 2664 |
+
>
|
| 2665 |
+
Coeficientes
|
| 2666 |
+
</button>
|
| 2667 |
+
<button
|
| 2668 |
+
type="button"
|
| 2669 |
+
className="btn-download-subtle"
|
| 2670 |
+
onClick={() => onDownloadTableCsv(fit.tabela_obs_calc, 'secao13_obs_calc')}
|
| 2671 |
+
disabled={loading || downloadingAssets || !fit.tabela_obs_calc}
|
| 2672 |
+
>
|
| 2673 |
+
Obs x Calc
|
| 2674 |
+
</button>
|
| 2675 |
+
<button
|
| 2676 |
+
type="button"
|
| 2677 |
+
className="btn-download-subtle"
|
| 2678 |
+
onClick={() => onDownloadTablesCsvBatch([
|
| 2679 |
+
{ table: fit.tabela_coef, fileNameBase: 'secao13_coeficientes' },
|
| 2680 |
+
{ table: fit.tabela_obs_calc, fileNameBase: 'secao13_obs_calc' },
|
| 2681 |
+
])}
|
| 2682 |
+
disabled={loading || downloadingAssets || (!fit.tabela_coef && !fit.tabela_obs_calc)}
|
| 2683 |
+
>
|
| 2684 |
+
Todos
|
| 2685 |
+
</button>
|
| 2686 |
+
</div>
|
| 2687 |
<div className="two-col diagnostic-tables">
|
| 2688 |
<div className="pane">
|
| 2689 |
<h4>Tabela de Coeficientes</h4>
|
|
|
|
| 2697 |
</SectionBlock>
|
| 2698 |
|
| 2699 |
<SectionBlock step="14" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
|
| 2700 |
+
<div className="download-actions-bar">
|
| 2701 |
+
<span className="download-actions-label">Fazer download:</span>
|
| 2702 |
+
<button
|
| 2703 |
+
type="button"
|
| 2704 |
+
className="btn-download-subtle"
|
| 2705 |
+
onClick={() => onDownloadFigurePng(fit.grafico_obs_calc, 'secao14_obs_calc')}
|
| 2706 |
+
disabled={loading || downloadingAssets || !fit.grafico_obs_calc}
|
| 2707 |
+
>
|
| 2708 |
+
Obs x calc
|
| 2709 |
+
</button>
|
| 2710 |
+
<button
|
| 2711 |
+
type="button"
|
| 2712 |
+
className="btn-download-subtle"
|
| 2713 |
+
onClick={() => onDownloadFigurePng(fit.grafico_residuos, 'secao14_residuos')}
|
| 2714 |
+
disabled={loading || downloadingAssets || !fit.grafico_residuos}
|
| 2715 |
+
>
|
| 2716 |
+
Resíduos
|
| 2717 |
+
</button>
|
| 2718 |
+
<button
|
| 2719 |
+
type="button"
|
| 2720 |
+
className="btn-download-subtle"
|
| 2721 |
+
onClick={() => onDownloadFigurePng(fit.grafico_histograma, 'secao14_histograma')}
|
| 2722 |
+
disabled={loading || downloadingAssets || !fit.grafico_histograma}
|
| 2723 |
+
>
|
| 2724 |
+
Histograma
|
| 2725 |
+
</button>
|
| 2726 |
+
<button
|
| 2727 |
+
type="button"
|
| 2728 |
+
className="btn-download-subtle"
|
| 2729 |
+
onClick={() => onDownloadFigurePng(fit.grafico_cook, 'secao14_cook', { forceHideLegend: true })}
|
| 2730 |
+
disabled={loading || downloadingAssets || !fit.grafico_cook}
|
| 2731 |
+
>
|
| 2732 |
+
Cook
|
| 2733 |
+
</button>
|
| 2734 |
+
<button
|
| 2735 |
+
type="button"
|
| 2736 |
+
className="btn-download-subtle"
|
| 2737 |
+
onClick={() => onDownloadFigurePng(fit.grafico_correlacao, 'secao14_correlacao')}
|
| 2738 |
+
disabled={loading || downloadingAssets || !fit.grafico_correlacao}
|
| 2739 |
+
>
|
| 2740 |
+
Correlação
|
| 2741 |
+
</button>
|
| 2742 |
+
<button
|
| 2743 |
+
type="button"
|
| 2744 |
+
className="btn-download-subtle"
|
| 2745 |
+
onClick={() => onDownloadFiguresPngBatch([
|
| 2746 |
+
{ figure: fit.grafico_obs_calc, fileNameBase: 'secao14_obs_calc' },
|
| 2747 |
+
{ figure: fit.grafico_residuos, fileNameBase: 'secao14_residuos' },
|
| 2748 |
+
{ figure: fit.grafico_histograma, fileNameBase: 'secao14_histograma' },
|
| 2749 |
+
{ figure: fit.grafico_cook, fileNameBase: 'secao14_cook', forceHideLegend: true },
|
| 2750 |
+
{ figure: fit.grafico_correlacao, fileNameBase: 'secao14_correlacao' },
|
| 2751 |
+
])}
|
| 2752 |
+
disabled={loading || downloadingAssets || (!fit.grafico_obs_calc && !fit.grafico_residuos && !fit.grafico_histograma && !fit.grafico_cook && !fit.grafico_correlacao)}
|
| 2753 |
+
>
|
| 2754 |
+
Todos
|
| 2755 |
+
</button>
|
| 2756 |
+
</div>
|
| 2757 |
<div className="plot-grid-2-fixed">
|
| 2758 |
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 2759 |
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
|
|
|
| 2766 |
</SectionBlock>
|
| 2767 |
|
| 2768 |
<SectionBlock step="15" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
|
| 2769 |
+
<div className="download-actions-bar">
|
| 2770 |
+
<button
|
| 2771 |
+
type="button"
|
| 2772 |
+
className="btn-download-subtle"
|
| 2773 |
+
onClick={() => onDownloadTableCsv(fit.tabela_metricas, 'secao15_tabela_metricas')}
|
| 2774 |
+
disabled={loading || downloadingAssets || !fit.tabela_metricas}
|
| 2775 |
+
>
|
| 2776 |
+
Fazer download
|
| 2777 |
+
</button>
|
| 2778 |
+
</div>
|
| 2779 |
<DataTable table={fit.tabela_metricas} maxHeight={320} />
|
| 2780 |
</SectionBlock>
|
| 2781 |
|
|
|
|
| 2837 |
<div className="outlier-actions-row">
|
| 2838 |
<button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
|
| 2839 |
<button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
|
| 2840 |
+
{outlierFiltrosPendentes ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2841 |
</div>
|
| 2842 |
</div>
|
| 2843 |
|
|
|
|
| 2858 |
<button onClick={onRestartIteration} disabled={loading} className="btn-reiniciar-iteracao">
|
| 2859 |
Atualizar Modelo (Excluir/Reincluir Outliers)
|
| 2860 |
</button>
|
| 2861 |
+
{outlierTextosPendentes ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2862 |
</div>
|
| 2863 |
</div>
|
| 2864 |
<div className="resumo-outliers-box">Iteração: {iteracao} | {resumoOutliers}</div>
|
|
|
|
| 2875 |
defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
|
| 2876 |
onChange={(e) => {
|
| 2877 |
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
|
| 2878 |
+
setAvaliacaoPendente(true)
|
| 2879 |
}}
|
| 2880 |
>
|
| 2881 |
<option value="">Selecione</option>
|
|
|
|
| 2892 |
placeholder={campo.placeholder || ''}
|
| 2893 |
onChange={(e) => {
|
| 2894 |
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
|
| 2895 |
+
setAvaliacaoPendente(true)
|
| 2896 |
}}
|
| 2897 |
/>
|
| 2898 |
)}
|
|
|
|
| 2903 |
<button onClick={onCalculateAvaliacao} disabled={loading}>Calcular</button>
|
| 2904 |
<button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
|
| 2905 |
<button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações (Excel)</button>
|
| 2906 |
+
{avaliacaoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
|
| 2907 |
</div>
|
| 2908 |
<div className="row avaliacao-base-row">
|
| 2909 |
<label>Base comparação</label>
|
frontend/src/components/PlotFigure.jsx
CHANGED
|
@@ -1,8 +1,37 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
import Plot from 'react-plotly.js'
|
| 3 |
import Plotly from 'plotly.js-dist-min'
|
| 4 |
|
| 5 |
-
function PlotFigure({ figure, title, forceHideLegend = false, className = '' }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
if (!figure) {
|
| 7 |
return <div className="empty-box">Grafico indisponivel.</div>
|
| 8 |
}
|
|
@@ -33,16 +62,25 @@ function PlotFigure({ figure, title, forceHideLegend = false, className = '' })
|
|
| 33 |
const cardClassName = `plot-card ${className}`.trim()
|
| 34 |
|
| 35 |
return (
|
| 36 |
-
<div className={cardClassName}>
|
| 37 |
-
{title
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
</div>
|
| 47 |
)
|
| 48 |
}
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import Plot from 'react-plotly.js'
|
| 3 |
import Plotly from 'plotly.js-dist-min'
|
| 4 |
|
| 5 |
+
function PlotFigure({ figure, title, subtitle = '', forceHideLegend = false, className = '', lazy = false }) {
|
| 6 |
+
const containerRef = useRef(null)
|
| 7 |
+
const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy)
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
if (!lazy) {
|
| 11 |
+
setShouldRenderPlot(true)
|
| 12 |
+
return undefined
|
| 13 |
+
}
|
| 14 |
+
if (shouldRenderPlot) return undefined
|
| 15 |
+
if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {
|
| 16 |
+
setShouldRenderPlot(true)
|
| 17 |
+
return undefined
|
| 18 |
+
}
|
| 19 |
+
const target = containerRef.current
|
| 20 |
+
if (!target) return undefined
|
| 21 |
+
const observer = new IntersectionObserver(
|
| 22 |
+
(entries) => {
|
| 23 |
+
const isVisible = entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0)
|
| 24 |
+
if (isVisible) {
|
| 25 |
+
setShouldRenderPlot(true)
|
| 26 |
+
observer.disconnect()
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
{ rootMargin: '240px 0px', threshold: 0.01 },
|
| 30 |
+
)
|
| 31 |
+
observer.observe(target)
|
| 32 |
+
return () => observer.disconnect()
|
| 33 |
+
}, [lazy, shouldRenderPlot])
|
| 34 |
+
|
| 35 |
if (!figure) {
|
| 36 |
return <div className="empty-box">Grafico indisponivel.</div>
|
| 37 |
}
|
|
|
|
| 62 |
const cardClassName = `plot-card ${className}`.trim()
|
| 63 |
|
| 64 |
return (
|
| 65 |
+
<div ref={containerRef} className={cardClassName}>
|
| 66 |
+
{title || subtitle ? (
|
| 67 |
+
<div className="plot-card-head">
|
| 68 |
+
{title ? <h4 className="plot-card-title">{title}</h4> : null}
|
| 69 |
+
{subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null}
|
| 70 |
+
</div>
|
| 71 |
+
) : null}
|
| 72 |
+
{shouldRenderPlot ? (
|
| 73 |
+
<Plot
|
| 74 |
+
data={data}
|
| 75 |
+
layout={layout}
|
| 76 |
+
config={{ responsive: true, displaylogo: false }}
|
| 77 |
+
style={{ width: '100%', height: plotHeight, minHeight: '320px' }}
|
| 78 |
+
useResizeHandler
|
| 79 |
+
plotly={Plotly}
|
| 80 |
+
/>
|
| 81 |
+
) : (
|
| 82 |
+
<div className="plot-lazy-placeholder">Carregando gráfico...</div>
|
| 83 |
+
)}
|
| 84 |
</div>
|
| 85 |
)
|
| 86 |
}
|
frontend/src/components/VisualizacaoTab.jsx
CHANGED
|
@@ -8,7 +8,8 @@ import SectionBlock from './SectionBlock'
|
|
| 8 |
|
| 9 |
const INNER_TABS = [
|
| 10 |
{ key: 'mapa', label: 'Mapa' },
|
| 11 |
-
{ key: '
|
|
|
|
| 12 |
{ key: 'transformacoes', label: 'Transformações' },
|
| 13 |
{ key: 'resumo', label: 'Resumo' },
|
| 14 |
{ key: 'coeficientes', label: 'Coeficientes' },
|
|
@@ -339,22 +340,18 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 339 |
</>
|
| 340 |
) : null}
|
| 341 |
|
| 342 |
-
{activeInnerTab === '
|
| 343 |
-
<
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
<div className="pane">
|
| 349 |
-
<h4>Estatísticas</h4>
|
| 350 |
-
<DataTable table={estatisticas} maxHeight={520} />
|
| 351 |
-
</div>
|
| 352 |
-
</div>
|
| 353 |
) : null}
|
| 354 |
|
| 355 |
{activeInnerTab === 'transformacoes' ? (
|
| 356 |
<>
|
| 357 |
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
|
|
|
|
| 358 |
<DataTable table={dadosTransformados} />
|
| 359 |
</>
|
| 360 |
) : null}
|
|
|
|
| 8 |
|
| 9 |
const INNER_TABS = [
|
| 10 |
{ key: 'mapa', label: 'Mapa' },
|
| 11 |
+
{ key: 'dados_mercado', label: 'Dados de Mercado' },
|
| 12 |
+
{ key: 'metricas', label: 'Métricas' },
|
| 13 |
{ key: 'transformacoes', label: 'Transformações' },
|
| 14 |
{ key: 'resumo', label: 'Resumo' },
|
| 15 |
{ key: 'coeficientes', label: 'Coeficientes' },
|
|
|
|
| 340 |
</>
|
| 341 |
) : null}
|
| 342 |
|
| 343 |
+
{activeInnerTab === 'dados_mercado' ? (
|
| 344 |
+
<DataTable table={dados} maxHeight={620} />
|
| 345 |
+
) : null}
|
| 346 |
+
|
| 347 |
+
{activeInnerTab === 'metricas' ? (
|
| 348 |
+
<DataTable table={estatisticas} maxHeight={620} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
) : null}
|
| 350 |
|
| 351 |
{activeInnerTab === 'transformacoes' ? (
|
| 352 |
<>
|
| 353 |
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
|
| 354 |
+
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 355 |
<DataTable table={dadosTransformados} />
|
| 356 |
</>
|
| 357 |
) : null}
|
frontend/src/styles.css
CHANGED
|
@@ -214,6 +214,13 @@ textarea {
|
|
| 214 |
padding: 14px;
|
| 215 |
}
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
.tab-content {
|
| 218 |
display: flex;
|
| 219 |
flex-direction: column;
|
|
@@ -1972,6 +1979,12 @@ button.btn-upload-select {
|
|
| 1972 |
gap: 12px;
|
| 1973 |
}
|
| 1974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1975 |
.plot-card {
|
| 1976 |
border: 1px solid #dbe5ef;
|
| 1977 |
border-radius: 12px;
|
|
@@ -1980,15 +1993,44 @@ button.btn-upload-select {
|
|
| 1980 |
padding: 8px;
|
| 1981 |
}
|
| 1982 |
|
| 1983 |
-
.plot-card
|
| 1984 |
margin: 4px 4px 8px;
|
| 1985 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1986 |
font-family: 'Sora', sans-serif;
|
| 1987 |
-
font-size: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1988 |
}
|
| 1989 |
|
| 1990 |
.plot-card.plot-stretch {
|
| 1991 |
-
min-height:
|
| 1992 |
}
|
| 1993 |
|
| 1994 |
.plot-full-width {
|
|
@@ -2259,6 +2301,15 @@ button.btn-upload-select {
|
|
| 2259 |
margin-bottom: 16px;
|
| 2260 |
}
|
| 2261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2262 |
.transformacao-origem-info {
|
| 2263 |
margin-top: 6px;
|
| 2264 |
margin-bottom: 4px;
|
|
@@ -2290,6 +2341,48 @@ button.btn-upload-select {
|
|
| 2290 |
align-items: center;
|
| 2291 |
}
|
| 2292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2293 |
.geo-correcoes {
|
| 2294 |
display: grid;
|
| 2295 |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
@@ -2763,12 +2856,16 @@ button.btn-upload-select {
|
|
| 2763 |
grid-template-columns: 1fr;
|
| 2764 |
}
|
| 2765 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2766 |
.plot-card {
|
| 2767 |
min-height: 340px;
|
| 2768 |
}
|
| 2769 |
|
| 2770 |
.plot-card.plot-stretch {
|
| 2771 |
-
min-height:
|
| 2772 |
}
|
| 2773 |
|
| 2774 |
.plot-card.plot-correlation-card {
|
|
|
|
| 214 |
padding: 14px;
|
| 215 |
}
|
| 216 |
|
| 217 |
+
.visualizacao-table-title {
|
| 218 |
+
margin: 10px 0 8px;
|
| 219 |
+
color: #3a4f64;
|
| 220 |
+
font-family: 'Sora', sans-serif;
|
| 221 |
+
font-size: 0.88rem;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
.tab-content {
|
| 225 |
display: flex;
|
| 226 |
flex-direction: column;
|
|
|
|
| 1979 |
gap: 12px;
|
| 1980 |
}
|
| 1981 |
|
| 1982 |
+
.plot-grid-scatter {
|
| 1983 |
+
display: grid;
|
| 1984 |
+
grid-template-columns: repeat(var(--plot-cols, 3), minmax(0, 1fr));
|
| 1985 |
+
gap: 12px;
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
.plot-card {
|
| 1989 |
border: 1px solid #dbe5ef;
|
| 1990 |
border-radius: 12px;
|
|
|
|
| 1993 |
padding: 8px;
|
| 1994 |
}
|
| 1995 |
|
| 1996 |
+
.plot-card-head {
|
| 1997 |
margin: 4px 4px 8px;
|
| 1998 |
+
min-height: 42px;
|
| 1999 |
+
display: grid;
|
| 2000 |
+
align-content: start;
|
| 2001 |
+
gap: 2px;
|
| 2002 |
+
}
|
| 2003 |
+
|
| 2004 |
+
.plot-card-title {
|
| 2005 |
+
margin: 0;
|
| 2006 |
+
color: #2f465c;
|
| 2007 |
font-family: 'Sora', sans-serif;
|
| 2008 |
+
font-size: 0.93rem;
|
| 2009 |
+
font-weight: 700;
|
| 2010 |
+
line-height: 1.2;
|
| 2011 |
+
}
|
| 2012 |
+
|
| 2013 |
+
.plot-card-subtitle {
|
| 2014 |
+
color: #5f7387;
|
| 2015 |
+
font-size: 0.82rem;
|
| 2016 |
+
font-family: 'JetBrains Mono', monospace;
|
| 2017 |
+
line-height: 1.15;
|
| 2018 |
+
}
|
| 2019 |
+
|
| 2020 |
+
.plot-lazy-placeholder {
|
| 2021 |
+
min-height: 320px;
|
| 2022 |
+
display: flex;
|
| 2023 |
+
align-items: center;
|
| 2024 |
+
justify-content: center;
|
| 2025 |
+
color: #6a7f94;
|
| 2026 |
+
font-size: 0.86rem;
|
| 2027 |
+
border: 1px dashed #dbe5ef;
|
| 2028 |
+
border-radius: 10px;
|
| 2029 |
+
background: linear-gradient(180deg, #fbfdff 0%, #f5f9fd 100%);
|
| 2030 |
}
|
| 2031 |
|
| 2032 |
.plot-card.plot-stretch {
|
| 2033 |
+
min-height: 380px;
|
| 2034 |
}
|
| 2035 |
|
| 2036 |
.plot-full-width {
|
|
|
|
| 2301 |
margin-bottom: 16px;
|
| 2302 |
}
|
| 2303 |
|
| 2304 |
+
.row-fit-transformacoes {
|
| 2305 |
+
margin-top: 14px;
|
| 2306 |
+
margin-bottom: 16px;
|
| 2307 |
+
}
|
| 2308 |
+
|
| 2309 |
+
.row-fit-transformacoes .btn-fit-model {
|
| 2310 |
+
margin: 0;
|
| 2311 |
+
}
|
| 2312 |
+
|
| 2313 |
.transformacao-origem-info {
|
| 2314 |
margin-top: 6px;
|
| 2315 |
margin-bottom: 4px;
|
|
|
|
| 2341 |
align-items: center;
|
| 2342 |
}
|
| 2343 |
|
| 2344 |
+
.pending-apply-note {
|
| 2345 |
+
display: inline-flex;
|
| 2346 |
+
align-items: center;
|
| 2347 |
+
min-height: 30px;
|
| 2348 |
+
padding: 4px 9px;
|
| 2349 |
+
border: 1px solid #f2cb8f;
|
| 2350 |
+
border-radius: 999px;
|
| 2351 |
+
background: #fff6e8;
|
| 2352 |
+
color: #8a5a15;
|
| 2353 |
+
font-size: 0.76rem;
|
| 2354 |
+
font-weight: 700;
|
| 2355 |
+
line-height: 1.25;
|
| 2356 |
+
}
|
| 2357 |
+
|
| 2358 |
+
.download-actions-bar {
|
| 2359 |
+
display: flex;
|
| 2360 |
+
flex-wrap: wrap;
|
| 2361 |
+
gap: 7px;
|
| 2362 |
+
margin: 8px 0 10px;
|
| 2363 |
+
}
|
| 2364 |
+
|
| 2365 |
+
.download-actions-label {
|
| 2366 |
+
display: inline-flex;
|
| 2367 |
+
align-items: center;
|
| 2368 |
+
font-size: 0.76rem;
|
| 2369 |
+
font-weight: 700;
|
| 2370 |
+
color: #5a7086;
|
| 2371 |
+
padding-right: 2px;
|
| 2372 |
+
}
|
| 2373 |
+
|
| 2374 |
+
button.btn-download-subtle {
|
| 2375 |
+
--btn-bg-start: #f5f8fb;
|
| 2376 |
+
--btn-bg-end: #edf2f7;
|
| 2377 |
+
--btn-border: #c9d7e4;
|
| 2378 |
+
--btn-shadow-soft: rgba(53, 74, 95, 0.08);
|
| 2379 |
+
--btn-shadow-strong: rgba(53, 74, 95, 0.12);
|
| 2380 |
+
color: #3f566d;
|
| 2381 |
+
font-size: 0.76rem;
|
| 2382 |
+
padding: 5px 10px;
|
| 2383 |
+
border-radius: 9px;
|
| 2384 |
+
}
|
| 2385 |
+
|
| 2386 |
.geo-correcoes {
|
| 2387 |
display: grid;
|
| 2388 |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
|
| 2856 |
grid-template-columns: 1fr;
|
| 2857 |
}
|
| 2858 |
|
| 2859 |
+
.plot-grid-scatter {
|
| 2860 |
+
grid-template-columns: 1fr;
|
| 2861 |
+
}
|
| 2862 |
+
|
| 2863 |
.plot-card {
|
| 2864 |
min-height: 340px;
|
| 2865 |
}
|
| 2866 |
|
| 2867 |
.plot-card.plot-stretch {
|
| 2868 |
+
min-height: 330px;
|
| 2869 |
}
|
| 2870 |
|
| 2871 |
.plot-card.plot-correlation-card {
|