""" Custom Dash components for the interactive visualizer. This module provides reusable UI components including the side panel, navigation controls, and chart containers. """ from dash import html, dcc import dash_bootstrap_components as dbc import plotly.graph_objects as go from plotly.subplots import make_subplots def create_side_panel(): """ Create the side panel component for displaying hover information and controls. Returns: dash component: Side panel layout """ return dbc.Card([ dbc.CardHeader("Chart Information"), dbc.CardBody([ html.Div(id="hover-info", children=[ html.P("Hover over charts to see detailed information") ]), html.Hr(), html.Div([ dbc.Button("Reset CVD", id="reset-cvd-btn", color="primary", className="me-2"), dbc.Button("Reset Zoom", id="reset-zoom-btn", color="secondary"), ]) ]) ], style={"height": "100vh"}) def create_chart_container(): """ Create the main chart container for the 4-subplot layout. Returns: dash component: Chart container with 4-subplot layout """ return dcc.Graph( id="main-charts", figure=create_empty_subplot_layout(), style={"height": "100vh"}, config={ "displayModeBar": True, "displaylogo": False, "modeBarButtonsToRemove": ["select2d", "lasso2d"], "modeBarButtonsToAdd": ["resetScale2d"], "scrollZoom": True, # Enable mouse wheel zooming "doubleClick": "reset+autosize" # Double-click to reset zoom } ) def create_empty_subplot_layout(): """ Create empty 4-subplot layout matching existing visualizer structure. Returns: plotly.graph_objects.Figure: Empty figure with 4 subplots """ fig = make_subplots( rows=4, cols=1, shared_xaxes=True, subplot_titles=["OHLC", "Volume", "Order Book Imbalance (OBI)", "Cumulative Volume Delta (CVD)"], vertical_spacing=0.02 ) # Configure layout to match existing styling fig.update_layout( height=800, showlegend=False, margin=dict(l=50, r=50, t=50, b=50), template="plotly_dark", # Professional dark theme paper_bgcolor='rgba(0,0,0,0)', # Transparent background plot_bgcolor='rgba(0,0,0,0)' # Transparent plot area ) # Configure synchronized zooming and panning configure_synchronized_axes(fig) return fig def configure_synchronized_axes(fig): """ Configure synchronized zooming and panning across all subplots. Args: fig: Plotly figure with subplots """ # Enable dragmode for panning and zooming fig.update_layout( dragmode='zoom', selectdirection='h' # Restrict selection to horizontal for time-based data ) # Configure X-axes for synchronized behavior (already shared via make_subplots) # All subplots will automatically share zoom/pan on X-axis due to shared_xaxes=True # Configure individual Y-axes for better UX fig.update_yaxes(fixedrange=False, gridcolor='rgba(128,128,128,0.2)') # Allow Y-axis zooming fig.update_xaxes(fixedrange=False, gridcolor='rgba(128,128,128,0.2)') # Allow X-axis zooming # Enable crosshair cursor spanning all charts fig.update_layout(hovermode='x unified') fig.update_traces(hovertemplate='') # Clean hover labels return fig def add_ohlc_trace(fig, ohlc_data: dict): """ Add OHLC candlestick trace to the first subplot. Args: fig: Plotly figure with subplots ohlc_data: Dict with x, open, high, low, close arrays """ candlestick = go.Candlestick( x=ohlc_data["x"], open=ohlc_data["open"], high=ohlc_data["high"], low=ohlc_data["low"], close=ohlc_data["close"], name="OHLC" ) fig.add_trace(candlestick, row=1, col=1) return fig def add_volume_trace(fig, volume_data: dict): """ Add Volume bar trace to the second subplot. Args: fig: Plotly figure with subplots volume_data: Dict with x (timestamps) and y (volumes) arrays """ volume_bar = go.Bar( x=volume_data["x"], y=volume_data["y"], name="Volume", marker_color='rgba(158, 185, 243, 0.7)', # Blue with transparency showlegend=False, hovertemplate="Volume: %{y}" ) fig.add_trace(volume_bar, row=2, col=1) return fig def add_obi_trace(fig, obi_data: dict): """ Add OBI line trace to the third subplot. Args: fig: Plotly figure with subplots obi_data: Dict with timestamp and obi arrays """ obi_line = go.Scatter( x=obi_data["timestamp"], y=obi_data["obi"], mode='lines', name="OBI", line=dict(color='blue', width=2), showlegend=False, hovertemplate="OBI: %{y:.3f}" ) # Add horizontal reference line at y=0 fig.add_hline(y=0, line=dict(color='gray', dash='dash', width=1), row=3, col=1) fig.add_trace(obi_line, row=3, col=1) return fig def add_cvd_trace(fig, cvd_data: dict): """ Add CVD line trace to the fourth subplot. Args: fig: Plotly figure with subplots cvd_data: Dict with timestamp and cvd arrays """ cvd_line = go.Scatter( x=cvd_data["timestamp"], y=cvd_data["cvd"], mode='lines', name="CVD", line=dict(color='red', width=2), showlegend=False, hovertemplate="CVD: %{y:.1f}" ) fig.add_trace(cvd_line, row=4, col=1) return fig def create_populated_chart(ohlc_data, metrics_data): """ Create a chart container with real data populated. Args: ohlc_data: List of OHLC tuples or None metrics_data: List of Metric objects or None Returns: dcc.Graph component with populated data """ from data_adapters import format_ohlc_for_plotly, format_volume_for_plotly, format_metrics_for_plotly # Create base subplot layout fig = create_empty_subplot_layout() # Add real data if available if ohlc_data: # Format OHLC data ohlc_formatted = format_ohlc_for_plotly(ohlc_data) volume_formatted = format_volume_for_plotly(ohlc_data) # Add OHLC trace fig = add_ohlc_trace(fig, ohlc_formatted) # Add Volume trace fig = add_volume_trace(fig, volume_formatted) if metrics_data: # Format metrics data metrics_formatted = format_metrics_for_plotly(metrics_data) # Add OBI and CVD traces if metrics_formatted["obi"]["x"]: # Check if we have OBI data obi_data = { "timestamp": metrics_formatted["obi"]["x"], "obi": metrics_formatted["obi"]["y"] } fig = add_obi_trace(fig, obi_data) if metrics_formatted["cvd"]["x"]: # Check if we have CVD data cvd_data = { "timestamp": metrics_formatted["cvd"]["x"], "cvd": metrics_formatted["cvd"]["y"] } fig = add_cvd_trace(fig, cvd_data) return dcc.Graph( id="main-charts", figure=fig, style={"height": "100vh"}, config={ "displayModeBar": True, "displaylogo": False, "modeBarButtonsToRemove": ["select2d", "lasso2d"], "modeBarButtonsToAdd": ["pan2d", "zoom2d", "zoomIn2d", "zoomOut2d", "resetScale2d"], "scrollZoom": True, "doubleClick": "reset+autosize" } )