/** * 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(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; 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 (
); }