| name | holoviews |
| description | Best practices for developing advanced, interactive, and publication-quality data visualizations using HoloViz HoloViews |
| metadata | {"version":"1.0.0","author":"holoviz","category":"data-visualization","difficulty":"intermediate"} |
HoloViews Development Skills
This document provides best practices for developing plots and charts with HoloViz HoloViews in notebooks and .py files.
Please develop as an Expert Python Developer developing advanced data-driven, analytics and testable data visualisations, dashboards and applications would do. Keep the code short, concise, documented, testable and professional.
Dependencies
Core dependencies provided with the holoviews Python package:
- holoviews: Declarative data visualization library with composable elements. Best for: complex multi-layered plots, advanced interactivity (linked brushing, selection), when you need fine control over plot composition, scientific visualizations. More powerful but steeper learning curve than hvPlot. hvPlot is built upon holoviews.
- colorcet: Perceptually uniform colormaps
- panel: Provides widgets and layouts enabling tool, dashboard and data app development.
- param: A declarative approach to creating classes with typed, validated, and documented parameters. Fundamental to the reactive programming model of hvPlot and the rest of the HoloViz ecosystem.
- pandas: Industry-standard DataFrame library for tabular data. Best for: data cleaning, transformation, time series analysis, datasets that fit in memory. The default choice for most data work.
Optional dependencies from the HoloViz Ecosystem:
- hvplot: Easy to use plotting library with Pandas
.plot like API. Built on top of HoloViews.
- datashader: Renders large datasets (millions+ points) into images for visualization. Best for: big data visualization, geospatial datasets, scatter plots with millions of points, heatmaps of dense data. Requires hvPlot or HoloViews as frontend.
- geoviews: Geographic data visualization with map projections and tile sources. Best for: geographic/geospatial plots, map-based dashboards, when you need coordinate systems and projections. Built on HoloViews, works seamlessly with hvPlot.
- holoviz-mcp: Model Context Protocol server for HoloViz ecosystem. Provides access to detailed documentation, component search and agent skills.
- hvsampledata: Shared datasets for the HoloViz projects.
Installation for Development
pip install holoviews hvsampledata panel watchfiles
For development in .py files DO always include watchfiles for Panel hotreload.
Earthquake Sample Data
In the example below we will use the earthquakes sample data:
import hvsampledata
hvsampledata.earthquakes("pandas")
Tabular record of earthquake events from the USGS Earthquake Catalog that provides detailed
information including parameters such as time, location as latitude/longitude coordinates
and place name, depth, and magnitude. The dataset contains 596 events.
Note: The columns `depth_class` and `mag_class` were created by categorizing numerical values from
the `depth` and `mag` columns in the original dataset using custom-defined binning:
Depth Classification
| depth | depth_class |
|-----------|--------------|
| Below 70 | Shallow |
| 70 - 300 | Intermediate |
| Above 300 | Deep |
Magnitude Classification
| mag | mag_class |
|-------------|-----------|
| 3.9 - <4.9 | Light |
| 4.9 - <5.9 | Moderate |
| 5.9 - <6.9 | Strong |
| 6.9 - <7.9 | Major |
Schema
------
| name | type | description |
|:------------|:-----------|:--------------------------------------------------------------------|
| time | datetime | UTC Time when the event occurred. |
| lat | float | Decimal degrees latitude. Negative values for southern latitudes. |
| lon | float | Decimal degrees longitude. Negative values for western longitudes |
| depth | float | Depth of the event in kilometers. |
| depth_class | category | The depth category derived from the depth column. |
| mag | float | The magnitude for the event. |
| mag_class | category | The magnitude category derived from the mag column. |
| place | string | Textual description of named geographic region near to the event. |
Reference Data Exploration Example
Below is a simple reference example for data exploration.
import hvsampledata
import holoviews as hv
hv.extension("bokeh")
data = hvsampledata.earthquakes("pandas")
mag_class_counts = data.groupby('mag_class').size().reset_index(name='counts')
plot = hv.Bars(
data = mag_class_counts,
kdims='mag_class',
vdims='counts'
).opts(
line_color=None,
title='Earthquake Counts by Magnitude Class'
)
plot
import panel as pn
if pn.state.served:
pn.extension()
pn.panel(plot, sizing_mode="stretch_both").servable()
If working in a .py file DO serve the plot with hotreload:
panel serve path/to/file.py --dev --show
DONT serve with python path_to_this_file.py.
Reference Group By
In this example we also groupby depth_class, i.e. a dropdown widget is added to select the depth_class to filter by.
import hvsampledata
import holoviews as hv
hv.extension("bokeh")
data = hvsampledata.earthquakes("pandas")
mag_class_counts = data.groupby(['mag_class', 'depth_class']).size().reset_index(name='counts')
print(mag_class_counts)
plot = hv.Bars(
data = mag_class_counts,
kdims=['mag_class','depth_class'],
vdims='counts',
).groupby(
"depth_class"
).opts(
line_color=None,
title='Earthquake Counts by Magnitude Class and Depth Class',
width=800,
)
plot
import panel as pn
if pn.state.served:
pn.extension()
pn.panel(plot, sizing_mode="stretch_both").servable()
If we add .layout the data will be visualized as 3 individual plots (one per depth_class):
import hvsampledata
import holoviews as hv
hv.extension("bokeh")
data = hvsampledata.earthquakes("pandas")
mag_class_counts = data.groupby(['mag_class', 'depth_class']).size().reset_index(name='counts')
print(mag_class_counts)
plot = hv.Bars(
data = mag_class_counts,
kdims=['mag_class','depth_class'],
vdims='counts',
).groupby(
"depth_class"
).opts(
line_color=None,
width=800,
).layout()
plot
import panel as pn
if pn.state.served:
pn.extension()
pn.panel(plot, sizing_mode="stretch_both").servable()
If instead of .layout() we add .overlay(), one plot will be created, but the depth_class'es will be visualized by different colors.
import hvsampledata
import holoviews as hv
hv.extension("bokeh")
data = hvsampledata.earthquakes("pandas")
mag_class_counts = data.groupby(['mag_class', 'depth_class']).size().reset_index(name='counts')
print(mag_class_counts)
plot = hv.Bars(
data = mag_class_counts,
kdims=['mag_class','depth_class'],
vdims='counts',
).groupby(
"depth_class"
).opts(
line_color=None,
width=800,
).overlay()
plot
import panel as pn
if pn.state.served:
pn.extension()
pn.panel(plot, sizing_mode="stretch_both").servable()
Note: This works better for Curve or Scatter plots
Reference Publication Quality Bar Chart
import hvsampledata
import panel as pn
from bokeh.models.formatters import NumeralTickFormatter
from bokeh.themes import Theme
import holoviews as hv
from holoviews.plotting.bokeh import ElementPlot
ACCENT_COLOR = '#007ACC'
FONT_FAMILY = 'Roboto'
def create_bokeh_theme(font_family=FONT_FAMILY, accent_color=ACCENT_COLOR):
"""Create custom theme with specified font. Default: Roboto"""
return Theme(json={
'attrs': {
'Title': {
'text_font': font_family,
'text_font_size': '16pt',
'text_font_style': 'bold'
},
'Axis': {
'axis_label_text_font': font_family,
'axis_label_text_font_size': '12pt',
'axis_label_text_font_style': 'bold',
'major_label_text_font': font_family,
'major_label_text_font_size': '10pt',
'major_tick_line_color': "black",
'minor_tick_line_color': None
},
'Plot': {
'background_fill_color': '#fafafa',
'border_fill_color': '#fafafa'
},
'Legend': {
'label_text_font': font_family,
'label_text_font_size': '10pt'
},
'Toolbar': {
"autohide": True,
"logo": None,
"stylesheets": [
f"""
.bk-OnOffButton.bk-active{{
border-color: {accent_color} !important;
}}
"""
]
},
'Tooltip': {
"stylesheets": [f"""
.bk-tooltip-row-label {{
color: {ACCENT_COLOR} !important;
}}"""]
}
}
})
hv.renderer('bokeh').theme = create_bokeh_theme()
GLOBAL_BACKEND_OPTS={
'plot.xgrid.visible': False,
'plot.ygrid.visible': True,
'plot.ygrid.grid_line_color': "black",
'plot.ygrid.grid_line_alpha': 0.1,
'plot.min_border_left': 80,
'plot.min_border_bottom': 80,
'plot.min_border_right': 30,
'plot.min_border_top': 80,
}
ElementPlot.param.backend_opts.default = GLOBAL_BACKEND_OPTS
ElementPlot.param.yformatter.default = NumeralTickFormatter(format='0a')
hv.opts.defaults(
hv.opts.Bars(
color=ACCENT_COLOR,
line_color=None,
),
hv.opts.Labels(
text_baseline='bottom',
text_font_size='11pt',
text_font_style='normal',
text_color='#333333',
),
)
hv.Cycle.default_cycles["default_colors"] = [ACCENT_COLOR, '#00948A', '#7E59BD', '#FFA20C', '#DA4341', '#D6F1FF', '#DAF5F4', '#F0E8FF', '#FFF8EA', '#FFF1EA', '#001142', '#003336', '#290031', '#371F00', '#3A0C13']
def get_earthquake_data():
"""Extract raw earthquake data from sample dataset"""
return hvsampledata.earthquakes("pandas")
def aggregate_by_magnitude(earthquake_data):
"""Transform: Group earthquakes by magnitude class with statistics"""
aggregated = (
earthquake_data
.groupby('mag_class', observed=True)
.agg({'mag': 'count', 'depth': 'mean'})
.reset_index()
.rename(columns={'mag': 'event_count', 'depth': 'avg_depth'})
.sort_values('event_count', ascending=False)
)
aggregated['percentage'] = (
aggregated['event_count'] / aggregated['event_count'].sum() * 100
)
return aggregated
def create_bar_chart(aggregated_data):
"""Create publication-quality bar chart with labels and tooltips"""
default_tools=['save']
bar_chart = hv.Bars(aggregated_data, kdims='mag_class', vdims=['event_count', 'percentage', 'avg_depth']).opts(
title='Earthquake Distribution by Magnitude',
xlabel='Magnitude',
ylabel='Number of Events',
hover_tooltips=[
('Magnitude', '@mag_class'),
('Events', '@event_count{0,0}'),
('Percentage', '@percentage{0 a}%'),
('Avg Depth', '@avg_depth{0f} km')
],
default_tools=default_tools
)
labels_data = aggregated_data.copy()
labels_data['label_y'] = labels_data['event_count'] + 20
text_labels = hv.Labels(labels_data, kdims=['mag_class', 'label_y'], vdims=['event_count', 'percentage', 'avg_depth']).opts(
hover_tooltips=[
('Magnitude', '@mag_class'),
('Events', '@event_count{0,0}'),
],
default_tools=default_tools
)
return bar_chart * text_labels
def create_plot():
"""Main function: Extract → Transform → Plot"""
earthquake_data = get_earthquake_data()
aggregated = aggregate_by_magnitude(earthquake_data)
chart = create_bar_chart(aggregated)
return chart
if pn.state.served:
pn.extension()
plot = create_plot()
pn.panel(plot, sizing_mode="stretch_both", margin=25).servable()
General Instructions
- In a notebook always run
hv.extension() to load any Javascript dependencies.
import holoviews as hv
hv.extension()
...
- Prefer Bokeh > Plotly > Matplotlib plotting backend for interactivity
- DO use bar charts over pie Charts. Pie charts are not supported.
- DO use NumeralTickFormatter and 'a' formatter for easy axis formatting:
from bokeh.models.formatters import NumeralTickFormatter
plot.opts(
yformatter=NumeralTickFormatter(format='0.00a'),
)
| Input | Format String | Output |
|---|
| 1230974 | '0.0a' | 1.2m |
| 1460 | '0 a' | 1 k |
| -104000 | '0a' | -104k |
Saving a plot
You can save a plot to html with hv.save:
hv.save(some_plot, 'some_plot.html')
Recommended Plot Types
Curve - Line plots for time series and continuous data
Scatter - Scatter plots for exploring relationships between variables
Bars - Bar charts for categorical comparisons
Histogram - Histograms for distribution analysis
Area - Area plots for stacked or filled visualizations
Workflows
Lookup additional information
- If the HoloViz MCP server tools are available, DO use them:
search (documentation), hv_list (available elements), hv_get (docstrings and options), skill_get (best-practice skills).
- If MCP tools are not available but the
holoviz-mcp CLI is installed (also available as hv), use the equivalent CLI commands: holoviz-mcp search, holoviz-mcp hv list, holoviz-mcp hv get.
- If neither is available, DO search the web. For example searching the HoloViews website for relevant information via https://holoviews.org url.
Test the app with pytest
DO add tests to the tests folder and run them with pytest tests/path/to/test_file.py.
- DO separate data extraction and transformation from plotting code.
- DO fix any test errors and rerun the tests
- DO run the tests and fix errors before displaying or serving the plots
Test the app manually with panel serve
DO always start and keep running a development server panel serve path_to_file.py --dev --show with hot reload while developing!
- Due to
--show flag, a browser tab will automatically open showing your app.
- Due to
--dev flag, the panel server and app will automatically reload if you change the code.
- The app will be served at http://localhost:5006/.
- DO make sure the correct virtual environment is activated before serving the app.
- DO fix any errors that show up in the terminal. Consider adding new tests to ensure they don't happen again.
- DON'T stop or restart the server after changing the code. The app will automatically reload.
- If you see 'Cannot start Bokeh server, port 5006 is already in use' in the terminal, DO serve the app on another port with
--port {port-number} flag.
- DO remind the user to test the plot on multiple screen sizes (desktop, tablet, mobile)
- DON'T use legacy
--autoreload flag
- DON't run
python path_to_file.py to test or serve the app.
- DO use
pn.Column, pn.Tabs, pn.Accordion to layout multiple plots
- If you close the server to run other commands DO remember to restart it.