Loading...
Loading...
Best practices for developing advanced, interactive, and publication-quality data visualizations using HoloViz HoloViews
npx skill4agent add marcskovmadsen/holoviz-mcp holoviewsholoviews.plotpip install holoviews hvsampledata panel watchfilesearthquakesimport 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. |import hvsampledata
import holoviews as hv
# DO always run hv.extension() to load the HoloViews javascript extensions
# DO specify the backend you intend to use (e.g., "bokeh", "matplotlib", "plotly")
hv.extension("bokeh")
# Do keep the extraction, transformation and plotting of data clearly separate
# Extract: earthquakes sample data
data = hvsampledata.earthquakes("pandas")
# Transform: Group by mag_class and count occurrences
mag_class_counts = data.groupby('mag_class').size().reset_index(name='counts')
# DO Specify an *element* type. Here its hv.Bars, i.e. a Bar plot.
plot = hv.Bars(
# DO provide the data explicitly
data = mag_class_counts,
# DO always specify the key dimensions (kdims) and value dimensions (vdims) as a single value or a list of values
kdims='mag_class',
vdims='counts'
).opts(
# DO specify optional styling options using .opts()
line_color=None,
# DO specify optional plot options using .opts()
title='Earthquake Counts by Magnitude Class'
)
# If working in notebook DO output to plot:
plot
# If working in .py file DO use panel:
import panel as pn
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
# Instead provide pn.state.served check
if pn.state.served:
# DO always run pn.extension() to load panel javascript extensions
pn.extension()
# DO remember to add .servable to the panel components you want to serve with the app
pn.panel(plot, sizing_mode="stretch_both").servable()panel serve path/to/file.py --dev --showpython path_to_this_file.pydepth_classdepth_classimport 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(
# DO specify optional styling options using .opts()
line_color=None,
# DO specify optional plot options using .opts()
title='Earthquake Counts by Magnitude Class and Depth Class',
width=800,
)
# If working in notebook DO output to plot:
plot
# If working in .py file DO use panel:
import panel as pn
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
# Instead provide pn.state.served check
if pn.state.served:
# DO always run pn.extension() to load panel javascript extensions
pn.extension()
# DO remember to add .servable to the panel components you want to serve with the app
pn.panel(plot, sizing_mode="stretch_both").servable().layoutimport 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(
# DO specify optional styling options using .opts()
line_color=None,
width=800,
).layout()
# If working in notebook DO output to plot:
plot
# If working in .py file DO use panel:
import panel as pn
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
# Instead provide pn.state.served check
if pn.state.served:
# DO always run pn.extension() to load panel javascript extensions
pn.extension()
# DO remember to add .servable to the panel components you want to serve with the app
pn.panel(plot, sizing_mode="stretch_both").servable().layout().overlay()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(
# DO specify optional styling options using .opts()
line_color=None,
width=800,
).overlay()
# If working in notebook DO output to plot:
plot
# If working in .py file DO use panel:
import panel as pn
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
# Instead provide pn.state.served check
if pn.state.served:
# DO always run pn.extension() to load panel javascript extensions
pn.extension()
# DO remember to add .servable to the panel components you want to serve with the app
pn.panel(plot, sizing_mode="stretch_both").servable()# ============================================================================
# Publication-Quality Bar Chart - HoloViews Best Practices Example
# ============================================================================
# Demonstrates:
# - Data extraction, transformation, and visualization separation
# - Custom Bokeh themes for consistent styling
# - Interactive tooltips with formatted data
# - Text annotations on bars
# - Professional fonts, grids, and axis formatting
# - Panel integration for web serving
# ============================================================================
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
# ============================================================================
# BOKEH THEME SETUP - Define global styling
# ============================================================================
ACCENT_COLOR = '#007ACC' # Professional blue
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", # Remove tick marks
'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;
}}
"""
]
},
# Does not work via Theme, so added here for reference purposes until I figure out how to do it
'Tooltip': {
"stylesheets": [f"""
.bk-tooltip-row-label {{
color: {ACCENT_COLOR} !important;
}}"""]
}
}
})
# Apply theme globally - affects all plots
hv.renderer('bokeh').theme = create_bokeh_theme()
# ============================================================================
# HOLOVIEWS OPTS SETUP - Define global configuration
# ============================================================================
GLOBAL_BACKEND_OPTS={
'plot.xgrid.visible': False, # Only horizontal grid lines
'plot.ygrid.visible': True,
'plot.ygrid.grid_line_color': "black",
'plot.ygrid.grid_line_alpha': 0.1,
'plot.min_border_left': 80, # Add padding on left (for y-axis label)
'plot.min_border_bottom': 80, # Add padding on bottom (for x-axis label)
'plot.min_border_right': 30, # Add padding on right
'plot.min_border_top': 80, # Add padding on top
}
ElementPlot.param.backend_opts.default = GLOBAL_BACKEND_OPTS
ElementPlot.param.yformatter.default = NumeralTickFormatter(format='0a') # 1k, ...
hv.opts.defaults(
hv.opts.Bars(
color=ACCENT_COLOR, # Professional blue
line_color=None, # Remove bar borders
),
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']
# ============================================================================
# DATA PIPELINE - Separate extraction, transformation, and plotting
# ============================================================================
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"""
# Aggregate: count events and calculate average depth per magnitude class
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)
)
# Add percentage column for tooltips
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']
# Main bar chart with professional styling
bar_chart = hv.Bars(aggregated_data, kdims='mag_class', vdims=['event_count', 'percentage', 'avg_depth']).opts(
# Titles and labels
title='Earthquake Distribution by Magnitude',
xlabel='Magnitude',
ylabel='Number of Events',
# Interactivity
# hover_cols = ["mag_class", "event_count", "percentage", "avg_depth"],
hover_tooltips=[
('Magnitude', '@mag_class'),
('Events', '@event_count{0,0}'), # Format: 1,234
('Percentage', '@percentage{0 a}%'), # Format: 45%
('Avg Depth', '@avg_depth{0f} km') # Format: 99 km
],
default_tools=default_tools
)
# Add text labels above bars
labels_data = aggregated_data.copy()
labels_data['label_y'] = labels_data['event_count'] + 20 # Offset above bars
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}'), # Format: 1,234
# tooltips below do currently not work on Labels
# ('Percentage', '@percentage{0 a}%'), # Format: 45%
# ('Avg Depth', '@avg_depth{0f} km'), # Format: 99 km
],
default_tools=default_tools
)
# Overlay: bar chart * text labels
return bar_chart * text_labels
def create_plot():
"""Main function: Extract → Transform → Plot"""
# Extract: Get raw data
earthquake_data = get_earthquake_data()
# Transform: Aggregate and calculate statistics
aggregated = aggregate_by_magnitude(earthquake_data)
# Visualize: Create publication-quality chart
chart = create_bar_chart(aggregated)
return chart
# ============================================================================
# PANEL APP SETUP
# ============================================================================
# Serve the chart when running with Panel
if pn.state.served:
# Load Panel JavaScript extensions
pn.extension()
# Apply custom Bokeh theme (override the global theme)
# Create and serve the chart
plot = create_plot()
pn.panel(plot, sizing_mode="stretch_both", margin=25).servable()hv.extension()import holoviews as hv
hv.extension()
...from bokeh.models.formatters import NumeralTickFormatter
plot.opts(
yformatter=NumeralTickFormatter(format='0.00a'), # Format as 1.00M, 2.50M, etc.
)| Input | Format String | Output |
|---|---|---|
| 1230974 | '0.0a' | 1.2m |
| 1460 | '0 a' | 1 k |
| -104000 | '0a' | -104k |
hv.savehv.save(some_plot, 'some_plot.html')searchhv_listhv_getskill_getholoviz-mcphvholoviz-mcp searchholoviz-mcp hv listholoviz-mcp hv gettestspytest tests/path/to/test_file.pypanel serve path_to_file.py --dev --show--show--dev--port {port-number}--autoreloadpython path_to_file.pypn.Column, pn.Tabs, pn.Accordion