from typing import List
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from ..core_data_structures.data_instance import DataInstance
from ..units import Timescale
from .plotting_constants import *
def _timestamps_to_seconds(
timestamp_np,
timestamp_unit: Timescale,
):
"""Convert a raw timestamp array to seconds for x-axis display.
Parameters
----------
timestamp_np : NDArray
Raw integer timestamp array.
timestamp_unit : Timescale
Unit of the input timestamps.
Returns
-------
NDArray
Timestamps as float64 seconds.
"""
timestamps = timestamp_np.astype(float)
if timestamp_unit == Timescale.US:
return timestamps / 1e6
if timestamp_unit == Timescale.MS:
return timestamps / 1e3
return timestamps
def _add_vlines(
fig: go.Figure,
vlines: List[float],
vline_config: "VLineConfig",
) -> None:
"""
Add vertical lines to an existing Plotly figure.
Parameters
----------
fig : go.Figure
Target figure
vlines : List[float]
X-axis positions (in seconds) where vertical lines are drawn
vline_config : VLineConfig
Visual configuration for the vertical lines
"""
for x in vlines:
fig.add_vline(
x=x,
line_dash=vline_config.dash,
line_color=vline_config.color,
line_width=vline_config.width,
opacity=vline_config.opacity,
)
[docs]
def plot_single_axis(
data_instances: List[DataInstance],
title: str | None = None,
y_axis_title: str | None = None,
show_legend: bool = True,
layout_config: LayoutConfig = DEFAULT_LAYOUT_CONFIG,
font_config: FontConfig = DEFAULT_FONT_CONFIG,
timestamp_unit: Timescale = Timescale.MS,
vlines: List[float] | None = None,
vline_config: "VLineConfig" = DEFAULT_VLINE_CONFIG,
) -> go.Figure:
"""
Plot one or more DataInstances on a single y-axis using Plotly.
Parameters
----------
data_instances : List[DataInstance]
List of DataInstance objects to plot
title : str | None, optional
Title for the entire plot.
y_axis_title : str | None, optional
Label for the y-axis.
show_legend : bool, optional
Whether to show plot legends. Default is True
layout_config : LayoutConfig, optional
font_config : FontConfig, optional
timestamp_unit : Timescale, optional
Timestamp unit in the underlying data. Converted to seconds for x-axis display.
vlines : List[float] | None, optional
X-axis positions (in seconds) where vertical lines are drawn. Default is None.
vline_config : VLineConfig, optional
Visual configuration for the vertical lines. Default is DEFAULT_VLINE_CONFIG.
Returns
-------
go.Figure
Examples
--------
>>> fig = plot_single_axis([speed_di, torque_di], title="Speed & Torque", y_axis_title="Value")
>>> fig.show()
"""
if not data_instances:
print("Warning: No data instances provided for plotting")
return
fig = go.Figure()
for di in data_instances:
if len(di) == 0:
print(f"Warning: No data points in DataInstance for {di.label}")
continue
# Convert timestamps from the log unit to seconds for plotting.
timestamps_s = _timestamps_to_seconds(di.timestamp_np, timestamp_unit)
fig.add_trace(
go.Scattergl(
x=timestamps_s,
y=di.value_np,
mode="lines",
name=di.label,
)
)
fig.update_layout(
title=dict(
text=title,
x=layout_config.title_x,
xanchor=layout_config.title_xanchor,
yanchor=layout_config.title_yanchor,
font=dict(size=font_config.large),
),
xaxis_title=dict(text="Time (s)", font=dict(size=font_config.medium)),
yaxis_title=dict(text=y_axis_title, font=dict(size=font_config.medium)),
showlegend=show_legend,
hovermode="x unified",
width=layout_config.width,
height=layout_config.height,
margin=layout_config.margin,
plot_bgcolor=layout_config.plot_bgcolor,
xaxis=dict(tickfont=dict(size=font_config.small)),
yaxis=dict(tickfont=dict(size=font_config.small)),
legend=dict(font=dict(size=font_config.small)),
)
if vlines:
_add_vlines(fig, vlines, vline_config)
return fig
[docs]
def plot_dual_axis(
left_data_instances: List[DataInstance],
right_data_instances: List[DataInstance],
title: str | None = None,
left_y_axis_title: str | None = None,
right_y_axis_title: str | None = None,
show_legend: bool = True,
font_config: FontConfig = DEFAULT_FONT_CONFIG,
layout_config: LayoutConfig = DEFAULT_LAYOUT_CONFIG,
timestamp_unit: Timescale = Timescale.MS,
vlines: List[float] | None = None,
vline_config: VLineConfig = DEFAULT_VLINE_CONFIG,
) -> go.Figure:
"""
Plot DataInstances on dual y-axes using Plotly.
Parameters
----------
left_data_instances : List[DataInstance]
List of DataInstance objects to plot on the left y-axis
right_data_instances : List[DataInstance]
List of DataInstance objects to plot on the right y-axis
title : str | None, optional
Title for the entire plot.
left_y_axis_title : str | None, optional
Label for the left y-axis.
right_y_axis_title : str | None, optional
Label for the right y-axis.
show_legend : bool, optional
Whether to show plot legends. Default is True
font_config : FontConfig, optional
Font configuration for plot elements. Default is DEFAULT_FONT_CONFIG
layout_config : LayoutConfig, optional
Layout configuration for plot dimensions. Default is DEFAULT_LAYOUT_CONFIG
timestamp_unit : Timescale, optional
Timestamp unit in the underlying data. Converted to seconds for x-axis display.
vlines : List[float] | None, optional
X-axis positions (in seconds) where vertical lines are drawn. Default is None.
vline_config : VLineConfig, optional
Visual configuration for the vertical lines. Default is DEFAULT_VLINE_CONFIG.
Returns
-------
go.Figure
Examples
--------
>>> fig = plot_dual_axis([speed_di], [torque_di], left_y_axis_title="Speed (mph)", right_y_axis_title="Torque (Nm)")
>>> fig.show()
"""
if not left_data_instances and not right_data_instances:
print("Warning: No data instances provided for plotting")
return
# Create figure with secondary y-axis
fig = make_subplots(specs=[[{"secondary_y": True}]])
# Plot left axis data
for di in left_data_instances:
if len(di) == 0:
print(f"Warning: No data points in DataInstance for {di.label}")
continue
# Convert timestamps from the log unit to seconds for plotting.
timestamps_s = _timestamps_to_seconds(di.timestamp_np, timestamp_unit)
fig.add_trace(
go.Scattergl(
x=timestamps_s,
y=di.value_np,
mode="lines",
name=di.label,
),
secondary_y=False,
)
# Plot right axis data
for di in right_data_instances:
if len(di) == 0:
print(f"Warning: No data points in DataInstance for {di.label}")
continue
# Convert timestamps from the log unit to seconds for plotting.
timestamps_s = _timestamps_to_seconds(di.timestamp_np, timestamp_unit)
fig.add_trace(
go.Scattergl(
x=timestamps_s,
y=di.value_np,
mode="lines",
name=di.label,
line=dict(dash="dash"),
),
secondary_y=True,
)
# Set axis titles
fig.update_xaxes(
title_text="Time (s)",
title_font=dict(size=font_config.medium),
tickfont=dict(size=font_config.small),
)
fig.update_yaxes(
title_text=left_y_axis_title,
title_font=dict(size=font_config.medium),
tickfont=dict(size=font_config.small),
secondary_y=False,
)
fig.update_yaxes(
title_text=right_y_axis_title,
title_font=dict(size=font_config.medium),
tickfont=dict(size=font_config.small),
secondary_y=True,
)
fig.update_layout(
title=dict(
text=title,
x=layout_config.title_x,
xanchor=layout_config.title_xanchor,
yanchor=layout_config.title_yanchor,
font=dict(size=font_config.large),
),
showlegend=show_legend,
hovermode="x unified",
width=layout_config.width,
height=layout_config.height,
margin=layout_config.margin,
plot_bgcolor=layout_config.plot_bgcolor,
legend=dict(font=dict(size=font_config.small)),
)
if vlines:
_add_vlines(fig, vlines, vline_config)
return fig