Publication Quality Bar Chart

I created this example of a publication quality bar chart inspired by A Complete Guide to Bar Charts | Atlassian.

Wanted to share and persist it because it took some experimentation and time to create.

Code
# ============================================================================
# Publication-Quality Bar Chart - hvPlot 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 hvplot.pandas  # noqa: F401
import panel as pn
import hvsampledata
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

def create_bokeh_theme(font_family='Roboto', 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 = aggregated_data.hvplot.bar(
        x='mag_class',
        y='event_count',

        # 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.7%
            ('Avg Depth', '@avg_depth{0f} km')  # Format: 99 km
        ],
    ).opts(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 = labels_data.hvplot.labels(
        x='mag_class',
        y='label_y',
        text='event_count',
        hover_cols = ["mag_class", "event_count"],
        hover_tooltips=[
            ('Magnitude', '@mag_class'),
            ('Events', '@event_count{0,0}'),      # Format: 1,234
        ],
    ).opts(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
    chart = create_plot()
    pn.panel(chart, sizing_mode="stretch_both", margin=25).servable()

Serve with

panel serve script.py

I could not

  • Figure out how to set the color of the tooltip text (change it from light blue to ACCENT_COLOR)
  • Add additional tooltips to the labels. See HoloViews #6769
1 Like

I’ve been really interested in this idea too and have saved a few links to use as reference and potentially implement one day in hvplot as utils

1 Like