"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import type { SurfaceData, ImbalanceMetrics } from "@/lib/depth-map-utils"; /** * Order Book Depth Map — Plotly.js 3D Surface * * Single unified surface: X = distance from mid (bps, -50 to +50), * Y = snapshot index (oldest → newest), Z = resting size (BTC). * * Warm amber/gold colorscale on dark background. * Live imbalance overlay with formula + wall detection. */ interface Props { surface: SurfaceData | null; metrics: ImbalanceMetrics | null; height?: number; } const COLORSCALE = [ [0, "rgb(8,8,18)"], [0.2, "rgb(18,18,48)"], [0.4, "rgb(50,25,90)"], [0.6, "rgb(140,60,30)"], [0.75, "rgb(210,110,30)"], [0.88, "rgb(245,170,45)"], [0.96, "rgb(255,220,100)"], [1, "rgb(255,245,190)"], ]; export function DepthMapPlotly({ surface, metrics, height = 440 }: Props) { const containerRef = useRef(null); const plotlyReady = useRef(false); const [loaded, setLoaded] = useState(false); // Load Plotly CDN once useEffect(() => { if ((window as any).Plotly) { setLoaded(true); return; } const s = document.createElement("script"); s.src = "https://cdn.plot.ly/plotly-3.0.0.min.js"; s.async = true; s.onload = () => setLoaded(true); document.head.appendChild(s); return () => { s.remove(); }; }, []); // Render / update chart useEffect(() => { if (!containerRef.current || !loaded || !surface) return; const Plotly = (window as any).Plotly; if (!Plotly) return; const cw = containerRef.current.clientWidth || 800; // Build trace — ensure no NaN/Infinity values const cleanZ = surface.z.map(row => row.map(v => (isFinite(v) && v > 0 ? v : 0)) ); const trace = { type: "surface", x: surface.x, y: surface.y, z: cleanZ, colorscale: COLORSCALE, contours: { z: { show: true, usecolormap: true, highlightcolor: "rgba(255,255,255,0.25)", project: { z: true }, }, }, lighting: { ambient: 0.5, diffuse: 0.7, specular: 0.25, roughness: 0.45, fresnel: 0.15, }, lightposition: { x: 150, y: 250, z: 350 }, showscale: true, colorbar: { title: { text: "Resting Size", font: { color: "#999", size: 10 } }, tickfont: { color: "#777", size: 8 }, thickness: 14, len: 0.65, x: 1.02, }, }; const layout: any = { title: { text: "ORDER BOOK IMBALANCE • BTC-USD-PERP", font: { size: 12, color: "#ccc", family: "Inter, sans-serif" }, x: 0.03, y: 0.98, }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", scene: { xaxis: { title: { text: "Distance from Mid (bps)", font: { size: 9, color: "#666" } }, gridcolor: "rgba(255,255,255,0.04)", zerolinecolor: "rgba(255,255,255,0.12)", tickfont: { size: 8, color: "#555" }, range: [-55, 55], }, yaxis: { title: { text: "Snapshot Index (oldest → newest)", font: { size: 9, color: "#666" } }, gridcolor: "rgba(255,255,255,0.04)", tickfont: { size: 8, color: "#555" }, }, zaxis: { title: { text: "Size (BTC)", font: { size: 9, color: "#666" } }, gridcolor: "rgba(255,255,255,0.04)", tickfont: { size: 8, color: "#555" }, }, camera: { eye: { x: 1.5, y: 1.2, z: 0.95 }, center: { x: 0, y: 0, z: -0.08 }, }, aspectmode: "manual", aspectratio: { x: 1.5, y: 1.0, z: 0.55 }, bgcolor: "rgba(0,0,0,0)", }, margin: { l: 0, r: 30, t: 32, b: 0 }, uirevision: "obi-surface-v2", autosize: true, font: { color: "#888" }, }; const config = { displayModeBar: true, modeBarButtonsToRemove: ["sendDataToCloud", "zoom2d", "pan2d", "select2d", "lasso2d", "autoScale2d"], displaylogo: false, responsive: true, }; if (plotlyReady.current) { Plotly.react(containerRef.current, [trace], layout, config); } else { Plotly.newPlot(containerRef.current, [trace], layout, config); plotlyReady.current = true; } }, [surface, loaded]); // Resize on container width change useEffect(() => { const obs = new ResizeObserver(() => { const Plotly = (window as any).Plotly; if (containerRef.current && Plotly) { Plotly.Plots.resize(containerRef.current); } }); if (containerRef.current) obs.observe(containerRef.current); return () => obs.disconnect(); }, []); return (
{/* Imbalance Overlay */} {metrics && (

Live Imbalance

0.005 ? "text-green-400" : metrics.imbalance < -0.005 ? "text-red-400" : "text-zinc-400"}`} > {metrics.imbalance > 0 ? "+" : ""}{metrics.imbalance.toFixed(3)}
{metrics.wallSide !== "none" && (

Wall: {metrics.wallSide.toUpperCase()}S ({(metrics.wallStrength ?? 0).toFixed(1)})

)}
= 0 ? "bg-green-500" : "bg-red-500"}`} style={{ width: `${Math.min(Math.abs(metrics.imbalance) * 350, 100)}%`, marginLeft: metrics.imbalance >= 0 ? "50%" : `${50 - Math.min(Math.abs(metrics.imbalance) * 350, 100) / 2}%`, }} />

I = (Vb − Va) / (Vb + Va)

)}
); }