"use client"; import { useEffect, useRef, useCallback } from "react"; import type { SurfaceData, ImbalanceMetrics } from "@/lib/depth-map-utils"; /** * Order Book Depth Map — Plotly.js 3D Surface * * Renders a professional quant-grade depth map with: * - 3D surface: X=distance from mid (bps), Y=snapshots, Z=resting size * - Color scale: warm orange/yellow peaks on dark background * - Live imbalance annotation * - Formula overlay * - Smooth camera controls with hover tooltips * * Performance: * - 100×60 grid = 6000 vertices — well within Plotly limits * - Update at 1-2 Hz recommended (depth maps don't need 60fps) * - Use uirevision to prevent camera reset on updates */ interface DepthMapPlotlyProps { surface: SurfaceData | null; metrics: ImbalanceMetrics | null; height?: number; } // Inline Plotly — avoids extra npm dependency for types declare global { interface Window { Plotly: any; } } export function DepthMapPlotly({ surface, metrics, height = 420 }: DepthMapPlotlyProps) { const containerRef = useRef(null); const plotlyRef = useRef(null); const revisionRef = useRef(0); // Load Plotly script once useEffect(() => { if (window.Plotly) return; const script = document.createElement("script"); script.src = "https://cdn.plot.ly/plotly-3.0.0.min.js"; script.async = true; script.onload = () => revisionRef.current++; document.head.appendChild(script); return () => { script.remove(); }; }, []); // Initialize or update chart useEffect(() => { if (!containerRef.current || !surface || !window.Plotly) return; const Plotly = window.Plotly; const trace: any = { type: "surface", x: surface.x, y: surface.y, z: surface.z, colorscale: [ [0, "rgb(10,10,20)"], [0.3, "rgb(20,20,60)"], [0.5, "rgb(60,30,100)"], [0.7, "rgb(180,100,30)"], [0.85, "rgb(240,160,40)"], [0.95, "rgb(255,210,80)"], [1, "rgb(255,240,180)"], ], contours: { z: { show: true, usecolormap: true, highlightcolor: "rgba(255,255,255,0.3)", project: { z: true } }, }, lighting: { ambient: 0.4, diffuse: 0.7, specular: 0.3, roughness: 0.5, fresnel: 0.2, }, lightposition: { x: 100, y: 200, z: 300 }, showscale: true, colorbar: { title: { text: "Resting Size", font: { color: "#888", size: 10 } }, tickfont: { color: "#666", size: 9 }, thickness: 12, len: 0.6, 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.05, 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.05)", zerolinecolor: "rgba(255,255,255,0.15)", tickfont: { size: 8, color: "#555" }, }, yaxis: { title: { text: "Snapshots (oldest → newest)", font: { size: 9, color: "#666" } }, gridcolor: "rgba(255,255,255,0.05)", tickfont: { size: 8, color: "#555" }, }, zaxis: { title: { text: "Size (BTC)", font: { size: 9, color: "#666" } }, gridcolor: "rgba(255,255,255,0.05)", tickfont: { size: 8, color: "#555" }, }, camera: { eye: { x: 1.5, y: 1.2, z: 1.0 }, center: { x: 0, y: 0, z: -0.1 }, }, aspectmode: "manual", aspectratio: { x: 1.6, y: 1.0, z: 0.6 }, bgcolor: "rgba(0,0,0,0)", }, margin: { l: 0, r: 20, t: 30, b: 0 }, uirevision: `depth-${revisionRef.current}`, autosize: true, font: { color: "#888" }, }; const config = { displayModeBar: true, modeBarButtonsToRemove: ["sendDataToCloud", "zoom2d", "pan2d", "select2d", "lasso2d", "autoScale2d"], modeBarButtonsToAdd: [], displaylogo: false, responsive: true, } as any; if (plotlyRef.current) { Plotly.react(containerRef.current, [trace], layout, config); } else { Plotly.newPlot(containerRef.current, [trace], layout, config); plotlyRef.current = true; // Bind hover for tooltip via Plotly's on() method (containerRef.current as any)?.on?.("plotly_hover", (d: any) => { const pt = d.points?.[0]; if (!pt) return; const bps = pt.x?.toFixed?.(1) ?? "?"; const size = pt.z?.toFixed?.(4) ?? "?"; // Custom tooltip handled by Plotly's built-in hoverlabel }); } }, [surface]); return (
{/* Imbalance Overlay */} {metrics && (
{/* Imbalance gauge */}

Imbalance

0 ? "text-green-400" : "text-red-400"}`}> {metrics.imbalance > 0 ? "+" : ""}{metrics.imbalance.toFixed(2)} {metrics.wallSide !== "none" && ( Wall on {metrics.wallSide === "bid" ? "BIDS" : "ASKS"} )}
{/* Mini bar gauge */}
= 0 ? "bg-green-500" : "bg-red-500"}`} style={{ width: `${Math.min(Math.abs(metrics.imbalance) * 500, 100)}%`, marginLeft: metrics.imbalance >= 0 ? "50%" : `${50 - Math.min(Math.abs(metrics.imbalance) * 500, 100) / 2}%` }} />
{/* Formula */}

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

= ({metrics.bidVolume.toFixed(1)} − {metrics.askVolume.toFixed(1)}) / {((metrics.bidVolume + metrics.askVolume)).toFixed(1)}

)}
); }