77.6 Plotly: Interactive Charts in Notebooks and Dash
Right, so you’ve made some static charts. They’re lovely. They belong in a PDF, framed on a wall. But you and I both know the real world is messy, and data begs to be poked and prodded. You want to hover over a point to see what that insane outlier actually is. You want to zoom in on that weird cluster. You want a chart that’s a conversation, not a monologue. That’s where Plotly comes in.
Plotly is the library that takes your boring, flat visualizations and gives them a PhD in interactivity. It renders your charts as HTML/JavaScript, which means they live in the modern web ecosystem. We’re going to focus on plotly.express, the high-level API that lets you build 90% of what you’ll need with shockingly few lines of code. It’s the “I don’t have time for your boilerplate” option.
The Express Lane to a Basic Chart
Let’s get the absolute basics down. Forget plt.figure() and ax.plot(). Plotly Express is all about declarative, data-driven plotting. You tell it what you want, not how to draw it.
import plotly.express as px
# Let's use the classic tips dataset because we're classic people.
df = px.data.tips()
# A scatter plot in one line. No, really.
fig = px.scatter(df, x='total_bill', y='tip', color='smoker',
title='The Eternal Question: Do Smokers Tip Better?')
fig.show()
Boom. You now have a fully interactive chart. Hover over points. Click on legends to isolate categories (“smoker” vs “non-smoker”). Pan and zoom. This isn’t magic; it’s just good design. The color argument automatically creates a legend and a color scale. Plotly Express intelligently maps your DataFrame columns to visual properties.
Leveling Up: Facets, Animation, and More
This is where Plotly starts to feel like you’re cheating. Want to break that same chart out by day of the week into small multiples? It’s one more argument.
fig = px.scatter(df, x='total_bill', y='tip', color='smoker',
facet_col='day', # This is the magic line
category_orders={'day': ['Thur', 'Fri', 'Sat', 'Sun']} # Force a sensible order
)
fig.show()
Notice the category_orders bit? This is a classic “gotcha.” Plotly will sometimes order categorical values alphabetically, which for days of the week is a special kind of nonsense. Always take control of your axis and legend ordering.
Now, for the party trick: animation.
fig = px.scatter(df, x='total_bill', y='tip', color='smoker',
animation_frame='day', # Animate over the days
range_x=[0, 60], range_y=[0, 12] # Crucial: fix the axis ranges!
)
fig.show()
Why the range_x and range_y? Because without it, the axes will rescale dynamically for each frame of the animation, making it utterly useless for comparison. The animation will just look like it’s zooming in and out randomly. It’s a baffling default and my number one complaint. Always, always lock your axes when animating.
The Pit of Despair: The Figure Object
Plotly Express is amazing, but sometimes you need to tweak something it doesn’t expose directly. This is where you have to descend into the lower-level plotly.graph_objects (go) layer. The fig object you get from Express is actually a graph_objects.Figure in disguise. This is both a blessing and a curse.
Let’s say you want to add a trendline for non-smokers only. Express might not let you be that specific.
import plotly.graph_objects as go
# First, make our base figure with Express (it's faster)
fig = px.scatter(df, x='total_bill', y='tip', color='smoker')
# Now, let's get fancy and add a trendline for just the non-smokers.
non_smokers_df = df[df['smoker'] == 'No']
# Create a trace ourselves using graph_objects
trendline_trace = go.Scatter(
x=non_smokers_df['total_bill'],
y=np.poly1d(np.polyfit(non_smokers_df['total_bill'], non_smokers_df['tip'], 1))(non_smokers_df['total_bill']),
mode='lines',
line=dict(color='firebrick', width=3, dash='dash'),
name='Non-Smoker Trendline',
showlegend=True # Important! Or it won't show in the legend
)
# Add our custom trace to the existing figure
fig.add_trace(trendline_trace)
fig.show()
The key thing to understand here is the union of traces. The Express fig already has traces (the data series) for “Yes” and “No”. When you add_trace, you’re adding a third trace to that figure. This is incredibly powerful, but it means you’re now responsible for managing the legend and making sure your new trace makes sense in the context of the old ones.
Rendering in Notebooks: The renderer Kerfuffle
This is the most common “it doesn’t work!” moment. Plotly has multiple renderers for output. In a Jupyter notebook, you usually want the notebook renderer. But sometimes it gets confused. The most reliable method is to set it at the top of your notebook or script.
import plotly.io as pio
pio.renderers.default = 'notebook' # or 'notebook_connected' if you need CDN access
If you’re in JupyterLab, you might need to install the jupyterlab-plotly extension. Yes, it’s annoying. No, there’s no way around it. The web moves fast, and plotting libraries have to keep up.
Best Practices: Don’t Be That Person
- Interactivity is a Superpower, Not a Crutch: Don’t make a wildly complicated interactive chart when a simple, clear static one would do. Use interactivity to enable exploration, not to hide poor design.
- Mind the File Size: Every interactive element is more JavaScript. A Plotly figure with 100,000 points will make your browser cry. For massive datasets, aggregate first or use a library like
datashaderalongside Plotly. - The Hover Text is Your Best Friend: Use the
hover_dataandhover_nameparameters in Express to add crucial context. Answer the question a user would have before they have to zoom and pan to find the answer.
fig = px.scatter(df, x='total_bill', y='tip', color='smoker',
hover_data=['time', 'size'], # Add extra data on hover
hover_name='day' # Make the bold title of the hover box the day
)
Plotly is the tool you use when the story isn’t a single, static viewpoint. It’s for exploration, for dashboards, for presenting data to an audience and letting them find their own narrative. It expects a bit more from you, the programmer, but the payoff is a chart that feels alive.