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

Refine diagnostic section layout

Browse files
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -711,6 +711,9 @@ function ScatterPlotCarousel({
711
  sectionFilePrefix,
712
  onDownloadFigure,
713
  onDownloadAll,
 
 
 
714
  loading = false,
715
  downloadingAssets = false,
716
  }) {
@@ -748,7 +751,9 @@ function ScatterPlotCarousel({
748
  }
749
 
750
  function buildFileNameBase(item, itemIndex) {
751
- return `${sectionFilePrefix}_${sanitizeFileName(item?.label, `dispersao_${itemIndex + 1}`)}`
 
 
752
  }
753
 
754
  return (
@@ -761,12 +766,12 @@ function ScatterPlotCarousel({
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)
@@ -786,28 +791,38 @@ function ScatterPlotCarousel({
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
@@ -969,17 +984,25 @@ function getDispersaoYLabel(eixoYTipo, eixoYResiduo, eixoYColuna, colunaYComRotu
969
  return `${colunaYComRotulo} transformado`
970
  }
971
 
972
- function DiagnosticPngCard({ title, pngPayload, alt }) {
973
  if (!pngPayload?.image_base64) {
974
  return (
975
  <div className="empty-box">Grafico indisponivel.</div>
976
  )
977
  }
978
  const mime = String(pngPayload.mime_type || 'image/png').trim() || 'image/png'
 
979
  return (
980
- <div className="plot-card plot-png-card">
981
  <div className="plot-card-head">
982
- <h4 className="plot-card-title">{title}</h4>
 
 
 
 
 
 
 
983
  </div>
984
  <img
985
  src={`data:${mime};base64,${pngPayload.image_base64}`}
@@ -5723,9 +5746,8 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
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}`}
@@ -5769,17 +5791,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
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">
@@ -5797,20 +5808,14 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
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>
@@ -5822,112 +5827,94 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5822
  Modo PNG automático para mais de {fit.graficos_diagnostico_limiar_png || 1500} pontos. Ao final da seção, podem ser gerados individualmente os gráficos interativos.
5823
  </div>
5824
  ) : null}
5825
- <div className="download-actions-bar">
5826
- <span className="download-actions-label">Fazer download:</span>
5827
- <button
5828
- type="button"
5829
- className="btn-download-subtle"
5830
- onClick={() => (
5831
- secao15DiagnosticoPng
5832
- ? onDownloadPngBase64(fit.grafico_obs_calc_png?.image_base64, fit.grafico_obs_calc_png?.mime_type, 'secao15_obs_calc')
5833
- : onDownloadFigurePng(fit.grafico_obs_calc, 'secao15_obs_calc')
5834
- )}
5835
- disabled={loading || downloadingAssets || (secao15DiagnosticoPng ? !fit.grafico_obs_calc_png?.image_base64 : !fit.grafico_obs_calc)}
5836
- >
5837
- Obs x calc
5838
- </button>
5839
- <button
5840
- type="button"
5841
- className="btn-download-subtle"
5842
- onClick={() => (
5843
- secao15DiagnosticoPng
5844
- ? onDownloadPngBase64(fit.grafico_residuos_png?.image_base64, fit.grafico_residuos_png?.mime_type, 'secao15_residuos')
5845
- : onDownloadFigurePng(fit.grafico_residuos, 'secao15_residuos')
5846
- )}
5847
- disabled={loading || downloadingAssets || (secao15DiagnosticoPng ? !fit.grafico_residuos_png?.image_base64 : !fit.grafico_residuos)}
5848
- >
5849
- Resíduos
5850
- </button>
5851
- <button
5852
- type="button"
5853
- className="btn-download-subtle"
5854
- onClick={() => (
5855
- secao15DiagnosticoPng
5856
- ? onDownloadPngBase64(fit.grafico_histograma_png?.image_base64, fit.grafico_histograma_png?.mime_type, 'secao15_histograma')
5857
- : onDownloadFigurePng(fit.grafico_histograma, 'secao15_histograma')
5858
- )}
5859
- disabled={loading || downloadingAssets || (secao15DiagnosticoPng ? !fit.grafico_histograma_png?.image_base64 : !fit.grafico_histograma)}
5860
- >
5861
- Histograma
5862
- </button>
5863
- <button
5864
- type="button"
5865
- className="btn-download-subtle"
5866
- onClick={() => (
5867
- secao15DiagnosticoPng
5868
- ? onDownloadPngBase64(fit.grafico_cook_png?.image_base64, fit.grafico_cook_png?.mime_type, 'secao15_cook')
5869
- : onDownloadFigurePng(fit.grafico_cook, 'secao15_cook', { forceHideLegend: true })
5870
- )}
5871
- disabled={loading || downloadingAssets || (secao15DiagnosticoPng ? !fit.grafico_cook_png?.image_base64 : !fit.grafico_cook)}
5872
- >
5873
- Cook
5874
- </button>
5875
- <button
5876
- type="button"
5877
- className="btn-download-subtle"
5878
- onClick={() => onDownloadFigurePng(fit.grafico_correlacao, 'secao15_correlacao')}
5879
- disabled={loading || downloadingAssets || !fit.grafico_correlacao}
5880
- >
5881
- Correlação
5882
- </button>
5883
- <button
5884
- type="button"
5885
- className="btn-download-subtle"
5886
- onClick={() => {
5887
- if (secao15DiagnosticoPng) {
5888
- void onDownloadPngBase64Batch([
5889
  { imageBase64: fit.grafico_obs_calc_png?.image_base64, mimeType: fit.grafico_obs_calc_png?.mime_type, fileNameBase: 'secao15_obs_calc' },
5890
  { imageBase64: fit.grafico_residuos_png?.image_base64, mimeType: fit.grafico_residuos_png?.mime_type, fileNameBase: 'secao15_residuos' },
5891
  { imageBase64: fit.grafico_histograma_png?.image_base64, mimeType: fit.grafico_histograma_png?.mime_type, fileNameBase: 'secao15_histograma' },
5892
  { imageBase64: fit.grafico_cook_png?.image_base64, mimeType: fit.grafico_cook_png?.mime_type, fileNameBase: 'secao15_cook' },
5893
- ])
5894
- return
5895
- }
5896
- void onDownloadFiguresPngBatch([
5897
- { figure: fit.grafico_obs_calc, fileNameBase: 'secao15_obs_calc' },
5898
- { figure: fit.grafico_residuos, fileNameBase: 'secao15_residuos' },
5899
- { figure: fit.grafico_histograma, fileNameBase: 'secao15_histograma' },
5900
- { figure: fit.grafico_cook, fileNameBase: 'secao15_cook', forceHideLegend: true },
5901
- { figure: fit.grafico_correlacao, fileNameBase: 'secao15_correlacao' },
5902
- ])
5903
  }}
5904
- disabled={loading || downloadingAssets || (
5905
- secao15DiagnosticoPng
5906
- ? (!fit.grafico_obs_calc_png?.image_base64 && !fit.grafico_residuos_png?.image_base64 && !fit.grafico_histograma_png?.image_base64 && !fit.grafico_cook_png?.image_base64)
5907
- : (!fit.grafico_obs_calc && !fit.grafico_residuos && !fit.grafico_histograma && !fit.grafico_cook && !fit.grafico_correlacao)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5908
  )}
5909
- >
5910
- Todos
5911
- </button>
5912
- </div>
5913
- {secao15DiagnosticoPng ? (
5914
- <div className="plot-grid-2-fixed">
5915
- <DiagnosticPngCard title="Obs x Calc" pngPayload={fit.grafico_obs_calc_png} alt="Obs x Calc em PNG" />
5916
- <DiagnosticPngCard title="Resíduos" pngPayload={fit.grafico_residuos_png} alt="Resíduos em PNG" />
5917
- <DiagnosticPngCard title="Histograma" pngPayload={fit.grafico_histograma_png} alt="Histograma em PNG" />
5918
- <DiagnosticPngCard title="Cook" pngPayload={fit.grafico_cook_png} alt="Cook em PNG" />
5919
- </div>
5920
  ) : (
5921
- <div className="plot-grid-2-fixed">
5922
- <PlotFigure figure={fit.grafico_obs_calc} indexedFigure={fit.grafico_obs_calc_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('obs_calc')} title="Obs x Calc" showPointIndexToggle />
5923
- <PlotFigure figure={fit.grafico_residuos} indexedFigure={fit.grafico_residuos_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('residuos')} title="Resíduos" showPointIndexToggle />
5924
- <PlotFigure figure={fit.grafico_histograma} indexedFigure={fit.grafico_histograma_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('histograma')} title="Histograma" showPointIndexToggle />
5925
- <PlotFigure figure={fit.grafico_cook} indexedFigure={fit.grafico_cook_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('cook')} title="Cook" showPointIndexToggle forceHideLegend />
5926
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5927
  )}
5928
- <div className="plot-full-width">
5929
- <PlotFigure figure={fit.grafico_correlacao} title="Matriz de correlação" showPointIndexToggle className="plot-correlation-card" />
5930
- </div>
5931
  {secao15DiagnosticoPng ? (
5932
  <>
5933
  <div className="scatter-interactive-control">
@@ -5946,23 +5933,6 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5946
  </div>
5947
  {secao15InterativoSelecionado !== 'none' ? (
5948
  <>
5949
- <div className="download-actions-bar">
5950
- <button
5951
- type="button"
5952
- className="btn-download-subtle"
5953
- onClick={() => {
5954
- if (!secao15InterativoFigura) return
5955
- void onDownloadFigurePng(
5956
- secao15InterativoFigura,
5957
- `secao15_${sanitizeFileName(secao15InterativoLabel, 'diagnostico_interativo')}`,
5958
- { forceHideLegend: secao15InterativoSelecionado === 'cook' },
5959
- )
5960
- }}
5961
- disabled={loading || downloadingAssets || !secao15InterativoFigura}
5962
- >
5963
- Fazer download
5964
- </button>
5965
- </div>
5966
  {secao15InterativoFigura ? (
5967
  <PlotFigure
5968
  key={`s15-interativo-${secao15InterativoSelecionado}`}
@@ -5974,6 +5944,23 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5974
  forceHideLegend={secao15InterativoSelecionado === 'cook'}
5975
  className="plot-stretch"
5976
  lazy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5977
  />
5978
  ) : (
5979
  <div className="empty-box">Grafico indisponivel.</div>
 
711
  sectionFilePrefix,
712
  onDownloadFigure,
713
  onDownloadAll,
714
+ renderItem = null,
715
+ downloadAllLabel = 'Baixar todos os gráficos',
716
+ pillsAriaLabel = 'Escolher dupla de gráficos',
717
  loading = false,
718
  downloadingAssets = false,
719
  }) {
 
751
  }
752
 
753
  function buildFileNameBase(item, itemIndex) {
754
+ if (item?.fileNameBase) return sanitizeFileName(item.fileNameBase, `grafico_${itemIndex + 1}`)
755
+ const prefix = sectionFilePrefix ? `${sectionFilePrefix}_` : ''
756
+ return `${prefix}${sanitizeFileName(item?.label, `dispersao_${itemIndex + 1}`)}`
757
  }
758
 
759
  return (
 
766
  onClick={onDownloadAll}
767
  disabled={loading || downloadingAssets || safeItems.length === 0}
768
  >
769
+ {downloadAllLabel}
770
  </button>
771
  </div>
772
  ) : null}
773
  {totalPages > 1 ? (
774
+ <div className="scatter-carousel-pills" aria-label={pillsAriaLabel}>
775
  {Array.from({ length: totalPages }, (_, pageIndex) => {
776
  const pairStart = (pageIndex * pageSize) + 1
777
  const pairEnd = Math.min(pairStart + pageSize - 1, safeItems.length)
 
791
  })}
792
  </div>
793
  ) : null}
794
+ <div className={`scatter-carousel-track${visibleItems.length === 1 ? ' is-single' : ''}`}>
795
  {visibleItems.map((item, visibleIndex) => {
796
  const itemIndex = startIndex + visibleIndex
797
  const fileNameBase = buildFileNameBase(item, itemIndex)
798
+ if (typeof renderItem === 'function') {
799
+ return (
800
+ <React.Fragment key={`${itemKeyPrefix}-${item.id || itemIndex}`}>
801
+ {renderItem({ item, itemIndex, fileNameBase })}
802
+ </React.Fragment>
803
+ )
804
+ }
805
+ const forceHideLegend = item.forceHideLegend ?? true
806
+ const showPointIndexToggle = item.showPointIndexToggle ?? true
807
+ const downloadOptions = item.downloadOptions || { forceHideLegend }
808
  return (
809
  <PlotFigure
810
+ key={`${itemKeyPrefix}-${item.id || itemIndex}`}
811
  figure={item.figure}
812
+ indexedFigure={item.indexedFigure || indexedFigureMap?.get(String(item.label || '').trim()) || null}
813
+ onRequestIndexedFigure={item.onRequestIndexedFigure || onRequestIndexedFigure}
814
  title={item.title}
815
  subtitle={item.subtitle}
816
+ showPointIndexToggle={showPointIndexToggle}
817
+ forceHideLegend={forceHideLegend}
818
+ className={item.className || 'plot-stretch'}
819
  lazy
820
  headerActions={typeof onDownloadFigure === 'function' ? (
821
  <button
822
  type="button"
823
  className="btn-download-subtle plot-card-download-btn"
824
  title={item.legenda || item.label || 'Fazer download'}
825
+ onClick={() => onDownloadFigure(item.figure, fileNameBase, downloadOptions)}
826
  disabled={loading || downloadingAssets || !item.figure}
827
  >
828
  Fazer download
 
984
  return `${colunaYComRotulo} transformado`
985
  }
986
 
987
+ function DiagnosticPngCard({ title, pngPayload, alt, headerActions = null, className = '' }) {
988
  if (!pngPayload?.image_base64) {
989
  return (
990
  <div className="empty-box">Grafico indisponivel.</div>
991
  )
992
  }
993
  const mime = String(pngPayload.mime_type || 'image/png').trim() || 'image/png'
994
+ const cardClassName = `plot-card plot-png-card ${className}`.trim()
995
  return (
996
+ <div className={cardClassName}>
997
  <div className="plot-card-head">
998
+ <div className="plot-card-head-main">
999
+ <h4 className="plot-card-title">{title}</h4>
1000
+ </div>
1001
+ {headerActions ? (
1002
+ <div className="plot-card-head-actions">
1003
+ {headerActions}
1004
+ </div>
1005
+ ) : null}
1006
  </div>
1007
  <img
1008
  src={`data:${mime};base64,${pngPayload.image_base64}`}
 
5746
  {[
5747
  { id: 'diagnosticos', label: 'Diagnósticos' },
5748
  { id: 'testes', label: 'Testes' },
 
5749
  { id: 'coeficientes', label: 'Coeficientes' },
5750
+ { id: 'equacoes', label: 'Equações' },
5751
  ].map((tab) => (
5752
  <button
5753
  key={`sec14-tab-${tab.id}`}
 
5791
  )
5792
  ) : null}
5793
 
 
 
 
 
 
 
 
 
 
 
 
5794
  {section14Tab === 'coeficientes' ? (
5795
  <div className="section14-table-panel">
5796
  <div className="section14-table-head">
 
5808
  </div>
5809
  ) : null}
5810
 
5811
+ {section14Tab === 'equacoes' ? (
5812
+ <div className="equation-formats-section section14-equations-panel">
5813
+ <h4>Equações do Modelo</h4>
5814
+ <EquationFormatsPanel
5815
+ equacoes={fit.equacoes}
5816
+ onDownload={(mode) => void onDownloadEquacao(mode)}
5817
+ disabled={loading || downloadingAssets}
5818
+ />
 
 
 
 
 
 
5819
  </div>
5820
  ) : null}
5821
  </div>
 
5827
  Modo PNG automático para mais de {fit.graficos_diagnostico_limiar_png || 1500} pontos. Ao final da seção, podem ser gerados individualmente os gráficos interativos.
5828
  </div>
5829
  ) : null}
5830
+ {secao15DiagnosticoPng ? (
5831
+ <ScatterPlotCarousel
5832
+ items={[
5833
+ { id: 'obs_calc_png', type: 'png', label: 'Obs x Calc', title: 'Obs x Calc', pngPayload: fit.grafico_obs_calc_png, fileNameBase: 'secao15_obs_calc' },
5834
+ { id: 'residuos_png', type: 'png', label: 'Resíduos', title: 'Resíduos', pngPayload: fit.grafico_residuos_png, fileNameBase: 'secao15_residuos' },
5835
+ { id: 'histograma_png', type: 'png', label: 'Histograma', title: 'Histograma', pngPayload: fit.grafico_histograma_png, fileNameBase: 'secao15_histograma' },
5836
+ { id: 'cook_png', type: 'png', label: 'Cook', title: 'Cook', pngPayload: fit.grafico_cook_png, fileNameBase: 'secao15_cook' },
5837
+ { id: 'correlacao', type: 'figure', label: 'Correlação', title: 'Matriz de correlação', figure: fit.grafico_correlacao, fileNameBase: 'secao15_correlacao', className: 'plot-stretch plot-correlation-card' },
5838
+ ]}
5839
+ itemKeyPrefix="s15-png"
5840
+ onDownloadAll={() => {
5841
+ void (async () => {
5842
+ const pngDownloads = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5843
  { imageBase64: fit.grafico_obs_calc_png?.image_base64, mimeType: fit.grafico_obs_calc_png?.mime_type, fileNameBase: 'secao15_obs_calc' },
5844
  { imageBase64: fit.grafico_residuos_png?.image_base64, mimeType: fit.grafico_residuos_png?.mime_type, fileNameBase: 'secao15_residuos' },
5845
  { imageBase64: fit.grafico_histograma_png?.image_base64, mimeType: fit.grafico_histograma_png?.mime_type, fileNameBase: 'secao15_histograma' },
5846
  { imageBase64: fit.grafico_cook_png?.image_base64, mimeType: fit.grafico_cook_png?.mime_type, fileNameBase: 'secao15_cook' },
5847
+ ]
5848
+ if (pngDownloads.some((item) => String(item.imageBase64 || '').trim())) {
5849
+ await onDownloadPngBase64Batch(pngDownloads)
5850
+ }
5851
+ if (fit.grafico_correlacao) {
5852
+ await onDownloadFigurePng(fit.grafico_correlacao, 'secao15_correlacao')
5853
+ }
5854
+ })()
 
 
5855
  }}
5856
+ renderItem={({ item, fileNameBase }) => (
5857
+ item.type === 'png' ? (
5858
+ <DiagnosticPngCard
5859
+ title={item.title}
5860
+ pngPayload={item.pngPayload}
5861
+ alt={`${item.title} em PNG`}
5862
+ className="plot-stretch"
5863
+ headerActions={(
5864
+ <button
5865
+ type="button"
5866
+ className="btn-download-subtle plot-card-download-btn"
5867
+ onClick={() => onDownloadPngBase64(item.pngPayload?.image_base64, item.pngPayload?.mime_type, fileNameBase)}
5868
+ disabled={loading || downloadingAssets || !item.pngPayload?.image_base64}
5869
+ >
5870
+ Fazer download
5871
+ </button>
5872
+ )}
5873
+ />
5874
+ ) : (
5875
+ <PlotFigure
5876
+ figure={item.figure}
5877
+ title={item.title}
5878
+ className={item.className}
5879
+ lazy
5880
+ headerActions={(
5881
+ <button
5882
+ type="button"
5883
+ className="btn-download-subtle plot-card-download-btn"
5884
+ onClick={() => onDownloadFigurePng(item.figure, fileNameBase)}
5885
+ disabled={loading || downloadingAssets || !item.figure}
5886
+ >
5887
+ Fazer download
5888
+ </button>
5889
+ )}
5890
+ />
5891
+ )
5892
  )}
5893
+ loading={loading}
5894
+ downloadingAssets={downloadingAssets}
5895
+ />
 
 
 
 
 
 
 
 
5896
  ) : (
5897
+ <ScatterPlotCarousel
5898
+ items={[
5899
+ { id: 'obs_calc', label: 'Obs x Calc', title: 'Obs x Calc', figure: fit.grafico_obs_calc, indexedFigure: fit.grafico_obs_calc_com_indices || null, onRequestIndexedFigure: () => ensureSecao15GraficoComIndices('obs_calc'), fileNameBase: 'secao15_obs_calc', forceHideLegend: false, downloadOptions: { forceHideLegend: false } },
5900
+ { id: 'residuos', label: 'Resíduos', title: 'Resíduos', figure: fit.grafico_residuos, indexedFigure: fit.grafico_residuos_com_indices || null, onRequestIndexedFigure: () => ensureSecao15GraficoComIndices('residuos'), fileNameBase: 'secao15_residuos', forceHideLegend: false, downloadOptions: { forceHideLegend: false } },
5901
+ { id: 'histograma', label: 'Histograma', title: 'Histograma', figure: fit.grafico_histograma, indexedFigure: fit.grafico_histograma_com_indices || null, onRequestIndexedFigure: () => ensureSecao15GraficoComIndices('histograma'), fileNameBase: 'secao15_histograma', forceHideLegend: false, downloadOptions: { forceHideLegend: false } },
5902
+ { id: 'cook', label: 'Cook', title: 'Cook', figure: fit.grafico_cook, indexedFigure: fit.grafico_cook_com_indices || null, onRequestIndexedFigure: () => ensureSecao15GraficoComIndices('cook'), fileNameBase: 'secao15_cook', forceHideLegend: true, downloadOptions: { forceHideLegend: true } },
5903
+ { id: 'correlacao', label: 'Correlação', title: 'Matriz de correlação', figure: fit.grafico_correlacao, fileNameBase: 'secao15_correlacao', forceHideLegend: false, showPointIndexToggle: false, className: 'plot-stretch plot-correlation-card', downloadOptions: { forceHideLegend: false } },
5904
+ ]}
5905
+ itemKeyPrefix="s15-plot"
5906
+ onDownloadFigure={onDownloadFigurePng}
5907
+ onDownloadAll={() => onDownloadFiguresPngBatch([
5908
+ { figure: fit.grafico_obs_calc, fileNameBase: 'secao15_obs_calc', forceHideLegend: false },
5909
+ { figure: fit.grafico_residuos, fileNameBase: 'secao15_residuos', forceHideLegend: false },
5910
+ { figure: fit.grafico_histograma, fileNameBase: 'secao15_histograma', forceHideLegend: false },
5911
+ { figure: fit.grafico_cook, fileNameBase: 'secao15_cook', forceHideLegend: true },
5912
+ { figure: fit.grafico_correlacao, fileNameBase: 'secao15_correlacao', forceHideLegend: false },
5913
+ ])}
5914
+ loading={loading}
5915
+ downloadingAssets={downloadingAssets}
5916
+ />
5917
  )}
 
 
 
5918
  {secao15DiagnosticoPng ? (
5919
  <>
5920
  <div className="scatter-interactive-control">
 
5933
  </div>
5934
  {secao15InterativoSelecionado !== 'none' ? (
5935
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5936
  {secao15InterativoFigura ? (
5937
  <PlotFigure
5938
  key={`s15-interativo-${secao15InterativoSelecionado}`}
 
5944
  forceHideLegend={secao15InterativoSelecionado === 'cook'}
5945
  className="plot-stretch"
5946
  lazy
5947
+ headerActions={(
5948
+ <button
5949
+ type="button"
5950
+ className="btn-download-subtle plot-card-download-btn"
5951
+ onClick={() => {
5952
+ if (!secao15InterativoFigura) return
5953
+ void onDownloadFigurePng(
5954
+ secao15InterativoFigura,
5955
+ `secao15_${sanitizeFileName(secao15InterativoLabel, 'diagnostico_interativo')}`,
5956
+ { forceHideLegend: secao15InterativoSelecionado === 'cook' },
5957
+ )
5958
+ }}
5959
+ disabled={loading || downloadingAssets || !secao15InterativoFigura}
5960
+ >
5961
+ Fazer download
5962
+ </button>
5963
+ )}
5964
  />
5965
  ) : (
5966
  <div className="empty-box">Grafico indisponivel.</div>
frontend/src/styles.css CHANGED
@@ -5156,7 +5156,9 @@ button.btn-upload-select {
5156
  }
5157
 
5158
  .section14-test-card .section14-field-row {
5159
- padding: 6px 8px;
 
 
5160
  }
5161
 
5162
  .section14-test-card .section14-field-label {
@@ -5168,6 +5170,13 @@ button.btn-upload-select {
5168
  line-height: 1.25;
5169
  }
5170
 
 
 
 
 
 
 
 
5171
  .section14-field-row {
5172
  display: flex;
5173
  justify-content: space-between;
@@ -6324,6 +6333,10 @@ button.btn-upload-select {
6324
  gap: 12px;
6325
  }
6326
 
 
 
 
 
6327
  .section-disclaimer-warning {
6328
  margin: 0 0 10px;
6329
  padding: 9px 12px;
 
5156
  }
5157
 
5158
  .section14-test-card .section14-field-row {
5159
+ padding: 4px 0;
5160
+ border-bottom: none;
5161
+ background: transparent;
5162
  }
5163
 
5164
  .section14-test-card .section14-field-label {
 
5170
  line-height: 1.25;
5171
  }
5172
 
5173
+ .section14-test-card .section14-field-grid {
5174
+ gap: 4px;
5175
+ border: none;
5176
+ border-radius: 0;
5177
+ overflow: visible;
5178
+ }
5179
+
5180
  .section14-field-row {
5181
  display: flex;
5182
  justify-content: space-between;
 
6333
  gap: 12px;
6334
  }
6335
 
6336
+ .scatter-carousel-track.is-single > * {
6337
+ grid-column: 1 / -1;
6338
+ }
6339
+
6340
  .section-disclaimer-warning {
6341
  margin: 0 0 10px;
6342
  padding: 9px 12px;