Plotly Best Practices
Plotly 6.5.0 is a powerful tool for interactive, publication-quality visualizations, crucial for modern AI/ML workflows. These rules ensure your Plotly code is maintainable, reproducible, and production-ready, leveraging the latest features including AI-assisted Studio tools.
Code Organization and Structure
1. Separate Data Preparation from Visualization
Keep data preprocessing, model outputs, and metric calculations distinct from plotting logic. This improves debugging and reusability.
❌ BAD:
import pandas as pd
import plotly.express as px
def analyze_and_plot_data(raw_data: pd.DataFrame):
# Data processing mixed with plotting
processed_df = raw_data[raw_data['value'] > 0]
processed_df['normalized'] = processed_df['value'] / processed_df['value'].max()
fig = px.scatter(processed_df, x='timestamp', y='normalized', title='Normalized Data')
fig.show()
✅ GOOD:
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
def preprocess_data(raw_data: pd.DataFrame) -> pd.DataFrame:
"""Processes raw data for visualization."""
processed_df = raw_data[raw_data['value'] > 0]
processed_df['normalized'] = processed_df['value'] / processed_df['value'].max()
return processed_df
def create_normalized_scatter_plot(data: pd.DataFrame) -> go.Figure:
"""Creates a scatter plot from processed data."""
fig = px.scatter(data, x='timestamp', y='normalized', title='Normalized Data Over Time')
return fig
# Usage example:
# raw_df = pd.read_csv('my_data.csv')
# clean_df = preprocess_data(raw_df)
# plot_fig = create_normalized_scatter_plot(clean_df)
# plot_fig.show()
2. Modular Figure Construction
Treat the Figure object as a modular data structure. Build traces, then define the layout. This mirrors Plotly's internal design and enhances readability.
❌ BAD:
import plotly.graph_objects as go
# Hard-to-read, deeply nested dictionary for figure definition
fig = go.Figure(
data=[{'type': 'bar', 'x': [1, 2, 3], 'y': [10, 15, 13]}],
layout={'title': {'text': 'Sales Data'}, 'xaxis': {'title': {'text': 'Month'}}}
)
✅ GOOD:
import plotly.graph_objects as go
# Separate data (traces) and layout definition
trace1 = go.Bar(x=[1, 2, 3], y=[10, 15, 13], name='Product A')
trace2 = go.Bar(x=[1, 2, 3], y=[12, 10, 18], name='Product B')
fig = go.Figure(data=[trace1, trace2])
fig.update_layout(
title_text='Monthly Sales Data',
xaxis_title_text='Month',
yaxis_title_text='Revenue ($)',
legend_title_text='Product'
)
Common Patterns and Anti-patterns
3. Prefer Plotly Express for Most Charts
Use plotly.express for common chart types. It's terse, consistent, and returns a full go.Figure object, allowing easy customization with graph_objects methods.
❌ BAD:
import plotly.graph_objects as go
import pandas as pd
df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 1, 2], 'category': ['A', 'B', 'A']})
# Manually creating a scatter plot with graph_objects for a simple case
fig = go.Figure(data=[
go.Scatter(x=df['x'], y=df['y'], mode='markers', marker=dict(color=df['category'].map({'A': 'red', 'B': 'blue'})))
])
fig.update_layout(title_text='Custom Scatter Plot')
✅ GOOD:
import plotly.express as px
import pandas as pd
df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 1, 2], 'category': ['A', 'B', 'A']})
# Plotly Express handles common mappings automatically
fig = px.scatter(df, x='x', y='y', color='category', title='Simple Scatter Plot')
# Further customization is still possible
fig.update_traces(marker_size=10)
4. Apply Consistent Labeling and Theming
Ensure all plots meet publication-quality standards with clear titles, descriptive axis labels, and consistent fonts. Leverage labels in px and update_layout for global styling.
❌ BAD:
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x='sepal_length', y='sepal_width') # No title, default labels
fig.show()
✅ GOOD:
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df,
x='sepal_length',
y='sepal_width',
color='species',
labels={
'sepal_length': 'Sepal Length (cm)',
'sepal_width': 'Sepal Width (cm)',
'species': 'Iris Species'
},
title='Iris Sepal Dimensions by Species')
fig.update_layout(
font_family="Arial",
font_color="darkblue",
title_font_size=20,
hoverlabel_font_family="monospace"
)
fig.show()
Performance Considerations
5. Optimize for Large Datasets
For extremely large datasets, consider data aggregation or sampling before plotting to maintain interactivity. Plotly.js can handle many points, but browser rendering has limits. Use go.Scattergl for hardware-accelerated rendering of large scatter plots.
❌ BAD:
import plotly.express as px
import numpy as np
import pandas as pd
# Plotting 10 million points directly, which might be slow
large_df = pd.DataFrame({'x': np.random.rand(10_000_000), 'y': np.random.rand(10_000_000)})
fig = px.scatter(large_df, x='x', y='y')
fig.show() # Might freeze browser or be very sluggish
✅ GOOD:
import plotly.graph_objects as go # Use go for Scattergl
import numpy as np
import pandas as pd
# For very large scatter plots, use go.Scattergl for WebGL acceleration
large_df = pd.DataFrame({'x': np.random.rand(10_000_000), 'y': np.random.rand(10_000_000)})
fig = go.Figure(data=[go.Scattergl(x=large_df['x'], y=large_df['y'], mode='markers')])
fig.update_layout(title_text='Large Data Scatter Plot (WebGL)')
fig.show()
# Alternatively, for other chart types, consider sampling or aggregation
# sampled_df = large_df.sample(n=100_000)
# fig = px.scatter(sampled_df, x='x', y='y', title='Sampled Data Scatter Plot')
# fig.show()
6. Choose Appropriate Output Format
For static reports or non-interactive embedding, export figures as images. For interactive dashboards, use fig.show() or integrate with Dash.
import plotly.express as px
import pandas as pd
df = px.data.iris()
fig = px.scatter(df, x='sepal_length', y='sepal_width', color='species')
# ✅ GOOD: For interactive display in notebooks/scripts
fig.show()
# ✅ GOOD: For static image export (requires kaleido: pip install kaleido)
fig.write_image("iris_scatter.png", width=800, height=600)
# ✅ GOOD: For interactive HTML export
fig.write_html("iris_scatter.html")
Common Pitfalls and Gotchas
7. Avoid Direct Dictionary Manipulation for Figures
While figures can be represented as dictionaries, directly building or modifying them this way bypasses Plotly's validation and convenience methods. Always use plotly.graph_objects.Figure or plotly.express.
❌ BAD:
# No validation, easy to make typos
fig = {
"data": [{"type": "bar", "x": [1, 2], "y": [3, 4]}],
"layout": {"title": {"text": "My Bar Chart"}}
}
import plotly.io as pio
pio.show(fig)
✅ GOOD:
import plotly.graph_objects as go
# Benefits from validation, docstrings, and update methods
fig = go.Figure(data=[go.Bar(x=[1, 2], y=[3, 4])])
fig.update_layout(title_text="My Bar Chart")
fig.show()
8. Leverage update_layout and update_traces
These methods provide a clean, efficient way to modify existing figures without recreating them. Use "magic underscores" for concise attribute setting.
❌ BAD:
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x='sepal_length', y='sepal_width')
# Reassigning individual attributes directly is verbose and less robust
fig.layout.title.text = 'Iris Data'
fig.layout.xaxis.title.text = 'Length'
fig.layout.yaxis.title.text = 'Width'
✅ GOOD:
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x='sepal_length', y='sepal_width')
# Use update methods with magic underscores for clarity and efficiency
fig.update_layout(
title_text='Iris Sepal Measurements',
xaxis_title_text='Sepal Length (cm)',
yaxis_title_text='Sepal Width (cm)',
font_size=12
)
fig.update_traces(marker_opacity=0.7, selector=dict(type='scatter')) # Apply to all scatter traces
Type Hints
9. Use Type Hints for Clarity and Maintainability
Explicitly type hint Plotly Figure objects and related data structures (pd.DataFrame) in function signatures. This improves code readability and enables static analysis.
❌ BAD:
import plotly.express as px
def create_plot(data): # Untyped input and output
fig = px.line(data, x='date', y='value')
return fig
✅ GOOD:
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
def create_time_series_plot(data: pd.DataFrame) -> go.Figure:
"""Generates a time series line plot."""
fig = px.line(data, x='date', y='value', title='Time Series Analysis')
return fig
Virtual Environments
10. Always Use Virtual Environments
Isolate your project dependencies. This is a fundamental Python best practice, critical for reproducibility, especially in AI/ML where library versions can significantly impact results.
✅ GOOD:
# Create a virtual environment
python -m venv .venv
# Activate it
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Install dependencies
pip install plotly pandas kaleido
Packaging
11. Package Visualization Code
When sharing or deploying your visualization logic, package it properly (e.g., using pyproject.toml and setuptools). This ensures consistent installation and dependency management.
✅ GOOD:
# pyproject.toml example
[project]
name = "my_ml_viz"
version = "0.1.0"
dependencies = [
"plotly>=6.5.0",
"pandas",
"kaleido"
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
Testing Approaches
12. Implement Testing for Visualization Logic
Test functions that generate figures, focusing on data input validation and layout configuration. For complex visual changes, consider visual regression testing.
✅ GOOD:
import pytest
import pandas as pd
import plotly.graph_objects as go
# Assuming create_time_series_plot is in a module named my_viz_module
# from my_viz_module import create_time_series_plot
def create_time_series_plot(data: pd.DataFrame) -> go.Figure:
"""Generates a time series line plot (for testing purposes)."""
fig = px.line(data, x='date', y='value', title='Time Series Analysis')
return fig
def test_create_time_series_plot():
# Test data
test_data = pd.DataFrame({
'date': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
'value': [10, 12, 8]
})
fig = create_time_series_plot(test_data)
# Assertions about the figure structure and content
assert isinstance(fig, go.Figure)
assert len(fig.data) == 1
assert fig.data[0].type == 'scatter' # px.line creates scatter traces
assert fig.layout.title.text == 'Time Series Analysis'
assert fig.data[0].x.tolist() == test_data['date'].tolist()
assert fig.data[0].y.tolist() == test_data['value'].tolist()
# For visual regression, you'd compare fig.to_json() or a generated