Spaces:
Paused
Paused
| /** | |
| * Lazy-loaded Plotly chart. | |
| * Plotly.js is ~3 MB; we import the minified dist to keep bundles lean. | |
| */ | |
| import { useEffect, useRef } from "react"; | |
| import type { PlotlySpec } from "../types"; | |
| interface Props { | |
| spec: PlotlySpec; | |
| } | |
| export default function PlotlyChart({ spec }: Props) { | |
| const divRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| if (!divRef.current) return; | |
| // Dynamic import keeps Plotly out of the initial bundle | |
| import("plotly.js-dist-min").then((Plotly) => { | |
| // Strip hardcoded width/height from AI-generated specs — they break | |
| // responsive layout by overriding autosize. We control sizing via CSS. | |
| const { width: _w, height: _h, ...restLayout } = spec.layout; | |
| // Enforce minimum safe margins — use Math.max so spec can increase but | |
| // not shrink below the floor. Zero margins on radial charts (sunburst, | |
| // polar) clip outer-ring labels inside the SVG viewport. | |
| const specMargin = (restLayout.margin ?? {}) as Record<string, number>; | |
| const margin = { | |
| l: Math.max(60, specMargin.l ?? 0), | |
| r: Math.max(100, specMargin.r ?? 0), | |
| t: Math.max(60, specMargin.t ?? 0), | |
| b: Math.max(60, specMargin.b ?? 0), | |
| }; | |
| Plotly.newPlot( | |
| divRef.current!, | |
| spec.data, | |
| { | |
| ...restLayout, | |
| autosize: true, | |
| margin, | |
| paper_bgcolor: "transparent", | |
| plot_bgcolor: "#f8fafc", | |
| font: { family: "Inter, sans-serif", size: 12 }, | |
| }, | |
| { responsive: true, displayModeBar: "hover" } | |
| ); | |
| }); | |
| return () => { | |
| import("plotly.js-dist-min").then((Plotly) => { | |
| if (divRef.current) Plotly.purge(divRef.current); | |
| }); | |
| }; | |
| }, [spec]); | |
| return ( | |
| <div className="mt-4 rounded-xl border border-slate-200 bg-white"> | |
| <div ref={divRef} style={{ width: "100%", height: 580 }} /> | |
| </div> | |
| ); | |
| } | |