from typing import Optional
import plotly.graph_objects as go
import plotly.io as pio
from .color_themes import get_theme
from .plotting_constants import *
from .utils import prepare_smooth_data_2D
# Set default plotly template for better aesthetics
pio.templates.default = "plotly_white"
[docs]
def plot2D(
x_list,
y_list,
title,
x_axis,
y_axis,
h_line: Optional[ReferenceLineConfig] = None,
v_line: Optional[ReferenceLineConfig] = None,
fit_curve=False,
show_points=False,
subtitle=None,
data_label=None,
theme: Optional[str] = None,
font_config: Optional[FontConfig] = None,
layout_config: Optional[LayoutConfig] = None,
smoothing_config: Optional[SmoothingConfig] = None,
):
"""
Generic 2D plotting utility.
Parameters
----------
x_list : numpy.ndarray
Numpy array of x coordinates
y_list : numpy.ndarray
Numpy array of y coordinates
title : str
Plot title
x_axis : str
X-axis label
y_axis : str
Y-axis label
h_line : ReferenceLineConfig, optional
ReferenceLineConfig for horizontal reference line (None = no line)
v_line : ReferenceLineConfig, optional
ReferenceLineConfig for vertical reference line (None = no line)
fit_curve : bool, optional
Whether to fit a curve to the data points
show_points : bool, optional
Whether to show individual data points
subtitle : str, optional
Optional subtitle
data_label : str, optional
Curve label used in legend
theme : str, optional
Color theme name, see plotting.color_theme
font_config : FontConfig, optional
FontConfig object for font settings
layout_config : LayoutConfig, optional
LayoutConfig object for layout settings
smoothing_config : SmoothingConfig, optional
SmoothingConfig object for smoothing settings
Returns
-------
go.Figure
Plotly figure object
"""
font_config = font_config or DEFAULT_FONT_CONFIG
layout_config = layout_config or DEFAULT_LAYOUT_CONFIG
smoothing_config = smoothing_config or DEFAULT_SMOOTHING_CONFIG
if fit_curve:
x_dense, y_dense = prepare_smooth_data_2D(
x_list, y_list, smoothing_config=smoothing_config
)
else:
x_dense, y_dense = x_list, y_list
fig = go.Figure()
theme_colors = get_theme(theme)
# Interpolated line
fig.add_trace(
go.Scatter(
x=x_dense,
y=y_dense,
mode="lines",
line=dict(
color=theme_colors["light"],
width=LINE_WIDTH,
shape="spline",
),
name=data_label,
hovertemplate=f"{x_axis}: %{{x:{FLOAT_PRECISION}}}<br>{y_axis}: %{{y:{FLOAT_PRECISION}}}<extra></extra>",
)
)
if show_points:
fig.add_trace(
go.Scatter(
x=x_list,
y=y_list,
mode="markers",
marker=dict(color=theme_colors["dark"], size=MARKER_SIZE_LARGE),
showlegend=False,
hovertemplate=None,
)
)
# Add horizontal reference line if specified
if h_line is not None and h_line.value is not None:
x_min, x_max = min(x_dense), max(x_dense)
fig.add_trace(
go.Scatter(
x=[x_min, x_max],
y=[h_line.value, h_line.value],
mode="lines",
line=dict(
color=h_line.color,
width=h_line.width,
dash=h_line.dash,
),
name=f"{h_line.label}",
hoverinfo="name",
)
)
# Add vertical reference line if specified
if v_line is not None and v_line.value is not None:
y_min, y_max = min(y_dense), max(y_dense)
fig.add_trace(
go.Scatter(
x=[v_line.value, v_line.value],
y=[y_min, y_max],
mode="lines",
line=dict(
color=v_line.color,
width=v_line.width,
dash=v_line.dash,
),
name=f"{v_line.label}",
hoverinfo="name",
)
)
# Title, Axes, Label, Legend, Size
full_title = title
if subtitle is not None:
full_title += f"<br><span style='font-size: {font_config.medium}px; color: {TEXT_COLOR_LIGHT};'>{subtitle}</span>"
fig.update_layout(
title={
"text": full_title,
"font": dict(size=font_config.large, color=TEXT_COLOR_DARK),
"x": layout_config.title_x,
"xanchor": layout_config.title_xanchor,
},
xaxis_title={
"text": x_axis,
"font": dict(size=font_config.medium, color=TEXT_COLOR_DARK),
},
yaxis_title={
"text": y_axis,
"font": dict(size=font_config.medium, color=TEXT_COLOR_DARK),
},
legend_title="Legend",
showlegend=(data_label is not None)
or (h_line is not None and h_line.value is not None)
or (v_line is not None and v_line.value is not None),
legend_title_font=dict(size=font_config.medium),
legend_font=dict(size=font_config.small),
hovermode=HOVER_MODE,
plot_bgcolor=layout_config.plot_bgcolor,
width=layout_config.width,
height=layout_config.height,
margin=layout_config.margin,
)
# Add subtle grid lines
fig.update_xaxes(
showgrid=True,
gridwidth=GRID_WIDTH,
gridcolor=GRID_COLOR,
zeroline=True,
zerolinewidth=ZEROLINE_WIDTH,
zerolinecolor=ZEROLINE_COLOR,
tickfont=dict(size=font_config.small),
tickformat=FLOAT_PRECISION,
)
fig.update_yaxes(
showgrid=True,
gridwidth=GRID_WIDTH,
gridcolor=GRID_COLOR,
zeroline=True,
zerolinewidth=ZEROLINE_WIDTH,
zerolinecolor=ZEROLINE_COLOR,
tickfont=dict(size=font_config.small),
tickformat=FLOAT_PRECISION,
)
if h_line is not None and h_line.value is not None:
padding = 1 + RANGE_PADDING
fig.update_yaxes(
range=[
(1 / padding) * min(min(y_dense), h_line.value),
padding * max(max(y_dense), h_line.value),
]
)
if v_line is not None and v_line.value is not None:
padding = 1 + RANGE_PADDING
fig.update_xaxes(
range=[
(1 / padding) * min(min(x_dense), v_line.value),
padding * max(max(x_dense), v_line.value),
]
)
return fig