hvPlot¶
Skill version 2026.08.03
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¶
- References — composing exploratory and distilling explanatory plots
- Lookup — plotting-options reference and site search
- Dependencies
- Serving and Iterating
- Plot Labels
- Geographic Plots
- Hover Tooltips
- Managing Dimensions
- Big Data
- Styling
- Interaction
- Timeseries
- Subplots and Layouts
- Statistical Functions
References¶
Read these for specialized topics. Each is a standalone document you can load on demand.
- Composing Exploratory Plots — layered marks (violin/box/scatter), shared-axis layout for comparison, overlay discipline, faceting around the nested-categorical-axis limit, ordered categoricals, cross-filtering with linked selections
- Distilling Explanatory Plots — one-message charts for an audience: takeaway title, gray context + one highlight, direct inline labels, in-place annotation, killing chart junk, assuming no interaction
Lookup¶
- Plotting options by group (axes, geographic, styling, resampling):
https://hvplot.holoviz.org/en/docs/latest/ref/plotting_options/{group}.html - Anything else: web-search
https://hvplot.holoviz.org/search.html?q=<topic>.
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.
Any example below (or elsewhere in this skill set) that calls hvplot.sampledata.<name>(...) (e.g. penguins, earthquakes, apple_stocks, stocks) requires the separate hvsampledata package. It is not installed by hvPlot itself — install it explicitly (pip install hvsampledata / conda install hvsampledata) or the call raises AttributeError: Install the package 'hvsampledata' to access datasets from the 'sampledata' module of hvPlot.
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. It requiresgeoviews, and so doescoastline=True— both raiseModuleNotFoundError: The 'geoviews' package must be installed in order to use geographic features.tiles=alone does not need geoviews, so reach fortiles=when you just want a basemap under lon/lat data.tiles=acceptsTrue, anxyzservices.TileProvider, or a tile-source name — but the set of valid names depends ongeo=, becausegeo=Falseresolves against HoloViews' tile sources (10) andgeo=Trueagainst GeoViews' (32).- Portable either way (safe default):
"CartoDark","CartoLight","EsriImagery","EsriNatGeo","EsriReference","EsriTerrain","EsriUSATopo","OSM","OpenTopoMap". "EsriStreet"works only withoutgeo=True; undergeo=Trueit raisesValueError: EsriStreet tiles not recognized. Use"EsriWorldStreetMap"there instead.geo=Trueadditionally unlocks GeoViews-only names ("CartoMidnight","EsriWorldHillshade","EsriWorldPhysical", the polar/ocean sets, …) that fail without it.- Never use
"CartoDB.DarkMatter"or"CartoDB Dark_Matter"— rejected in both modes. - Enumerate the live set with
holoviews.element.tiles.tile_sourcesorgeoviews.tile_sources.tile_sources. 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 and hvPlot plots that — the y-axis is labelledindexwith numeric ticks instead of your category labels. With the index set you get proper categorical factors and correct axis labels. (Passingx=/y=explicitly also works.) - 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 error for bar/barh — it silently applies only the first entry to every bar, which is easy to miss. For stacked bars withby=, pass the same dict viacmap=. - Simplify when labels carry the information:
xaxis=False,yaxis=False, andshow_frame=Falsevia.opts()(it's a HoloViews plot option, not a bare hvPlot kwarg). - 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()forresponsive/sizing — hvPlot writes its ownwidth=700onto the element that.opts(responsive=True)cannot remove. See Plotting in Panel for the full explanation. - 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¶
For the full guidance on stripping toolbar/tool chart junk (toolbar=None, default_tools=[], active_tools=[]), see Decluttering Plots. A supplementary/alternative option for polished output without fully removing the toolbar: backend_opts={"plot.toolbar.autohide": True} (hides it until hover).
- 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. Set the active tool withactive_tools=['xwheel_zoom'](a list).
# 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. Pick components the data actually varies over — on daily data every timestamp has hour 0, so anindex.houraxis collapses to a single value and the chart says nothing. - 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 — year × month, both of which vary in daily data
apple.hvplot.heatmap(x='index.year', 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.- Narrow subplots (e.g.
width=300) crowd datetime tick labels until they overlap. Pass a lowerxticks=count (an integer, not a rotation) so fewer labels are drawn and they stay readable —xrotation=does not affect hvPlot/Bokeh datetime axes.
stocks = hvplot.sampledata.stocks("pandas")
# Linked subplots, 2 per row — xticks keeps narrow datetime axes readable
stocks.hvplot(
x="date", y=["Apple", "Amazon", "Google"],
subplots=True, shared_axes=False,
width=300, height=200, xticks=4,
).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.
parallel_coordinatesplots each numeric column on its own vertical axis but shares one y-scale across all of them. If the columns have very different magnitudes (e.g.body_mass_gin the thousands next tobill_length_mmin the tens), the small-scale columns flatten to a line near zero and the chart stops being readable. Normalize/rescale columns to a common range (e.g. min-max or z-score) before passing them in, unless they're already comparable.
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
# Rescale to [0, 1] first — parallel_coordinates shares one y-axis across all columns
normalized = penguins.copy()
num_cols = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
normalized[num_cols] = (normalized[num_cols] - normalized[num_cols].min()) / (
normalized[num_cols].max() - normalized[num_cols].min()
)
hvplot.parallel_coordinates(normalized, 'species') # multivariate structure
hvplot.andrews_curves(penguins, 'species') # Fourier-series class separation
hvplot.lag_plot(apple[['close']], lag=5, alpha=0.3) # autocorrelation detection