"""
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"
}
)