Spaces:
Running
Running
| import React, { useEffect, useRef, useState } from 'react' | |
| import Plot from 'react-plotly.js' | |
| import Plotly from 'plotly.js-dist-min' | |
| function PlotFigure({ | |
| figure, | |
| indexedFigure = null, | |
| onRequestIndexedFigure = null, | |
| title, | |
| subtitle = '', | |
| forceHideLegend = false, | |
| className = '', | |
| lazy = false, | |
| showPointIndexToggle = false, | |
| headerActions = null, | |
| trailingHeaderActions = null, | |
| revisionKey = '', | |
| }) { | |
| const containerRef = useRef(null) | |
| const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy) | |
| const [showPointIndices, setShowPointIndices] = useState(false) | |
| const [loadingIndexedFigure, setLoadingIndexedFigure] = useState(false) | |
| const figureResetKey = revisionKey ? String(revisionKey) : figure | |
| useEffect(() => { | |
| if (!lazy) { | |
| setShouldRenderPlot(true) | |
| return undefined | |
| } | |
| if (shouldRenderPlot) return undefined | |
| if (typeof window === 'undefined' || !('IntersectionObserver' in window)) { | |
| setShouldRenderPlot(true) | |
| return undefined | |
| } | |
| const target = containerRef.current | |
| if (!target) return undefined | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| const isVisible = entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0) | |
| if (isVisible) { | |
| setShouldRenderPlot(true) | |
| observer.disconnect() | |
| } | |
| }, | |
| { rootMargin: '240px 0px', threshold: 0.01 }, | |
| ) | |
| observer.observe(target) | |
| return () => observer.disconnect() | |
| }, [lazy, shouldRenderPlot]) | |
| useEffect(() => { | |
| setShowPointIndices(false) | |
| setLoadingIndexedFigure(false) | |
| }, [figureResetKey]) | |
| if (!figure) { | |
| return <div className="empty-box">Grafico indisponivel.</div> | |
| } | |
| const hasIndexedFigure = Boolean(indexedFigure) | |
| const canRequestIndexedFigure = typeof onRequestIndexedFigure === 'function' | |
| const canToggleIndices = hasIndexedFigure || canRequestIndexedFigure | |
| const activeFigure = showPointIndices && hasIndexedFigure ? indexedFigure : figure | |
| const safeFigure = activeFigure || { data: [], layout: {} } | |
| const baseLayout = safeFigure.layout || {} | |
| const { width: _ignoreWidth, ...layoutNoWidth } = baseLayout | |
| const safeAnnotations = Array.isArray(baseLayout.annotations) | |
| ? baseLayout.annotations.map((annotation) => { | |
| const { ax, ay, axref, ayref, arrowhead, arrowsize, arrowwidth, arrowcolor, standoff, ...clean } = annotation || {} | |
| return { ...clean, showarrow: false } | |
| }) | |
| : baseLayout.annotations | |
| const data = (safeFigure.data || []).map((trace) => ( | |
| forceHideLegend ? { ...trace, showlegend: false } : { ...trace } | |
| )) | |
| const layout = { | |
| ...layoutNoWidth, | |
| autosize: true, | |
| annotations: Array.isArray(safeAnnotations) ? safeAnnotations : [], | |
| margin: baseLayout.margin || { t: 40, r: 20, b: 50, l: 50 }, | |
| } | |
| if (forceHideLegend) { | |
| layout.showlegend = false | |
| } | |
| const plotHeight = layout.height ? `${layout.height}px` : '100%' | |
| const plotRevision = `${showPointIndices && hasIndexedFigure ? 'indices' : 'base'}:${String(revisionKey || '')}` | |
| const cardClassName = `plot-card ${className}`.trim() | |
| async function handleToggleChange(event) { | |
| const checked = Boolean(event.target.checked) | |
| if (!checked) { | |
| setShowPointIndices(false) | |
| return | |
| } | |
| if (hasIndexedFigure) { | |
| setShowPointIndices(true) | |
| return | |
| } | |
| if (!canRequestIndexedFigure || loadingIndexedFigure) return | |
| setLoadingIndexedFigure(true) | |
| try { | |
| const loadedFigure = await onRequestIndexedFigure() | |
| setShowPointIndices(Boolean(loadedFigure)) | |
| } catch { | |
| setShowPointIndices(false) | |
| } finally { | |
| setLoadingIndexedFigure(false) | |
| } | |
| } | |
| const toggleTitle = loadingIndexedFigure | |
| ? 'Carregando INDICES dos pontos...' | |
| : hasIndexedFigure | |
| ? 'Exibir INDICES dos pontos' | |
| : canRequestIndexedFigure | |
| ? 'Carregar INDICES dos pontos sob demanda' | |
| : 'Este gráfico não trouxe uma versão com INDICES.' | |
| return ( | |
| <div ref={containerRef} className={cardClassName}> | |
| {title || subtitle || showPointIndexToggle || headerActions || trailingHeaderActions ? ( | |
| <div className="plot-card-head"> | |
| <div className="plot-card-head-main"> | |
| {title ? <h4 className="plot-card-title">{title}</h4> : null} | |
| {subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null} | |
| </div> | |
| <div className="plot-card-head-actions"> | |
| {headerActions} | |
| {showPointIndexToggle ? ( | |
| <label className={`plot-card-toggle${canToggleIndices ? '' : ' is-disabled'}`}> | |
| <input | |
| type="checkbox" | |
| checked={showPointIndices} | |
| disabled={!canToggleIndices || loadingIndexedFigure} | |
| onChange={handleToggleChange} | |
| title={toggleTitle} | |
| /> | |
| {loadingIndexedFigure ? 'Carregando...' : 'INDICES'} | |
| </label> | |
| ) : null} | |
| {trailingHeaderActions} | |
| </div> | |
| </div> | |
| ) : null} | |
| {shouldRenderPlot ? ( | |
| <div className="plot-card-body"> | |
| <Plot | |
| data={data} | |
| layout={layout} | |
| revision={plotRevision} | |
| config={{ responsive: true, displaylogo: false }} | |
| style={{ width: '100%', height: plotHeight, minHeight: '320px' }} | |
| useResizeHandler | |
| plotly={Plotly} | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="plot-lazy-placeholder">Carregando gráfico...</div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| export default PlotFigure | |