Using hvPlot¶
This skill provides correct patterns and common pitfalls for hvPlot to visualize data interactively.
Before plotting, consider: what story does the data tell? What comparison matters most? Then choose the plot type, encoding, and labels that make that story obvious. For publication-quality figures needing fine-grained control, use HoloViews directly with the Matplotlib backend instead of Bokeh.
Contents¶
- Dependencies
- Serving and Iterating
- Plot Labels
- Geographic Plots
- Hover Tooltips
- Managing Dimensions
- Big Data
- Styling
- Interaction
- Timeseries
- Subplots and Layouts
- Statistical Functions
Dependencies¶
Activate the .hvplot accessor with the appropriate backend import: import hvplot.pandas, hvplot.polars, hvplot.xarray, hvplot.duckdb, or hvplot.dask. Backends like Polars and DuckDB must be installed separately. Optional: datashader for resampling, geoviews or geopandas for geographic data. Prefer the Bokeh backend (default) for interactivity, Matplotlib for static/print output, Plotly as a last resort.
Do NOT add import holoviews as hv or hv.extension('bokeh') to hvplot-only code. import hvplot.pandas activates the Bokeh backend automatically. Only import holoviews when you need HoloViews elements or containers like VLine, HLine, Text, Overlay, etc. For .opts() system, formatters, Bokeh tools, streams, and other HoloViews concepts, see the HoloViews skill.
# WRONG — redundant import, do not do this
import holoviews as hv
import hvplot.pandas
# CORRECT — hvplot.pandas is sufficient
import hvplot.pandas
Serving and Iterating¶
To serve an hvPlot visualization as a Panel app for interactive development:
import hvplot.pandas # noqa
import panel as pn
pn.extension()
plot = df.hvplot.scatter(x="col_a", y="col_b")
pn.pane.HoloViews(plot).servable()
Run with panel serve app.py --dev --show. See the Panel skill for the full Viewer class pattern and Iterating on Panel Apps for the serve → screenshot → debug loop.
Plot Labels¶
- Label axes and colorbars with descriptive names and units.
- Titles should tell the story (what, where, when); don't repeat what the axes already show.
- Compute dynamic labels (date ranges, region names) as variables before the plot call to keep it readable.
- Use
coastlineortilesfor geographic context on geospatial plots.
import hvplot.pandas # noqa
earthquakes = hvplot.sampledata.earthquakes("pandas")
time_range_label = f"({earthquakes['time'].min():%b %Y} to {earthquakes['time'].max():%b %Y})"
title = f"Indonesian Archipelago Earthquakes {time_range_label}"
earthquakes.hvplot.points(
x="lon",
y="lat",
color="mag",
xlabel="Longitude (°E)",
ylabel="Latitude (°N)",
clabel="Magnitude",
title=title,
coastline=True,
)
Geographic Plots¶
geo=Trueenables geographic (web-mercator) projection and tile basemaps. Requiresgeoviewsto be installed.tiles=accepts these built-in strings:"CartoDark","CartoLight","CartoMidnight","OSM","EsriImagery","EsriTerrain"orxyzservices.TileProviderinstances Do not use"CartoDB.DarkMatter","CartoDB Dark_Matter". See https://hvplot.holoviz.org/en/docs/latest/ref/plotting_options/geographic.html#tiles- Do not convert to a GeoDataFrame or reproject to EPSG:3857 manually. Pass
geo=Trueand hvplot handles projection.
import hvplot.pandas # noqa
earthquakes = hvplot.sampledata.earthquakes("pandas")
earthquakes.hvplot.points(
x="lon",
y="lat",
geo=True,
tiles="CartoDark", # correct — do NOT use "CartoDB.DarkMatter"
color="mag",
cmap="viridis_r",
clabel="Magnitude",
size=6,
alpha=0.8,
hover_cols=["place", "time", "mag", "depth"],
responsive=True,
title="Earthquakes colored by magnitude",
)
Hover Tooltips¶
- Pair
hover_colswithhover_tooltips: columns not used as x/y/color are not sent to the client by default, producing???in tooltips. - Only include the columns you need to avoid sending all data to the client.
hover_formattersis deprecated and should not be used. Do not pass it even alongsidehover_tooltips. Format datetimes inline in the tooltip string instead.- Bokeh format syntax:
{0.1f}floats,{0,0}thousands,{%F %H:%M}datetimes. Literal text likekmcan follow the format.
...
earthquakes.hvplot.points(
...,
hover_tooltips=[
("Place", "@place"),
("Time", "@time{%F %H:%M}"),
("Magnitude", "@mag{0.1f}"),
("Depth", "@depth{0.0f} km"),
],
hover_cols=["place", "time", "mag", "depth"],
title=title,
coastline=True,
)
Managing Dimensions¶
color=andsize=are vectorized; they scale to large data. Usecmap=/clabel=with color,scale=with size.by=overlays color-coded layers with a legend; useful for stacked/side-by-side bars.groupby=adds a dropdown widget to filter to one category at a time.by=andgroupby=loop over categories, creating a separate element per value. Slow with many categories; prefercolor=for high-cardinality columns.- Options not directly exposed by hvPlot (e.g.
color_levelsfor a discrete colorbar) can be set via.opts("Points", ...). Usecticks=andclim=in hvPlot to control colorbar tick count and range.
...
earthquakes.hvplot.points(
...,
color="mag",
cmap="viridis_r",
clabel="Magnitude",
cticks=5,
clim=(4, 7),
size="depth",
scale=0.5,
).opts("Points", color_levels=6)
Big Data¶
- If data exceeds 100k points, consider
rasterize=Trueoverdatashade=True. Rasterize aggregates server-side but colormaps in the browser, preserving hover tooltips, colorbars, andcnorm/cmapcontrol. Datashade sends an opaque RGB image — use it only for categorical color mixing (aggregator='count_cat'). resample_when=Ndisables resampling when the viewport contains fewer than N points (e.g. after zooming in), creating a dynamic overlay that toggles opacity of either the rasterized or original layer. However, because it is an overlay, it can be finicky and may cause confusion, e.g. the hover tooltips will only work when the original layer is active.
Styling¶
- Sort values so the largest is at top (or bottom) for easy comparison.
- For
barh,.set_index("category")before plotting. Without this,sort_values()leaves a numeric index that renders as NaN on the y-axis. - Use a single neutral color by default; reserve color encoding for when it maps to data. Use
c="column", cmap={val: "#hex", ...}for categorical coloring (a list of hex values viacolor=does not work for bar/barh). For stacked bars withby=, pass the same dict viacmap=. - Simplify when labels carry the information:
xaxis=False,yaxis=False,show_frame=False. - Overlay
hvplot.labelsto show values directly on bars, eliminating the need for axis ticks entirely. Passresponsive=Trueandhover=Falseon both plot and labels calls. Inside bars:y="pos"wherepos = value * 0.98,text_align="right",text_color="white". Outside bars:pos = value + offset,text_align="left",text_color="black". For stacked bars + labels, the labels DataFrame must include stacked columns viahover_cols. backend_optsaccesses Bokeh model properties (e.g."outline_line_alpha": 0)..opts()accesses HoloViews plot options (e.g.show_frame=False).- Prefer hvPlot kwargs over
.opts()— they take precedence. Passresponsive=Truein the hvPlot call, not via.opts(), which can conflict with default dimensions. - Use
NumeralTickFormatter(format='0a')for large-number axes viaxformatter=/yformatter=. - Use
fontscale=for readability. - This example is heavily stylized to illustrate what's possible; use discretion.
import hvplot.pandas # noqa
earthquakes = hvplot.sampledata.earthquakes("pandas")
counts = earthquakes.value_counts("depth_class").sort_values()
counts = counts.reset_index().assign(**{"position": counts.values + 3})
barh = counts.hvplot.barh(
x="depth_class",
y="count",
title="Most Earthquakes Occur at Shallow Depths",
color="#4e79a7",
hover_tooltips=[("Depth Class", "@depth_class"), ("Count", "@count{0,0}")],
hover_cols=["depth_class", "count"],
xaxis=False,
yaxis="bare",
legend=False,
fontscale=1.35,
padding=(0, 0.25),
backend_opts={"plot.toolbar.autohide": True, "plot.outline_line_alpha": 0},
).opts(show_frame=False)
labels = counts.hvplot.labels(
x="depth_class",
y="position",
text="{count} {depth_class}",
hover_cols=["count"],
text_align="left",
text_color="black",
)
barh * labels
Interaction¶
- Hide the toolbar for polished output:
backend_opts={"plot.toolbar.autohide": True}(shows on hover) or.opts(toolbar=None)(removes entirely). Hide on secondary panels in a layout (e.g. the bottom chart) and keep on the main chart. - For timeseries, use
hover_mode='vline'to snap the crosshair to the nearest x-value — much easier to read than the default point hover. - Configure wheel zoom axis:
tools=['xwheel_zoom']for timeseries (zoom x only),tools=['ywheel_zoom']for vertical, or default'wheel_zoom'for both. Replace the active tool withactive_scroll=.
# Timeseries with vline crosshair, x-only zoom, toolbar hidden
apple.hvplot.line(
y='close',
hover_mode='vline',
tools=['xwheel_zoom'],
active_tools=['xwheel_zoom'],
responsive=True, height=300,
).opts(toolbar=None)
Timeseries¶
subcoordinate_y=Truegives each series its own y sub-axis — useful for multi-stock views with different scales.- Access datetime index components directly:
'index.month','index.hour','index.year'work inside hvPlot for aggregation. - Use
xformatter=DatetimeTickFormatter(months='%b %Y')for custom date formatting. - For large timeseries, use
downsample=True(LTTB algorithm) to reduce points sent to the browser while preserving visual shape. Updates dynamically on zoom.
import hvplot.pandas # noqa
import numpy as np
from bokeh.models.formatters import DatetimeTickFormatter
apple = hvplot.sampledata.apple_stocks('pandas').set_index('date')
stocks = hvplot.sampledata.stocks('pandas').set_index('date')
# Each series gets its own y sub-axis
stocks.hvplot.line(y=['Apple', 'Amazon', 'Google', 'Meta'], subcoordinate_y=True)
# Custom date formatting + LTTB downsampling
apple.hvplot.line(
y='close',
xformatter=DatetimeTickFormatter(months='%b %Y'),
downsample=True,
)
# Aggregate by datetime component
apple.hvplot.heatmap(x='index.hour', y='index.month', C='close', cmap='reds', reduce_function=np.mean)
Subplots and Layouts¶
col=/row=creates a cleaner faceted grid thansubplots=Truefor categorical splits. Useshared_axes=Falsefor independent ranges.
stocks = hvplot.sampledata.stocks("pandas")
# Linked subplots, 2 per row
stocks.hvplot(
x="date", y=["Apple", "Amazon", "Google"],
subplots=True, shared_axes=False,
width=300, height=200,
).cols(2)
penguins = hvplot.sampledata.penguins("pandas")
# Faceted grid: one panel per species × island
penguins.hvplot.scatter(
x="bill_length_mm", y="bill_depth_mm",
col="species", row="island", alpha=0.5,
)
Statistical Functions¶
These are top-level functions, not .hvplot accessor methods.
import hvplot
import hvplot.pandas # noqa
penguins = hvplot.sampledata.penguins('pandas')[[
'bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g', 'species'
]].dropna()
apple = hvplot.sampledata.apple_stocks('pandas').set_index('date')
hvplot.scatter_matrix(penguins, c='species') # pairwise scatter with linked brushing
hvplot.parallel_coordinates(penguins, 'species') # multivariate structure
hvplot.andrews_curves(penguins, 'species') # Fourier-series class separation
hvplot.lag_plot(apple[['close']], lag=5, alpha=0.3) # autocorrelation detection