Guilherme Silberfarb Costa commited on
Commit
d953b73
·
1 Parent(s): 1aafde3

Refine elaboracao UI layout

Browse files
backend/app/core/elaboracao/formatadores.py CHANGED
@@ -221,7 +221,26 @@ def _renderizar_secao_micro(titulo, resultados_dict, secao_tipo="padrao"):
221
  html = f'<div class="section-title-orange micro-group-title">{titulo}</div>'
222
  html += f'<div class="{grid_class}">'
223
 
224
- for coluna, info in resultados_dict.items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  status = "✅" if info["valido"] else "⚠️"
226
  status_class = "micro-ok" if info["valido"] else "micro-warn"
227
  categorias_lista = info.get("categorias") or []
@@ -230,7 +249,7 @@ def _renderizar_secao_micro(titulo, resultados_dict, secao_tipo="padrao"):
230
  f'<div class="micro-msg {"micro-msg-ok" if item.get("valido") else "micro-msg-fail"}">'
231
  f'{escape(str(item.get("mensagem", "")))}'
232
  f'</div>'
233
- for item in categorias_lista
234
  )
235
  else:
236
  mensagens_lista = info["mensagens"]
@@ -313,14 +332,11 @@ def formatar_outliers_anteriores_html(n_outliers, lista_indices):
313
  """Formata informações de outliers excluídos como HTML card."""
314
  lista_str = lista_indices if lista_indices else "Nenhum"
315
  return f'''
316
- <div style="display: flex; gap: 16px; align-items: baseline;
317
- padding: 10px 16px; background: var(--background-fill-secondary, #f8f9fa);
318
- border-radius: 8px; border: 1px solid var(--border-color-primary, #e2e8f0);
319
- flex-wrap: wrap;">
320
- <span style="font-weight: 600; color: var(--body-text-color, #495057); white-space: nowrap;">
321
  {n_outliers} outlier(s) excluídos do modelo ajustado
322
  </span>
323
- <span style="color: var(--body-text-color-subdued, #6c757d); font-size: 0.92em;">
324
  Índices: {lista_str}
325
  </span>
326
  </div>'''
 
221
  html = f'<div class="section-title-orange micro-group-title">{titulo}</div>'
222
  html += f'<div class="{grid_class}">'
223
 
224
+ def ordenar_categoria(item):
225
+ return (
226
+ 1 if item.get("valido") else 0,
227
+ int(item.get("ni", 0) or 0),
228
+ str(item.get("categoria", "")),
229
+ )
230
+
231
+ def ordenar_variavel(item):
232
+ coluna, info = item
233
+ categorias = info.get("categorias") or []
234
+ falhas = [cat for cat in categorias if not cat.get("valido")]
235
+ todos_ni = [int(cat.get("ni", 0) or 0) for cat in categorias]
236
+ return (
237
+ 1 if info.get("valido") else 0,
238
+ -len(falhas),
239
+ min(todos_ni) if todos_ni else 0,
240
+ str(coluna),
241
+ )
242
+
243
+ for coluna, info in sorted(resultados_dict.items(), key=ordenar_variavel):
244
  status = "✅" if info["valido"] else "⚠️"
245
  status_class = "micro-ok" if info["valido"] else "micro-warn"
246
  categorias_lista = info.get("categorias") or []
 
249
  f'<div class="micro-msg {"micro-msg-ok" if item.get("valido") else "micro-msg-fail"}">'
250
  f'{escape(str(item.get("mensagem", "")))}'
251
  f'</div>'
252
+ for item in sorted(categorias_lista, key=ordenar_categoria)
253
  )
254
  else:
255
  mensagens_lista = info["mensagens"]
 
332
  """Formata informações de outliers excluídos como HTML card."""
333
  lista_str = lista_indices if lista_indices else "Nenhum"
334
  return f'''
335
+ <div class="outliers-previous-card">
336
+ <span class="outliers-previous-count">
 
 
 
337
  {n_outliers} outlier(s) excluídos do modelo ajustado
338
  </span>
339
+ <span class="outliers-previous-indices">
340
  Índices: {lista_str}
341
  </span>
342
  </div>'''
backend/app/services/elaboracao_service.py CHANGED
@@ -1591,6 +1591,7 @@ def fit_model(
1591
  tabela_outliers_excluidos = _montar_tabela_outliers_excluidos(session)
1592
 
1593
  return {
 
1594
  "diagnosticos_html": diagnosticos_html,
1595
  "transformacao_y": session.transformacao_y,
1596
  "transformacoes_x": sanitize_value(session.transformacoes_x),
 
1591
  tabela_outliers_excluidos = _montar_tabela_outliers_excluidos(session)
1592
 
1593
  return {
1594
+ "diagnosticos": sanitize_value(resultado["diagnosticos"]),
1595
  "diagnosticos_html": diagnosticos_html,
1596
  "transformacao_y": session.transformacao_y,
1597
  "transformacoes_x": sanitize_value(session.transformacoes_x),
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -234,6 +234,19 @@ function formatCurrencyBr(value) {
234
  return num.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  function formatarValorTabelaKnn(coluna, valor) {
238
  if (valor === null || valor === undefined || valor === '') return '-'
239
  const nome = String(coluna || '').toLowerCase()
@@ -690,6 +703,124 @@ function buildScatterPanelFigureMap(panels) {
690
  return mapa
691
  }
692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  function formatConselhoRegistro(elaborador) {
694
  if (!elaborador) return ''
695
  const conselho = String(elaborador.conselho || '').trim()
@@ -945,6 +1076,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
945
  const [section11LocksOpen, setSection11LocksOpen] = useState(false)
946
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
947
  const [section6EditOpen, setSection6EditOpen] = useState(true)
 
948
 
949
  const [fit, setFit] = useState(null)
950
  const [dispersaoEixoX, setDispersaoEixoX] = useState('transformado')
@@ -1012,6 +1144,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1012
  const [sectionsMountKey, setSectionsMountKey] = useState(0)
1013
  const [renderedSectionSteps, setRenderedSectionSteps] = useState(() => ['1'])
1014
  const [visibleSectionSteps, setVisibleSectionSteps] = useState(() => ['1'])
 
1015
  const visibleSectionStepsRef = useRef(new Set(['1']))
1016
  const elaboracaoShareHref = repoModeloSelecionado
1017
  ? buildElaboracaoModeloLink(repoModeloSelecionado)
@@ -1279,6 +1412,68 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1279
 
1280
  return faixas
1281
  }, [fit?.tabela_metricas?.rows, fit?.variaveis_filtro])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1282
  const resumoResiduoPadStats = useMemo(() => {
1283
  const rows = fit?.tabela_metricas?.rows
1284
  if (!Array.isArray(rows) || rows.length === 0) return null
@@ -1416,10 +1611,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1416
  if (secao10InterativoSelecionado === 'none' || graficosSecao9Interativo.length === 0) return null
1417
  return graficosSecao9Interativo.find((item) => String(item.label || '') === secao10InterativoSelecionado) || graficosSecao9Interativo[0]
1418
  }, [graficosSecao9Interativo, secao10InterativoSelecionado])
1419
- const colunasGraficosSecao9 = useMemo(
1420
- () => Math.max(1, Math.min(3, graficosSecao9.length || 1)),
1421
- [graficosSecao9.length],
1422
- )
1423
  const colunasOriginaisDispersao = useMemo(() => {
1424
  const cols = Array.isArray(dados?.columns) ? dados.columns : []
1425
  return cols
@@ -1505,10 +1696,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1505
  if (secao13InterativoSelecionado === 'none' || graficosSecao12Interativo.length === 0) return null
1506
  return graficosSecao12Interativo.find((item) => String(item.label || '') === secao13InterativoSelecionado) || graficosSecao12Interativo[0]
1507
  }, [graficosSecao12Interativo, secao13InterativoSelecionado])
1508
- const colunasGraficosSecao12 = useMemo(
1509
- () => Math.max(1, Math.min(3, graficosSecao12.length || 1)),
1510
- [graficosSecao12.length],
1511
- )
1512
  const secao15DiagnosticoPng = useMemo(
1513
  () => String(fit?.graficos_diagnostico_modo || '') === 'png',
1514
  [fit?.graficos_diagnostico_modo],
@@ -1619,6 +1806,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1619
  if (typeof window === 'undefined' || typeof document === 'undefined') {
1620
  setRenderedSectionSteps(['1'])
1621
  setVisibleSectionSteps(['1'])
 
1622
  visibleSectionStepsRef.current = new Set(['1'])
1623
  return undefined
1624
  }
@@ -1632,6 +1820,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1632
 
1633
  if (sections.length === 0) {
1634
  setVisibleSectionSteps(['1'])
 
1635
  visibleSectionStepsRef.current = new Set(['1'])
1636
  return undefined
1637
  }
@@ -1675,8 +1864,10 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1675
  .map((section) => section.getAttribute('data-section-step')),
1676
  )
1677
  const normalizedVisible = nextVisible.length > 0 ? nextVisible : [getClosestStepToTop()]
 
1678
  visibleSectionStepsRef.current = new Set(normalizedVisible)
1679
  setVisibleSectionSteps((current) => (isSameSectionStepList(current, normalizedVisible) ? current : normalizedVisible))
 
1680
  }
1681
 
1682
  computeVisibleSteps()
@@ -3944,7 +4135,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
3944
 
3945
  return (
3946
  <div ref={elaboracaoRootRef} className="tab-content">
3947
- <div className={`elaboracao-layout${repoModeloDropdownOpen ? ' is-repo-model-open' : ''}`} style={sideNavDynamicStyle}>
 
 
 
 
3948
  <aside className="elaboracao-side-nav" aria-label="Navegação de seções da elaboração">
3949
  <ol className="elaboracao-side-nav-list">
3950
  {ELABORACAO_SECOES_NAV.map((secao) => {
@@ -4567,7 +4762,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4567
  : 'Há outliers excluídos: não'}
4568
  </div>
4569
  {outliersAnteriores.length > 0 ? (
4570
- <div className="resumo-outliers-box">Índices excluídos: {joinSelection(outliersAnteriores)}</div>
 
 
4571
  ) : null}
4572
  </div>
4573
  <div className="download-actions-bar">
@@ -4582,7 +4779,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4582
  Fazer download
4583
  </button>
4584
  </div>
4585
- <DataTable table={dados} maxHeight={540} />
4586
  </div>
4587
  </SectionBlock>
4588
 
@@ -4710,7 +4907,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4710
  {colunasX.length}/{colunasXDisponiveis.length} selecionadas
4711
  </span>
4712
  </div>
4713
- <div className="checkbox-inline-wrap">
4714
  {colunasXDisponiveis.map((col) => (
4715
  <label key={`x-${col}`} className="compact-checkbox">
4716
  <input
@@ -4771,7 +4968,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4771
 
4772
  <div className="compact-option-group compact-option-group-codigo">
4773
  <h4>Variáveis Categóricas Codificadas</h4>
4774
- <div className="checkbox-inline-wrap">
4775
  {colunasX.map((col) => (
4776
  <label key={`c-${col}`} className="compact-checkbox">
4777
  <input type="checkbox" checked={codigoAlocado.includes(col)} onChange={() => toggleSelection(setCodigoAlocado, col)} />
@@ -4848,7 +5045,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4848
  <div className="section6-summary-group">
4849
  <div className="section6-summary-label">Variáveis categóricas codificadas:</div>
4850
  {selecaoAplicada.codigo_alocado.length > 0 ? (
4851
- <div className="checkbox-inline-wrap">
4852
  {selecaoAplicada.codigo_alocado.map((coluna) => (
4853
  <span key={`selected-c-${coluna}`} className="compact-chip">{coluna}</span>
4854
  ))}
@@ -4952,24 +5149,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4952
  </div>
4953
  {secao10InterativoSelecionado !== 'none' ? (
4954
  <>
4955
- <div className="download-actions-bar">
4956
- <button
4957
- type="button"
4958
- className="btn-download-subtle"
4959
- title={secao10InterativoAtual?.legenda || ''}
4960
- onClick={() => {
4961
- if (!secao10InterativoAtual) return
4962
- void onDownloadFigurePng(
4963
- secao10InterativoAtual.figure,
4964
- `secao10_${sanitizeFileName(secao10InterativoAtual.label, 'dispersao_interativo')}`,
4965
- { forceHideLegend: true },
4966
- )
4967
- }}
4968
- disabled={loading || downloadingAssets || !secao10InterativoAtual?.figure}
4969
- >
4970
- Fazer download
4971
- </button>
4972
- </div>
4973
  {secao10InterativoAtual?.figure ? (
4974
  <PlotFigure
4975
  key={`s9-interativo-plot-${secao10InterativoAtual.id}`}
@@ -4982,6 +5161,23 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4982
  forceHideLegend
4983
  className="plot-stretch"
4984
  lazy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4985
  />
4986
  ) : (
4987
  <div className="empty-box">Grafico indisponivel.</div>
@@ -4991,57 +5187,24 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4991
  </>
4992
  ) : (
4993
  <>
4994
- <div className="download-actions-bar">
4995
- {graficosSecao9.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
4996
- {graficosSecao9.map((item, idx) => {
4997
- const fileBase = `secao10_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
4998
- return (
4999
- <button
5000
- key={`s9-dl-${item.id}`}
5001
- type="button"
5002
- className="btn-download-subtle"
5003
- title={item.legenda}
5004
- onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
5005
- disabled={loading || downloadingAssets || !item.figure}
5006
- >
5007
- {graficosSecao9.length > 1 ? item.label : 'Fazer download'}
5008
- </button>
5009
- )
5010
- })}
5011
- {graficosSecao9.length > 1 ? (
5012
- <button
5013
- type="button"
5014
- className="btn-download-subtle"
5015
- onClick={() => onDownloadFiguresPngBatch(
5016
- graficosSecao9.map((item, idx) => ({
5017
- figure: item.figure,
5018
- fileNameBase: `secao10_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
5019
- forceHideLegend: true,
5020
- })),
5021
- )}
5022
- disabled={loading || downloadingAssets || graficosSecao9.length === 0}
5023
- >
5024
- Todos
5025
- </button>
5026
- ) : null}
5027
- </div>
5028
  {graficosSecao9.length > 0 ? (
5029
- <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao9 }}>
5030
- {graficosSecao9.map((item) => (
5031
- <PlotFigure
5032
- key={`s9-plot-${item.id}`}
5033
- figure={item.figure}
5034
- indexedFigure={graficosSecao9ComIndicesMap.get(String(item.label || '').trim()) || null}
5035
- onRequestIndexedFigure={ensureSecao10GraficoComIndices}
5036
- title={item.title}
5037
- subtitle={item.subtitle}
5038
- showPointIndexToggle
5039
- forceHideLegend
5040
- className="plot-stretch"
5041
- lazy
5042
- />
5043
- ))}
5044
- </div>
 
5045
  ) : (
5046
  <div className="empty-box">Grafico indisponivel.</div>
5047
  )}
@@ -5121,41 +5284,44 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5121
  </div>
5122
  </div>
5123
  {(selection.busca?.resultados || []).length > 0 ? (
5124
- <div className="transform-suggestions-grid">
5125
- {(selection.busca?.resultados || []).map((item, idx) => (
5126
- <div key={`sug-${item.rank || idx + 1}`} className="transform-suggestion-card">
5127
- <div className="transform-suggestion-head">
5128
- <span className="transform-suggestion-rank">#{item.rank || idx + 1}</span>
5129
- <div className="transform-suggestion-metrics">
5130
- <span className="transform-suggestion-r2">R² = {formatMetric4(item.r2)}</span>
5131
- <span className="transform-suggestion-r2adj">R² ajustado = {formatMetric4(item.r2_ajustado)}</span>
5132
- <span className={grauBadgeClass(Number(item.grau_f ?? 0))}>
5133
- Teste F: {GRAU_LABEL_CURTO[Number(item.grau_f ?? 0)] || 'Sem enq.'}
5134
- </span>
 
 
 
5135
  </div>
5136
- </div>
5137
- <div className="transform-suggestion-list">
5138
- <div className="transform-suggestion-item transform-suggestion-item-y">
5139
- <span className="transform-suggestion-col">{`${colunaY || 'Y'} (Y)`}</span>
5140
- <span className="transform-suggestion-fn">{item.transformacao_y || '(x)'}</span>
5141
- <span className="transform-suggestion-item-note">Dependente</span>
 
 
 
 
 
 
 
 
 
 
5142
  </div>
5143
- {Object.entries(item.transformacoes_x || {}).map(([coluna, transf]) => {
5144
- const grau = Number(item.graus_coef?.[coluna] ?? 0)
5145
- return (
5146
- <div key={`scol-${item.rank || idx}-${coluna}`} className="transform-suggestion-item">
5147
- <span className="transform-suggestion-col">{coluna}</span>
5148
- <span className="transform-suggestion-fn">{transf}</span>
5149
- <span className={grauBadgeClass(grau)}>{GRAU_LABEL_CURTO[grau] || 'Sem enq.'}</span>
5150
- </div>
5151
- )
5152
- })}
5153
  </div>
5154
- <button className="btn-adopt-model" onClick={() => onAdoptSuggestion(idx)} disabled={loading}>
5155
- Adotar e Ajustar Modelo
5156
- </button>
5157
- </div>
5158
- ))}
5159
  </div>
5160
  ) : (
5161
  <div dangerouslySetInnerHTML={{ __html: selection.busca?.html || '' }} />
@@ -5284,7 +5450,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5284
  Modo PNG automático para mais de {secao13PngPayload?.limiar || fit?.grafico_dispersao_modelo_limiar_png || 1500} pontos. Ao final da seção, podem ser gerados individualmente os gráficos interativos.
5285
  </div>
5286
  ) : null}
5287
- <div className="row dispersao-config-row">
5288
  <div className="dispersao-config-field">
5289
  <label>Eixo X</label>
5290
  <select
@@ -5401,7 +5567,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5401
  </div>
5402
  {secao13InterativoSelecionado !== 'none' ? (
5403
  <>
5404
- <div className="row dispersao-config-row">
5405
  <div className="dispersao-config-field">
5406
  <label>Eixo X (interativo)</label>
5407
  <select
@@ -5489,24 +5655,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5489
  )}
5490
  </select>
5491
  </div>
5492
- <div className="download-actions-bar">
5493
- <button
5494
- type="button"
5495
- className="btn-download-subtle"
5496
- title={secao13InterativoAtual?.legenda || ''}
5497
- onClick={() => {
5498
- if (!secao13InterativoAtual) return
5499
- void onDownloadFigurePng(
5500
- secao13InterativoAtual.figure,
5501
- `secao13_${sanitizeFileName(secao13InterativoAtual.label, 'dispersao_interativo')}`,
5502
- { forceHideLegend: true },
5503
- )
5504
- }}
5505
- disabled={loading || downloadingAssets || !secao13InterativoAtual?.figure}
5506
- >
5507
- Fazer download
5508
- </button>
5509
- </div>
5510
  {secao13InterativoAtual?.figure ? (
5511
  <PlotFigure
5512
  key={`s12-interativo-plot-${secao13InterativoAtual.id}`}
@@ -5519,6 +5667,23 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5519
  forceHideLegend
5520
  className="plot-stretch"
5521
  lazy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5522
  />
5523
  ) : (
5524
  <div className="empty-box">Grafico indisponivel.</div>
@@ -5528,57 +5693,24 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5528
  </>
5529
  ) : (
5530
  <>
5531
- <div className="download-actions-bar">
5532
- {graficosSecao12.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
5533
- {graficosSecao12.map((item, idx) => {
5534
- const fileBase = `secao13_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
5535
- return (
5536
- <button
5537
- key={`s12-dl-${item.id}`}
5538
- type="button"
5539
- className="btn-download-subtle"
5540
- title={item.legenda}
5541
- onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
5542
- disabled={loading || downloadingAssets || !item.figure}
5543
- >
5544
- {graficosSecao12.length > 1 ? item.label : 'Fazer download'}
5545
- </button>
5546
- )
5547
- })}
5548
- {graficosSecao12.length > 1 ? (
5549
- <button
5550
- type="button"
5551
- className="btn-download-subtle"
5552
- onClick={() => onDownloadFiguresPngBatch(
5553
- graficosSecao12.map((item, idx) => ({
5554
- figure: item.figure,
5555
- fileNameBase: `secao13_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
5556
- forceHideLegend: true,
5557
- })),
5558
- )}
5559
- disabled={loading || downloadingAssets || graficosSecao12.length === 0}
5560
- >
5561
- Todos
5562
- </button>
5563
- ) : null}
5564
- </div>
5565
  {graficosSecao12.length > 0 ? (
5566
- <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao12 }}>
5567
- {graficosSecao12.map((item) => (
5568
- <PlotFigure
5569
- key={`s12-plot-${item.id}`}
5570
- figure={item.figure}
5571
- indexedFigure={graficosSecao12ComIndicesMap.get(String(item.label || '').trim()) || null}
5572
- onRequestIndexedFigure={ensureSecao13GraficoComIndices}
5573
- title={item.title}
5574
- subtitle={item.subtitle}
5575
- showPointIndexToggle
5576
- forceHideLegend
5577
- className="plot-stretch"
5578
- lazy
5579
- />
5580
- ))}
5581
- </div>
 
5582
  ) : (
5583
  <div className="empty-box">Grafico indisponivel.</div>
5584
  )}
@@ -5587,54 +5719,100 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5587
  </SectionBlock>
5588
 
5589
  <SectionBlock step="14" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
5590
- <div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
5591
- <div className="equation-formats-section">
5592
- <h4>Equações do Modelo</h4>
5593
- <EquationFormatsPanel
5594
- equacoes={fit.equacoes}
5595
- onDownload={(mode) => void onDownloadEquacao(mode)}
5596
- disabled={loading || downloadingAssets}
5597
- />
5598
- </div>
5599
- <div className="download-actions-bar">
5600
- <span className="download-actions-label">Fazer download:</span>
5601
- <button
5602
- type="button"
5603
- className="btn-download-subtle"
5604
- onClick={() => onDownloadTableCsv(fit.tabela_coef, 'secao14_coeficientes')}
5605
- disabled={loading || downloadingAssets || !fit.tabela_coef}
5606
- >
5607
- Coeficientes
5608
- </button>
5609
- <button
5610
- type="button"
5611
- className="btn-download-subtle"
5612
- onClick={() => onDownloadTableCsv(fit.tabela_obs_calc, 'secao14_obs_calc')}
5613
- disabled={loading || downloadingAssets || !fit.tabela_obs_calc}
5614
- >
5615
- Obs x Calc
5616
- </button>
5617
- <button
5618
- type="button"
5619
- className="btn-download-subtle"
5620
- onClick={() => onDownloadTablesCsvBatch([
5621
- { table: fit.tabela_coef, fileNameBase: 'secao14_coeficientes' },
5622
- { table: fit.tabela_obs_calc, fileNameBase: 'secao14_obs_calc' },
5623
- ])}
5624
- disabled={loading || downloadingAssets || (!fit.tabela_coef && !fit.tabela_obs_calc)}
5625
- >
5626
- Todos
5627
- </button>
5628
  </div>
5629
- <div className="two-col diagnostic-tables">
5630
- <div className="pane">
5631
- <h4>Tabela de Coeficientes</h4>
5632
- <DataTable table={fit.tabela_coef} maxHeight={460} />
5633
- </div>
5634
- <div className="pane">
5635
- <h4>Valores Observados x Calculados</h4>
5636
- <DataTable table={fit.tabela_obs_calc} maxHeight={460} />
5637
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5638
  </div>
5639
  </SectionBlock>
5640
 
 
234
  return num.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
235
  }
236
 
237
+ function Section14MetricRows({ rows }) {
238
+ return (
239
+ <div className="section14-field-grid">
240
+ {(rows || []).map((row) => (
241
+ <div key={row.label} className="section14-field-row">
242
+ <span className="section14-field-label">{row.label}</span>
243
+ <span className="section14-field-value">{row.value}</span>
244
+ </div>
245
+ ))}
246
+ </div>
247
+ )
248
+ }
249
+
250
  function formatarValorTabelaKnn(coluna, valor) {
251
  if (valor === null || valor === undefined || valor === '') return '-'
252
  const nome = String(coluna || '').toLowerCase()
 
703
  return mapa
704
  }
705
 
706
+ function ScatterPlotCarousel({
707
+ items,
708
+ indexedFigureMap,
709
+ onRequestIndexedFigure,
710
+ itemKeyPrefix,
711
+ sectionFilePrefix,
712
+ onDownloadFigure,
713
+ onDownloadAll,
714
+ loading = false,
715
+ downloadingAssets = false,
716
+ }) {
717
+ const [page, setPage] = useState(0)
718
+ const pageSize = 2
719
+ const safeItems = Array.isArray(items) ? items : []
720
+ const totalPages = Math.max(1, Math.ceil(safeItems.length / pageSize))
721
+ const itemsSignature = safeItems.map((item) => String(item?.id || item?.label || '')).join('|')
722
+ const startIndex = Math.min(page, totalPages - 1) * pageSize
723
+ const visibleItems = safeItems.slice(startIndex, startIndex + pageSize)
724
+
725
+ useEffect(() => {
726
+ setPage(0)
727
+ }, [itemsSignature])
728
+
729
+ useEffect(() => {
730
+ setPage((current) => Math.min(current, totalPages - 1))
731
+ }, [totalPages])
732
+
733
+ if (safeItems.length === 0) {
734
+ return <div className="empty-box">Grafico indisponivel.</div>
735
+ }
736
+
737
+ function getPairTitle(pageIndex) {
738
+ const pair = safeItems.slice(pageIndex * pageSize, (pageIndex * pageSize) + pageSize)
739
+ return pair.map((item) => String(item?.label || item?.title || '').trim()).filter(Boolean).join(' e ')
740
+ }
741
+
742
+ function getPairLabel(pageIndex) {
743
+ const title = getPairTitle(pageIndex)
744
+ if (title) return title
745
+ const pairStart = (pageIndex * pageSize) + 1
746
+ const pairEnd = Math.min(pairStart + pageSize - 1, safeItems.length)
747
+ return pairStart === pairEnd ? String(pairStart) : `${pairStart}-${pairEnd}`
748
+ }
749
+
750
+ function buildFileNameBase(item, itemIndex) {
751
+ return `${sectionFilePrefix}_${sanitizeFileName(item?.label, `dispersao_${itemIndex + 1}`)}`
752
+ }
753
+
754
+ return (
755
+ <div className="scatter-carousel">
756
+ {typeof onDownloadAll === 'function' && safeItems.length > 1 ? (
757
+ <div className="scatter-carousel-actions">
758
+ <button
759
+ type="button"
760
+ className="btn-download-subtle scatter-carousel-download-all"
761
+ onClick={onDownloadAll}
762
+ disabled={loading || downloadingAssets || safeItems.length === 0}
763
+ >
764
+ Baixar todos os gráficos
765
+ </button>
766
+ </div>
767
+ ) : null}
768
+ {totalPages > 1 ? (
769
+ <div className="scatter-carousel-pills" aria-label="Escolher dupla de gráficos">
770
+ {Array.from({ length: totalPages }, (_, pageIndex) => {
771
+ const pairStart = (pageIndex * pageSize) + 1
772
+ const pairEnd = Math.min(pairStart + pageSize - 1, safeItems.length)
773
+ const active = pageIndex === page
774
+ return (
775
+ <button
776
+ key={`scatter-pair-${pairStart}-${pairEnd}`}
777
+ type="button"
778
+ className={`scatter-carousel-pill${active ? ' is-active' : ''}`}
779
+ onClick={() => setPage(pageIndex)}
780
+ title={getPairTitle(pageIndex)}
781
+ aria-current={active ? 'true' : undefined}
782
+ >
783
+ {getPairLabel(pageIndex)}
784
+ </button>
785
+ )
786
+ })}
787
+ </div>
788
+ ) : null}
789
+ <div className="scatter-carousel-track">
790
+ {visibleItems.map((item, visibleIndex) => {
791
+ const itemIndex = startIndex + visibleIndex
792
+ const fileNameBase = buildFileNameBase(item, itemIndex)
793
+ return (
794
+ <PlotFigure
795
+ key={`${itemKeyPrefix}-${item.id}`}
796
+ figure={item.figure}
797
+ indexedFigure={indexedFigureMap?.get(String(item.label || '').trim()) || null}
798
+ onRequestIndexedFigure={onRequestIndexedFigure}
799
+ title={item.title}
800
+ subtitle={item.subtitle}
801
+ showPointIndexToggle
802
+ forceHideLegend
803
+ className="plot-stretch"
804
+ lazy
805
+ headerActions={typeof onDownloadFigure === 'function' ? (
806
+ <button
807
+ type="button"
808
+ className="btn-download-subtle plot-card-download-btn"
809
+ title={item.legenda || item.label || 'Fazer download'}
810
+ onClick={() => onDownloadFigure(item.figure, fileNameBase, { forceHideLegend: true })}
811
+ disabled={loading || downloadingAssets || !item.figure}
812
+ >
813
+ Fazer download
814
+ </button>
815
+ ) : null}
816
+ />
817
+ )
818
+ })}
819
+ </div>
820
+ </div>
821
+ )
822
+ }
823
+
824
  function formatConselhoRegistro(elaborador) {
825
  if (!elaborador) return ''
826
  const conselho = String(elaborador.conselho || '').trim()
 
1076
  const [section11LocksOpen, setSection11LocksOpen] = useState(false)
1077
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
1078
  const [section6EditOpen, setSection6EditOpen] = useState(true)
1079
+ const [section14Tab, setSection14Tab] = useState('diagnosticos')
1080
 
1081
  const [fit, setFit] = useState(null)
1082
  const [dispersaoEixoX, setDispersaoEixoX] = useState('transformado')
 
1144
  const [sectionsMountKey, setSectionsMountKey] = useState(0)
1145
  const [renderedSectionSteps, setRenderedSectionSteps] = useState(() => ['1'])
1146
  const [visibleSectionSteps, setVisibleSectionSteps] = useState(() => ['1'])
1147
+ const [currentSectionStep, setCurrentSectionStep] = useState('1')
1148
  const visibleSectionStepsRef = useRef(new Set(['1']))
1149
  const elaboracaoShareHref = repoModeloSelecionado
1150
  ? buildElaboracaoModeloLink(repoModeloSelecionado)
 
1412
 
1413
  return faixas
1414
  }, [fit?.tabela_metricas?.rows, fit?.variaveis_filtro])
1415
+ const section14DiagnosticosGerais = useMemo(() => {
1416
+ const diagnosticos = fit?.diagnosticos || {}
1417
+ return [
1418
+ { label: 'Número de observações', value: formatNumberBr(diagnosticos.n, 0) },
1419
+ { label: 'Número de variáveis independentes', value: formatNumberBr(diagnosticos.k, 0) },
1420
+ { label: 'Desvio padrão dos resíduos', value: formatMetric4(diagnosticos.desvio_padrao_residuos) },
1421
+ { label: 'MSE', value: formatMetric4(diagnosticos.mse) },
1422
+ { label: 'R²', value: formatMetric4(diagnosticos.r2) },
1423
+ { label: 'R² ajustado', value: formatMetric4(diagnosticos.r2_ajustado) },
1424
+ { label: 'Correlação Pearson', value: formatMetric4(diagnosticos.r_pearson) },
1425
+ ]
1426
+ }, [fit?.diagnosticos])
1427
+ const section14Testes = useMemo(() => {
1428
+ const diagnosticos = fit?.diagnosticos || {}
1429
+ return [
1430
+ {
1431
+ id: 'f',
1432
+ label: 'Teste F',
1433
+ rows: [
1434
+ { label: 'Estatística F', value: formatMetric4(diagnosticos.Fc) },
1435
+ { label: 'P-valor', value: formatMetric4(diagnosticos.p_valor_F) },
1436
+ { label: 'Interpretação', value: diagnosticos.interp_F || '-' },
1437
+ ],
1438
+ },
1439
+ {
1440
+ id: 'ks',
1441
+ label: 'Normalidade KS',
1442
+ rows: [
1443
+ { label: 'Estatística KS', value: formatMetric4(diagnosticos.ks_stat) },
1444
+ { label: 'P-valor', value: formatMetric4(diagnosticos.ks_p) },
1445
+ { label: 'Interpretação', value: diagnosticos.interp_KS || '-' },
1446
+ ],
1447
+ },
1448
+ {
1449
+ id: 'curva',
1450
+ label: 'Curva normal',
1451
+ rows: [
1452
+ { label: 'Percentuais atingidos', value: diagnosticos.perc_resid || '-' },
1453
+ { label: 'Ideal 68%', value: 'aceitável entre 64% e 75%' },
1454
+ { label: 'Ideal 90%', value: 'aceitável entre 88% e 95%' },
1455
+ { label: 'Ideal 95%', value: 'aceitável entre 95% e 100%' },
1456
+ ],
1457
+ },
1458
+ {
1459
+ id: 'dw',
1460
+ label: 'Durbin-Watson',
1461
+ rows: [
1462
+ { label: 'Estatística DW', value: formatMetric4(diagnosticos.dw) },
1463
+ { label: 'Interpretação', value: diagnosticos.interp_DW || '-' },
1464
+ ],
1465
+ },
1466
+ {
1467
+ id: 'bp',
1468
+ label: 'Breusch-Pagan',
1469
+ rows: [
1470
+ { label: 'Estatística LM', value: formatMetric4(diagnosticos.bp_lm) },
1471
+ { label: 'P-valor', value: formatMetric4(diagnosticos.bp_p) },
1472
+ { label: 'Interpretação', value: diagnosticos.interp_BP || '-' },
1473
+ ],
1474
+ },
1475
+ ]
1476
+ }, [fit?.diagnosticos])
1477
  const resumoResiduoPadStats = useMemo(() => {
1478
  const rows = fit?.tabela_metricas?.rows
1479
  if (!Array.isArray(rows) || rows.length === 0) return null
 
1611
  if (secao10InterativoSelecionado === 'none' || graficosSecao9Interativo.length === 0) return null
1612
  return graficosSecao9Interativo.find((item) => String(item.label || '') === secao10InterativoSelecionado) || graficosSecao9Interativo[0]
1613
  }, [graficosSecao9Interativo, secao10InterativoSelecionado])
 
 
 
 
1614
  const colunasOriginaisDispersao = useMemo(() => {
1615
  const cols = Array.isArray(dados?.columns) ? dados.columns : []
1616
  return cols
 
1696
  if (secao13InterativoSelecionado === 'none' || graficosSecao12Interativo.length === 0) return null
1697
  return graficosSecao12Interativo.find((item) => String(item.label || '') === secao13InterativoSelecionado) || graficosSecao12Interativo[0]
1698
  }, [graficosSecao12Interativo, secao13InterativoSelecionado])
 
 
 
 
1699
  const secao15DiagnosticoPng = useMemo(
1700
  () => String(fit?.graficos_diagnostico_modo || '') === 'png',
1701
  [fit?.graficos_diagnostico_modo],
 
1806
  if (typeof window === 'undefined' || typeof document === 'undefined') {
1807
  setRenderedSectionSteps(['1'])
1808
  setVisibleSectionSteps(['1'])
1809
+ setCurrentSectionStep('1')
1810
  visibleSectionStepsRef.current = new Set(['1'])
1811
  return undefined
1812
  }
 
1820
 
1821
  if (sections.length === 0) {
1822
  setVisibleSectionSteps(['1'])
1823
+ setCurrentSectionStep('1')
1824
  visibleSectionStepsRef.current = new Set(['1'])
1825
  return undefined
1826
  }
 
1864
  .map((section) => section.getAttribute('data-section-step')),
1865
  )
1866
  const normalizedVisible = nextVisible.length > 0 ? nextVisible : [getClosestStepToTop()]
1867
+ const nextCurrentStep = getClosestStepToTop()
1868
  visibleSectionStepsRef.current = new Set(normalizedVisible)
1869
  setVisibleSectionSteps((current) => (isSameSectionStepList(current, normalizedVisible) ? current : normalizedVisible))
1870
+ setCurrentSectionStep((current) => (current === nextCurrentStep ? current : nextCurrentStep))
1871
  }
1872
 
1873
  computeVisibleSteps()
 
4135
 
4136
  return (
4137
  <div ref={elaboracaoRootRef} className="tab-content">
4138
+ <div
4139
+ className={`elaboracao-layout${repoModeloDropdownOpen ? ' is-repo-model-open' : ''}`}
4140
+ style={sideNavDynamicStyle}
4141
+ data-current-section-step={currentSectionStep}
4142
+ >
4143
  <aside className="elaboracao-side-nav" aria-label="Navegação de seções da elaboração">
4144
  <ol className="elaboracao-side-nav-list">
4145
  {ELABORACAO_SECOES_NAV.map((secao) => {
 
4762
  : 'Há outliers excluídos: não'}
4763
  </div>
4764
  {outliersAnteriores.length > 0 ? (
4765
+ <div className="resumo-outliers-box resumo-outliers-box-scrollable">
4766
+ Índices excluídos: {joinSelection(outliersAnteriores)}
4767
+ </div>
4768
  ) : null}
4769
  </div>
4770
  <div className="download-actions-bar">
 
4779
  Fazer download
4780
  </button>
4781
  </div>
4782
+ <DataTable table={dados} maxHeight={320} />
4783
  </div>
4784
  </SectionBlock>
4785
 
 
4907
  {colunasX.length}/{colunasXDisponiveis.length} selecionadas
4908
  </span>
4909
  </div>
4910
+ <div className="checkbox-inline-wrap checkbox-inline-wrap-scrollable">
4911
  {colunasXDisponiveis.map((col) => (
4912
  <label key={`x-${col}`} className="compact-checkbox">
4913
  <input
 
4968
 
4969
  <div className="compact-option-group compact-option-group-codigo">
4970
  <h4>Variáveis Categóricas Codificadas</h4>
4971
+ <div className="checkbox-inline-wrap checkbox-inline-wrap-scrollable">
4972
  {colunasX.map((col) => (
4973
  <label key={`c-${col}`} className="compact-checkbox">
4974
  <input type="checkbox" checked={codigoAlocado.includes(col)} onChange={() => toggleSelection(setCodigoAlocado, col)} />
 
5045
  <div className="section6-summary-group">
5046
  <div className="section6-summary-label">Variáveis categóricas codificadas:</div>
5047
  {selecaoAplicada.codigo_alocado.length > 0 ? (
5048
+ <div className="checkbox-inline-wrap checkbox-inline-wrap-scrollable">
5049
  {selecaoAplicada.codigo_alocado.map((coluna) => (
5050
  <span key={`selected-c-${coluna}`} className="compact-chip">{coluna}</span>
5051
  ))}
 
5149
  </div>
5150
  {secao10InterativoSelecionado !== 'none' ? (
5151
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5152
  {secao10InterativoAtual?.figure ? (
5153
  <PlotFigure
5154
  key={`s9-interativo-plot-${secao10InterativoAtual.id}`}
 
5161
  forceHideLegend
5162
  className="plot-stretch"
5163
  lazy
5164
+ headerActions={(
5165
+ <button
5166
+ type="button"
5167
+ className="btn-download-subtle plot-card-download-btn"
5168
+ title={secao10InterativoAtual?.legenda || ''}
5169
+ onClick={() => {
5170
+ void onDownloadFigurePng(
5171
+ secao10InterativoAtual.figure,
5172
+ `secao10_${sanitizeFileName(secao10InterativoAtual.label, 'dispersao_interativo')}`,
5173
+ { forceHideLegend: true },
5174
+ )
5175
+ }}
5176
+ disabled={loading || downloadingAssets || !secao10InterativoAtual?.figure}
5177
+ >
5178
+ Fazer download
5179
+ </button>
5180
+ )}
5181
  />
5182
  ) : (
5183
  <div className="empty-box">Grafico indisponivel.</div>
 
5187
  </>
5188
  ) : (
5189
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5190
  {graficosSecao9.length > 0 ? (
5191
+ <ScatterPlotCarousel
5192
+ items={graficosSecao9}
5193
+ indexedFigureMap={graficosSecao9ComIndicesMap}
5194
+ onRequestIndexedFigure={ensureSecao10GraficoComIndices}
5195
+ itemKeyPrefix="s9-plot"
5196
+ sectionFilePrefix="secao10"
5197
+ onDownloadFigure={onDownloadFigurePng}
5198
+ onDownloadAll={() => onDownloadFiguresPngBatch(
5199
+ graficosSecao9.map((item, idx) => ({
5200
+ figure: item.figure,
5201
+ fileNameBase: `secao10_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
5202
+ forceHideLegend: true,
5203
+ })),
5204
+ )}
5205
+ loading={loading}
5206
+ downloadingAssets={downloadingAssets}
5207
+ />
5208
  ) : (
5209
  <div className="empty-box">Grafico indisponivel.</div>
5210
  )}
 
5284
  </div>
5285
  </div>
5286
  {(selection.busca?.resultados || []).length > 0 ? (
5287
+ <div className="section11-suggestions-block">
5288
+ <div className="section11-suggestions-title">Transformações Sugeridas</div>
5289
+ <div className="transform-suggestions-grid">
5290
+ {(selection.busca?.resultados || []).map((item, idx) => (
5291
+ <div key={`sug-${item.rank || idx + 1}`} className="transform-suggestion-card">
5292
+ <div className="transform-suggestion-head">
5293
+ <span className="transform-suggestion-rank">#{item.rank || idx + 1}</span>
5294
+ <div className="transform-suggestion-metrics">
5295
+ <span className="transform-suggestion-r2">R² = {formatMetric4(item.r2)}</span>
5296
+ <span className="transform-suggestion-r2adj">R² ajustado = {formatMetric4(item.r2_ajustado)}</span>
5297
+ <span className={grauBadgeClass(Number(item.grau_f ?? 0))}>
5298
+ Teste F: {GRAU_LABEL_CURTO[Number(item.grau_f ?? 0)] || 'Sem enq.'}
5299
+ </span>
5300
+ </div>
5301
  </div>
5302
+ <div className="transform-suggestion-list">
5303
+ <div className="transform-suggestion-item transform-suggestion-item-y">
5304
+ <span className="transform-suggestion-col">{`${colunaY || 'Y'} (Y)`}</span>
5305
+ <span className="transform-suggestion-fn">{item.transformacao_y || '(x)'}</span>
5306
+ <span className="transform-suggestion-item-note">Dependente</span>
5307
+ </div>
5308
+ {Object.entries(item.transformacoes_x || {}).map(([coluna, transf]) => {
5309
+ const grau = Number(item.graus_coef?.[coluna] ?? 0)
5310
+ return (
5311
+ <div key={`scol-${item.rank || idx}-${coluna}`} className="transform-suggestion-item">
5312
+ <span className="transform-suggestion-col">{coluna}</span>
5313
+ <span className="transform-suggestion-fn">{transf}</span>
5314
+ <span className={grauBadgeClass(grau)}>{GRAU_LABEL_CURTO[grau] || 'Sem enq.'}</span>
5315
+ </div>
5316
+ )
5317
+ })}
5318
  </div>
5319
+ <button className="btn-adopt-model" onClick={() => onAdoptSuggestion(idx)} disabled={loading}>
5320
+ Adotar e Ajustar Modelo
5321
+ </button>
 
 
 
 
 
 
 
5322
  </div>
5323
+ ))}
5324
+ </div>
 
 
 
5325
  </div>
5326
  ) : (
5327
  <div dangerouslySetInnerHTML={{ __html: selection.busca?.html || '' }} />
 
5450
  Modo PNG automático para mais de {secao13PngPayload?.limiar || fit?.grafico_dispersao_modelo_limiar_png || 1500} pontos. Ao final da seção, podem ser gerados individualmente os gráficos interativos.
5451
  </div>
5452
  ) : null}
5453
+ <div className="row dispersao-config-row dispersao-config-panel">
5454
  <div className="dispersao-config-field">
5455
  <label>Eixo X</label>
5456
  <select
 
5567
  </div>
5568
  {secao13InterativoSelecionado !== 'none' ? (
5569
  <>
5570
+ <div className="row dispersao-config-row dispersao-config-panel">
5571
  <div className="dispersao-config-field">
5572
  <label>Eixo X (interativo)</label>
5573
  <select
 
5655
  )}
5656
  </select>
5657
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5658
  {secao13InterativoAtual?.figure ? (
5659
  <PlotFigure
5660
  key={`s12-interativo-plot-${secao13InterativoAtual.id}`}
 
5667
  forceHideLegend
5668
  className="plot-stretch"
5669
  lazy
5670
+ headerActions={(
5671
+ <button
5672
+ type="button"
5673
+ className="btn-download-subtle plot-card-download-btn"
5674
+ title={secao13InterativoAtual?.legenda || ''}
5675
+ onClick={() => {
5676
+ void onDownloadFigurePng(
5677
+ secao13InterativoAtual.figure,
5678
+ `secao13_${sanitizeFileName(secao13InterativoAtual.label, 'dispersao_interativo')}`,
5679
+ { forceHideLegend: true },
5680
+ )
5681
+ }}
5682
+ disabled={loading || downloadingAssets || !secao13InterativoAtual?.figure}
5683
+ >
5684
+ Fazer download
5685
+ </button>
5686
+ )}
5687
  />
5688
  ) : (
5689
  <div className="empty-box">Grafico indisponivel.</div>
 
5693
  </>
5694
  ) : (
5695
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5696
  {graficosSecao12.length > 0 ? (
5697
+ <ScatterPlotCarousel
5698
+ items={graficosSecao12}
5699
+ indexedFigureMap={graficosSecao12ComIndicesMap}
5700
+ onRequestIndexedFigure={ensureSecao13GraficoComIndices}
5701
+ itemKeyPrefix="s12-plot"
5702
+ sectionFilePrefix="secao13"
5703
+ onDownloadFigure={onDownloadFigurePng}
5704
+ onDownloadAll={() => onDownloadFiguresPngBatch(
5705
+ graficosSecao12.map((item, idx) => ({
5706
+ figure: item.figure,
5707
+ fileNameBase: `secao13_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
5708
+ forceHideLegend: true,
5709
+ })),
5710
+ )}
5711
+ loading={loading}
5712
+ downloadingAssets={downloadingAssets}
5713
+ />
5714
  ) : (
5715
  <div className="empty-box">Grafico indisponivel.</div>
5716
  )}
 
5719
  </SectionBlock>
5720
 
5721
  <SectionBlock step="14" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
5722
+ <div className="section14-tabs" role="tablist" aria-label="Conteúdos do diagnóstico do modelo">
5723
+ {[
5724
+ { id: 'diagnosticos', label: 'Diagnósticos' },
5725
+ { id: 'testes', label: 'Testes' },
5726
+ { id: 'equacoes', label: 'Equações' },
5727
+ { id: 'coeficientes', label: 'Coeficientes' },
5728
+ { id: 'obs_calc', label: 'Obs x Calc' },
5729
+ ].map((tab) => (
5730
+ <button
5731
+ key={`sec14-tab-${tab.id}`}
5732
+ type="button"
5733
+ className={`section14-tab-pill${section14Tab === tab.id ? ' is-active' : ''}`}
5734
+ onClick={() => setSection14Tab(tab.id)}
5735
+ aria-selected={section14Tab === tab.id}
5736
+ role="tab"
5737
+ >
5738
+ {tab.label}
5739
+ </button>
5740
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5741
  </div>
5742
+
5743
+ <div className="section14-panel" role="tabpanel">
5744
+ {section14Tab === 'diagnosticos' ? (
5745
+ fit?.diagnosticos ? (
5746
+ <div className="section14-diagnostics-panel">
5747
+ <h4>Estatísticas Gerais</h4>
5748
+ <Section14MetricRows rows={section14DiagnosticosGerais} />
5749
+ </div>
5750
+ ) : (
5751
+ <div className="section14-diagnostics-panel" dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
5752
+ )
5753
+ ) : null}
5754
+
5755
+ {section14Tab === 'testes' ? (
5756
+ fit?.diagnosticos ? (
5757
+ <div className="section14-tests-panel">
5758
+ <div className="section14-tests-grid">
5759
+ {section14Testes.map((teste) => (
5760
+ <div key={`sec14-test-card-${teste.id}`} className="section14-test-card">
5761
+ <h4>{teste.label}</h4>
5762
+ <Section14MetricRows rows={teste.rows} />
5763
+ </div>
5764
+ ))}
5765
+ </div>
5766
+ </div>
5767
+ ) : (
5768
+ <div className="section14-diagnostics-panel" dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
5769
+ )
5770
+ ) : null}
5771
+
5772
+ {section14Tab === 'equacoes' ? (
5773
+ <div className="equation-formats-section section14-equations-panel">
5774
+ <h4>Equações do Modelo</h4>
5775
+ <EquationFormatsPanel
5776
+ equacoes={fit.equacoes}
5777
+ onDownload={(mode) => void onDownloadEquacao(mode)}
5778
+ disabled={loading || downloadingAssets}
5779
+ />
5780
+ </div>
5781
+ ) : null}
5782
+
5783
+ {section14Tab === 'coeficientes' ? (
5784
+ <div className="section14-table-panel">
5785
+ <div className="section14-table-head">
5786
+ <h4>Tabela de Coeficientes</h4>
5787
+ <button
5788
+ type="button"
5789
+ className="btn-download-subtle"
5790
+ onClick={() => onDownloadTableCsv(fit.tabela_coef, 'secao14_coeficientes')}
5791
+ disabled={loading || downloadingAssets || !fit.tabela_coef}
5792
+ >
5793
+ Fazer download
5794
+ </button>
5795
+ </div>
5796
+ <DataTable table={fit.tabela_coef} maxHeight={460} />
5797
+ </div>
5798
+ ) : null}
5799
+
5800
+ {section14Tab === 'obs_calc' ? (
5801
+ <div className="section14-table-panel">
5802
+ <div className="section14-table-head">
5803
+ <h4>Valores Observados x Calculados</h4>
5804
+ <button
5805
+ type="button"
5806
+ className="btn-download-subtle"
5807
+ onClick={() => onDownloadTableCsv(fit.tabela_obs_calc, 'secao14_obs_calc')}
5808
+ disabled={loading || downloadingAssets || !fit.tabela_obs_calc}
5809
+ >
5810
+ Fazer download
5811
+ </button>
5812
+ </div>
5813
+ <DataTable table={fit.tabela_obs_calc} maxHeight={460} />
5814
+ </div>
5815
+ ) : null}
5816
  </div>
5817
  </SectionBlock>
5818
 
frontend/src/components/PlotFigure.jsx CHANGED
@@ -12,6 +12,7 @@ function PlotFigure({
12
  className = '',
13
  lazy = false,
14
  showPointIndexToggle = false,
 
15
  }) {
16
  const containerRef = useRef(null)
17
  const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy)
@@ -115,22 +116,27 @@ function PlotFigure({
115
 
116
  return (
117
  <div ref={containerRef} className={cardClassName}>
118
- {title || subtitle ? (
119
  <div className="plot-card-head">
120
- {title ? <h4 className="plot-card-title">{title}</h4> : null}
121
- {subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null}
122
- {showPointIndexToggle ? (
123
- <label className={`plot-card-toggle${canToggleIndices ? '' : ' is-disabled'}`}>
124
- <input
125
- type="checkbox"
126
- checked={showPointIndices}
127
- disabled={!canToggleIndices || loadingIndexedFigure}
128
- onChange={handleToggleChange}
129
- title={toggleTitle}
130
- />
131
- {loadingIndexedFigure ? 'Carregando índices...' : 'Exibir índices dos pontos'}
132
- </label>
133
- ) : null}
 
 
 
 
 
134
  </div>
135
  ) : null}
136
  {shouldRenderPlot ? (
 
12
  className = '',
13
  lazy = false,
14
  showPointIndexToggle = false,
15
+ headerActions = null,
16
  }) {
17
  const containerRef = useRef(null)
18
  const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy)
 
116
 
117
  return (
118
  <div ref={containerRef} className={cardClassName}>
119
+ {title || subtitle || showPointIndexToggle || headerActions ? (
120
  <div className="plot-card-head">
121
+ <div className="plot-card-head-main">
122
+ {title ? <h4 className="plot-card-title">{title}</h4> : null}
123
+ {subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null}
124
+ </div>
125
+ <div className="plot-card-head-actions">
126
+ {showPointIndexToggle ? (
127
+ <label className={`plot-card-toggle${canToggleIndices ? '' : ' is-disabled'}`}>
128
+ <input
129
+ type="checkbox"
130
+ checked={showPointIndices}
131
+ disabled={!canToggleIndices || loadingIndexedFigure}
132
+ onChange={handleToggleChange}
133
+ title={toggleTitle}
134
+ />
135
+ {loadingIndexedFigure ? 'Carregando índices...' : 'Exibir índices dos pontos'}
136
+ </label>
137
+ ) : null}
138
+ {headerActions}
139
+ </div>
140
  </div>
141
  ) : null}
142
  {shouldRenderPlot ? (
frontend/src/styles.css CHANGED
@@ -1793,6 +1793,32 @@ textarea {
1793
  inset 0 0 0 1px #dfe9f3;
1794
  animation: sectionIn 0.35s ease both;
1795
  animation-delay: calc(var(--section-order, 1) * 25ms);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1796
  }
1797
 
1798
  .section-head {
@@ -2068,6 +2094,15 @@ textarea {
2068
  gap: 14px;
2069
  }
2070
 
 
 
 
 
 
 
 
 
 
2071
  .dispersao-config-field {
2072
  display: grid;
2073
  gap: 6px;
@@ -3153,8 +3188,7 @@ button.pesquisa-coluna-remove:hover {
3153
  .plot-card-toggle {
3154
  display: inline-flex;
3155
  align-items: center;
3156
- gap: 8px;
3157
- margin-top: 8px;
3158
  color: #42586e;
3159
  font-size: 0.8rem;
3160
  font-weight: 700;
@@ -5021,6 +5055,152 @@ button.btn-upload-select {
5021
  font-size: 0.9rem;
5022
  }
5023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5024
  .map-frame {
5025
  display: block;
5026
  width: 100%;
@@ -5605,6 +5785,14 @@ button.btn-upload-select {
5605
  gap: 8px 10px;
5606
  }
5607
 
 
 
 
 
 
 
 
 
5608
  .checkbox-inline-wrap-tools {
5609
  align-items: center;
5610
  margin-bottom: 6px;
@@ -5697,14 +5885,14 @@ button.btn-upload-select {
5697
  align-items: flex-start;
5698
  justify-content: space-between;
5699
  gap: 8px;
5700
- margin-bottom: 16px;
5701
  }
5702
 
5703
  .transform-suggestion-metrics {
5704
  display: flex;
5705
  flex-direction: column;
5706
  align-items: flex-end;
5707
- gap: 7px;
5708
  }
5709
 
5710
  .transform-suggestion-rank {
@@ -5719,8 +5907,8 @@ button.btn-upload-select {
5719
  background: #eaf4fe;
5720
  color: #1e4d78;
5721
  font-family: 'JetBrains Mono', monospace;
5722
- font-size: 0.78rem;
5723
- padding: 3px 7px;
5724
  font-weight: 700;
5725
  }
5726
 
@@ -5730,8 +5918,8 @@ button.btn-upload-select {
5730
  background: #f4f8fc;
5731
  color: #355a78;
5732
  font-family: 'JetBrains Mono', monospace;
5733
- font-size: 0.76rem;
5734
- padding: 3px 7px;
5735
  font-weight: 700;
5736
  }
5737
 
@@ -5777,8 +5965,11 @@ button.btn-upload-select {
5777
 
5778
  .section11-search-criteria {
5779
  margin-top: 8px;
5780
- padding-top: 11px;
5781
- border-top: 1px solid #d7e2ee;
 
 
 
5782
  display: grid;
5783
  gap: 8px;
5784
  }
@@ -5795,6 +5986,21 @@ button.btn-upload-select {
5795
  margin-bottom: 0;
5796
  }
5797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5798
  .transform-preview-summary {
5799
  border: 1px solid #d8e5f2;
5800
  border-radius: 12px;
@@ -5940,6 +6146,7 @@ button.btn-upload-select {
5940
  .transform-suggestion-col {
5941
  color: #2e4358;
5942
  font-weight: 700;
 
5943
  min-width: 0;
5944
  overflow: hidden;
5945
  text-overflow: ellipsis;
@@ -6001,6 +6208,11 @@ button.btn-upload-select {
6001
  color: #9d2f2f;
6002
  }
6003
 
 
 
 
 
 
6004
  .transform-suggestion-card button {
6005
  width: 100%;
6006
  justify-content: center;
@@ -6034,6 +6246,84 @@ button.btn-upload-select {
6034
  gap: 12px;
6035
  }
6036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6037
  .section-disclaimer-warning {
6038
  margin: 0 0 10px;
6039
  padding: 9px 12px;
@@ -6106,13 +6396,34 @@ button.btn-upload-select {
6106
  .plot-card-head {
6107
  margin: 4px 4px 8px;
6108
  min-height: 42px;
6109
- display: grid;
6110
- align-content: start;
6111
- gap: 2px;
 
6112
  position: relative;
6113
  z-index: 2;
6114
  }
6115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6116
  .plot-card-title {
6117
  margin: 0;
6118
  color: #2f465c;
@@ -6245,6 +6556,30 @@ button.btn-upload-select {
6245
  margin-bottom: 10px;
6246
  }
6247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6248
  .outlier-actions-row {
6249
  display: flex;
6250
  flex-wrap: wrap;
@@ -6481,6 +6816,13 @@ button.btn-upload-select {
6481
  margin-top: 8px;
6482
  }
6483
 
 
 
 
 
 
 
 
6484
  .outliers-excluidos-details {
6485
  margin-top: 10px;
6486
  border: 1px solid #dbe6f1;
@@ -7050,6 +7392,11 @@ button.btn-download-subtle {
7050
  .micro-msg-grid-codigo {
7051
  grid-template-columns: repeat(4, minmax(0, 1fr));
7052
  gap: 6px 14px;
 
 
 
 
 
7053
  }
7054
 
7055
  .micro-msg {
@@ -7271,6 +7618,18 @@ button.btn-download-subtle {
7271
  grid-template-columns: 1fr;
7272
  }
7273
 
 
 
 
 
 
 
 
 
 
 
 
 
7274
  .plot-card {
7275
  min-height: 340px;
7276
  }
 
1793
  inset 0 0 0 1px #dfe9f3;
1794
  animation: sectionIn 0.35s ease both;
1795
  animation-delay: calc(var(--section-order, 1) * 25ms);
1796
+ transition: border-color 0.18s ease, box-shadow 0.18s ease;
1797
+ }
1798
+
1799
+ .elaboracao-layout[data-current-section-step="1"] .workflow-section[data-section-step="1"],
1800
+ .elaboracao-layout[data-current-section-step="2"] .workflow-section[data-section-step="2"],
1801
+ .elaboracao-layout[data-current-section-step="3"] .workflow-section[data-section-step="3"],
1802
+ .elaboracao-layout[data-current-section-step="4"] .workflow-section[data-section-step="4"],
1803
+ .elaboracao-layout[data-current-section-step="5"] .workflow-section[data-section-step="5"],
1804
+ .elaboracao-layout[data-current-section-step="6"] .workflow-section[data-section-step="6"],
1805
+ .elaboracao-layout[data-current-section-step="7"] .workflow-section[data-section-step="7"],
1806
+ .elaboracao-layout[data-current-section-step="8"] .workflow-section[data-section-step="8"],
1807
+ .elaboracao-layout[data-current-section-step="9"] .workflow-section[data-section-step="9"],
1808
+ .elaboracao-layout[data-current-section-step="10"] .workflow-section[data-section-step="10"],
1809
+ .elaboracao-layout[data-current-section-step="11"] .workflow-section[data-section-step="11"],
1810
+ .elaboracao-layout[data-current-section-step="12"] .workflow-section[data-section-step="12"],
1811
+ .elaboracao-layout[data-current-section-step="13"] .workflow-section[data-section-step="13"],
1812
+ .elaboracao-layout[data-current-section-step="14"] .workflow-section[data-section-step="14"],
1813
+ .elaboracao-layout[data-current-section-step="15"] .workflow-section[data-section-step="15"],
1814
+ .elaboracao-layout[data-current-section-step="16"] .workflow-section[data-section-step="16"],
1815
+ .elaboracao-layout[data-current-section-step="17"] .workflow-section[data-section-step="17"],
1816
+ .elaboracao-layout[data-current-section-step="18"] .workflow-section[data-section-step="18"],
1817
+ .elaboracao-layout[data-current-section-step="19"] .workflow-section[data-section-step="19"] {
1818
+ border-color: #111820;
1819
+ box-shadow:
1820
+ 0 8px 22px rgba(20, 28, 36, 0.1),
1821
+ inset 0 0 0 1px #dfe9f3;
1822
  }
1823
 
1824
  .section-head {
 
2094
  gap: 14px;
2095
  }
2096
 
2097
+ .dispersao-config-panel {
2098
+ margin: 0 0 14px;
2099
+ padding: 12px;
2100
+ border: 1px solid #d7e2ee;
2101
+ border-left: 4px solid #2f80cf;
2102
+ border-radius: 8px;
2103
+ background: linear-gradient(180deg, #fbfdff 0%, #f5f9fd 100%);
2104
+ }
2105
+
2106
  .dispersao-config-field {
2107
  display: grid;
2108
  gap: 6px;
 
3188
  .plot-card-toggle {
3189
  display: inline-flex;
3190
  align-items: center;
3191
+ gap: 6px;
 
3192
  color: #42586e;
3193
  font-size: 0.8rem;
3194
  font-weight: 700;
 
5055
  font-size: 0.9rem;
5056
  }
5057
 
5058
+ .section14-tabs {
5059
+ display: flex;
5060
+ align-items: center;
5061
+ justify-content: center;
5062
+ flex-wrap: wrap;
5063
+ gap: 8px;
5064
+ margin: 0 0 12px;
5065
+ padding: 10px;
5066
+ border: 1px solid #d7e2ee;
5067
+ border-radius: 8px;
5068
+ background: linear-gradient(180deg, #fbfdff 0%, #f5f9fd 100%);
5069
+ }
5070
+
5071
+ .section14-tab-pill {
5072
+ min-height: 34px;
5073
+ padding: 7px 12px;
5074
+ border: 1px solid #c3d2e1;
5075
+ border-radius: 8px;
5076
+ background: linear-gradient(180deg, #f8fbff 0%, #edf4fa 100%);
5077
+ color: #4b6177;
5078
+ font-family: 'Sora', sans-serif;
5079
+ font-size: 0.78rem;
5080
+ font-weight: 800;
5081
+ box-shadow: none;
5082
+ }
5083
+
5084
+ .section14-tab-pill:hover,
5085
+ .section14-tab-pill:focus-visible {
5086
+ transform: translateY(-1px);
5087
+ border-color: #d06f00;
5088
+ color: #6c3900;
5089
+ }
5090
+
5091
+ .section14-tab-pill.is-active {
5092
+ border-color: #bf6500;
5093
+ background: linear-gradient(180deg, #ff9f31 0%, #e67900 100%);
5094
+ color: #ffffff;
5095
+ }
5096
+
5097
+ .section14-panel {
5098
+ border: 1px solid #dbe5ef;
5099
+ border-radius: 8px;
5100
+ background: #ffffff;
5101
+ padding: 12px;
5102
+ }
5103
+
5104
+ .section14-diagnostics-panel .dai-card {
5105
+ margin: 0;
5106
+ border: none;
5107
+ padding: 0;
5108
+ }
5109
+
5110
+ .section14-equations-panel {
5111
+ margin: 0;
5112
+ }
5113
+
5114
+ .section14-diagnostics-panel h4,
5115
+ .section14-test-card h4,
5116
+ .section14-equations-panel h4,
5117
+ .section14-table-head h4 {
5118
+ margin: 0;
5119
+ color: #3b4f64;
5120
+ font-family: 'Sora', sans-serif;
5121
+ font-size: 0.9rem;
5122
+ }
5123
+
5124
+ .section14-diagnostics-panel,
5125
+ .section14-tests-panel,
5126
+ .section14-table-panel {
5127
+ display: grid;
5128
+ gap: 10px;
5129
+ }
5130
+
5131
+ .section14-field-grid {
5132
+ display: grid;
5133
+ border: 1px solid #e1eaf2;
5134
+ border-radius: 8px;
5135
+ overflow: hidden;
5136
+ }
5137
+
5138
+ .section14-tests-grid {
5139
+ display: grid;
5140
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
5141
+ gap: 12px;
5142
+ }
5143
+
5144
+ .section14-test-card {
5145
+ display: grid;
5146
+ align-content: start;
5147
+ gap: 8px;
5148
+ padding: 10px;
5149
+ border: 1px solid #dbe6f1;
5150
+ border-radius: 8px;
5151
+ background: #fbfdff;
5152
+ }
5153
+
5154
+ .section14-test-card h4 {
5155
+ font-size: 0.82rem;
5156
+ }
5157
+
5158
+ .section14-test-card .section14-field-row {
5159
+ padding: 6px 8px;
5160
+ }
5161
+
5162
+ .section14-test-card .section14-field-label {
5163
+ font-size: 0.78rem;
5164
+ }
5165
+
5166
+ .section14-test-card .section14-field-value {
5167
+ font-size: 0.68rem;
5168
+ line-height: 1.25;
5169
+ }
5170
+
5171
+ .section14-field-row {
5172
+ display: flex;
5173
+ justify-content: space-between;
5174
+ gap: 12px;
5175
+ padding: 8px 10px;
5176
+ border-bottom: 1px solid #edf2f6;
5177
+ background: #ffffff;
5178
+ }
5179
+
5180
+ .section14-field-row:last-child {
5181
+ border-bottom: none;
5182
+ }
5183
+
5184
+ .section14-field-label {
5185
+ color: #586e84;
5186
+ font-weight: 700;
5187
+ }
5188
+
5189
+ .section14-field-value {
5190
+ color: #24384d;
5191
+ font-family: 'JetBrains Mono', monospace;
5192
+ font-size: 0.82rem;
5193
+ text-align: right;
5194
+ }
5195
+
5196
+ .section14-table-head {
5197
+ display: flex;
5198
+ align-items: center;
5199
+ justify-content: space-between;
5200
+ gap: 10px;
5201
+ flex-wrap: wrap;
5202
+ }
5203
+
5204
  .map-frame {
5205
  display: block;
5206
  width: 100%;
 
5785
  gap: 8px 10px;
5786
  }
5787
 
5788
+ .checkbox-inline-wrap-scrollable {
5789
+ max-height: 132px;
5790
+ overflow-y: auto;
5791
+ padding-right: 6px;
5792
+ align-content: flex-start;
5793
+ overscroll-behavior: contain;
5794
+ }
5795
+
5796
  .checkbox-inline-wrap-tools {
5797
  align-items: center;
5798
  margin-bottom: 6px;
 
5885
  align-items: flex-start;
5886
  justify-content: space-between;
5887
  gap: 8px;
5888
+ margin-bottom: 12px;
5889
  }
5890
 
5891
  .transform-suggestion-metrics {
5892
  display: flex;
5893
  flex-direction: column;
5894
  align-items: flex-end;
5895
+ gap: 5px;
5896
  }
5897
 
5898
  .transform-suggestion-rank {
 
5907
  background: #eaf4fe;
5908
  color: #1e4d78;
5909
  font-family: 'JetBrains Mono', monospace;
5910
+ font-size: 0.72rem;
5911
+ padding: 2px 6px;
5912
  font-weight: 700;
5913
  }
5914
 
 
5918
  background: #f4f8fc;
5919
  color: #355a78;
5920
  font-family: 'JetBrains Mono', monospace;
5921
+ font-size: 0.7rem;
5922
+ padding: 2px 6px;
5923
  font-weight: 700;
5924
  }
5925
 
 
5965
 
5966
  .section11-search-criteria {
5967
  margin-top: 8px;
5968
+ padding: 12px;
5969
+ border: 1px solid #d7e2ee;
5970
+ border-left: 4px solid #2f80cf;
5971
+ border-radius: 8px;
5972
+ background: linear-gradient(180deg, #fbfdff 0%, #f5f9fd 100%);
5973
  display: grid;
5974
  gap: 8px;
5975
  }
 
5986
  margin-bottom: 0;
5987
  }
5988
 
5989
+ .section11-suggestions-block {
5990
+ margin-top: 18px;
5991
+ padding-top: 16px;
5992
+ border-top: 1px solid #d7e2ee;
5993
+ display: grid;
5994
+ gap: 10px;
5995
+ }
5996
+
5997
+ .section11-suggestions-title {
5998
+ color: #3a4f64;
5999
+ font-family: 'Sora', sans-serif;
6000
+ font-size: 0.9rem;
6001
+ font-weight: 800;
6002
+ }
6003
+
6004
  .transform-preview-summary {
6005
  border: 1px solid #d8e5f2;
6006
  border-radius: 12px;
 
6146
  .transform-suggestion-col {
6147
  color: #2e4358;
6148
  font-weight: 700;
6149
+ font-size: 0.84rem;
6150
  min-width: 0;
6151
  overflow: hidden;
6152
  text-overflow: ellipsis;
 
6208
  color: #9d2f2f;
6209
  }
6210
 
6211
+ .transform-suggestion-card .grau-badge {
6212
+ font-size: 0.68rem;
6213
+ padding: 2px 6px;
6214
+ }
6215
+
6216
  .transform-suggestion-card button {
6217
  width: 100%;
6218
  justify-content: center;
 
6246
  gap: 12px;
6247
  }
6248
 
6249
+ .scatter-carousel {
6250
+ display: grid;
6251
+ gap: 10px;
6252
+ margin-top: 10px;
6253
+ padding: 12px;
6254
+ border: 1px solid #d7e2ee;
6255
+ border-radius: 8px;
6256
+ background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
6257
+ }
6258
+
6259
+ .scatter-carousel-controls {
6260
+ display: inline-flex;
6261
+ align-items: center;
6262
+ gap: 8px;
6263
+ }
6264
+
6265
+ .scatter-carousel-btn {
6266
+ min-width: 104px;
6267
+ }
6268
+
6269
+ .scatter-carousel-download-all {
6270
+ min-width: 190px;
6271
+ }
6272
+
6273
+ .scatter-carousel-actions {
6274
+ display: flex;
6275
+ justify-content: center;
6276
+ }
6277
+
6278
+ .scatter-carousel-pills {
6279
+ display: flex;
6280
+ align-items: center;
6281
+ justify-content: center;
6282
+ gap: 7px;
6283
+ flex-wrap: wrap;
6284
+ width: 100%;
6285
+ padding: 8px 0 10px;
6286
+ border-top: 1px solid #e4edf5;
6287
+ border-bottom: 1px solid #e4edf5;
6288
+ }
6289
+
6290
+ .scatter-carousel-pill {
6291
+ max-width: min(360px, 100%);
6292
+ min-height: 32px;
6293
+ padding: 6px 11px;
6294
+ border: 1px solid #c3d2e1;
6295
+ border-radius: 8px;
6296
+ background: linear-gradient(180deg, #f8fbff 0%, #edf4fa 100%);
6297
+ color: #5b7188;
6298
+ font-family: 'Sora', sans-serif;
6299
+ font-size: 0.74rem;
6300
+ font-weight: 800;
6301
+ line-height: 1.18;
6302
+ text-align: left;
6303
+ overflow: hidden;
6304
+ text-overflow: ellipsis;
6305
+ box-shadow: 0 3px 8px rgba(23, 37, 50, 0.1);
6306
+ }
6307
+
6308
+ .scatter-carousel-pill:hover,
6309
+ .scatter-carousel-pill:focus-visible {
6310
+ transform: translateY(-1px);
6311
+ border-color: #d06f00;
6312
+ color: #6c3900;
6313
+ }
6314
+
6315
+ .scatter-carousel-pill.is-active {
6316
+ border-color: #bf6500;
6317
+ background: linear-gradient(180deg, #ff9f31 0%, #e67900 100%);
6318
+ color: #ffffff;
6319
+ }
6320
+
6321
+ .scatter-carousel-track {
6322
+ display: grid;
6323
+ grid-template-columns: repeat(2, minmax(0, 1fr));
6324
+ gap: 12px;
6325
+ }
6326
+
6327
  .section-disclaimer-warning {
6328
  margin: 0 0 10px;
6329
  padding: 9px 12px;
 
6396
  .plot-card-head {
6397
  margin: 4px 4px 8px;
6398
  min-height: 42px;
6399
+ display: flex;
6400
+ align-items: flex-start;
6401
+ justify-content: space-between;
6402
+ gap: 10px;
6403
  position: relative;
6404
  z-index: 2;
6405
  }
6406
 
6407
+ .plot-card-head-main {
6408
+ min-width: 0;
6409
+ display: grid;
6410
+ gap: 2px;
6411
+ }
6412
+
6413
+ .plot-card-head-actions {
6414
+ display: inline-flex;
6415
+ align-items: center;
6416
+ justify-content: flex-end;
6417
+ gap: 8px;
6418
+ flex-wrap: wrap;
6419
+ flex-shrink: 0;
6420
+ }
6421
+
6422
+ .plot-card-download-btn {
6423
+ font-size: 0.76rem;
6424
+ padding: 6px 9px;
6425
+ }
6426
+
6427
  .plot-card-title {
6428
  margin: 0;
6429
  color: #2f465c;
 
6556
  margin-bottom: 10px;
6557
  }
6558
 
6559
+ .outliers-previous-card {
6560
+ display: grid;
6561
+ gap: 8px;
6562
+ padding: 10px 16px;
6563
+ border: 1px solid #dbe6f0;
6564
+ border-radius: 8px;
6565
+ background: #f8f9fa;
6566
+ }
6567
+
6568
+ .outliers-previous-count {
6569
+ color: #495057;
6570
+ font-weight: 700;
6571
+ }
6572
+
6573
+ .outliers-previous-indices {
6574
+ max-height: 92px;
6575
+ overflow-y: auto;
6576
+ overflow-wrap: anywhere;
6577
+ padding-right: 10px;
6578
+ color: #6c757d;
6579
+ font-size: 0.86rem;
6580
+ line-height: 1.35;
6581
+ }
6582
+
6583
  .outlier-actions-row {
6584
  display: flex;
6585
  flex-wrap: wrap;
 
6816
  margin-top: 8px;
6817
  }
6818
 
6819
+ .resumo-outliers-box-scrollable {
6820
+ max-height: 92px;
6821
+ overflow-y: auto;
6822
+ overflow-wrap: anywhere;
6823
+ padding-right: 12px;
6824
+ }
6825
+
6826
  .outliers-excluidos-details {
6827
  margin-top: 10px;
6828
  border: 1px solid #dbe6f1;
 
7392
  .micro-msg-grid-codigo {
7393
  grid-template-columns: repeat(4, minmax(0, 1fr));
7394
  gap: 6px 14px;
7395
+ max-height: 144px;
7396
+ overflow-y: auto;
7397
+ padding-right: 6px;
7398
+ align-content: start;
7399
+ overscroll-behavior: contain;
7400
  }
7401
 
7402
  .micro-msg {
 
7618
  grid-template-columns: 1fr;
7619
  }
7620
 
7621
+ .scatter-carousel-track {
7622
+ grid-template-columns: 1fr;
7623
+ }
7624
+
7625
+ .scatter-carousel-controls {
7626
+ width: 100%;
7627
+ }
7628
+
7629
+ .scatter-carousel-btn {
7630
+ flex: 1;
7631
+ }
7632
+
7633
  .plot-card {
7634
  min-height: 340px;
7635
  }