Dumbbell plot

Dumbbell plots visualize change between two data points with connecting lines. Features include flexible orientation, adjustable point sizes, line widths, and optional glow effects.

from bokeh.plotting import figure, show, output_file, curdoc
from bokeh.models import HoverTool
from bokeh.io import output_notebook
curdoc().theme = 'dark_minimal'
# curdoc().theme = 'light_minimal'

def dumbbell_plot(data, start_col, end_col, category_col, 
                  orientation='horizontal', title='Dumbbell Plot',
                  start_color='#3498db', end_color='#e74c3c', 
                  line_color='#95a5a6', width=800, height=400,
                  start_label='Start', end_label='End',
                  glow=True, line_width=2, point_size=12):
    """
    Create a dumbbell plot using Bokeh.
    
    Parameters:
    -----------
    data : list of dict or pandas DataFrame
        Data containing categories and start/end values
    start_col : str
        Column name for start values
    end_col : str
        Column name for end values
    category_col : str
        Column name for categories
    orientation : str
        'horizontal' or 'vertical'
    title : str
        Plot title
    start_color : str
        Color for start points
    end_color : str
        Color for end points
    line_color : str
        Color for connecting lines
    width : int
        Plot width in pixels
    height : int
        Plot height in pixels
    start_label : str
        Label for start points in legend
    end_label : str
        Label for end points in legend
    glow : bool
        Enable glow effect around points
    line_width : int or float
        Width of the connecting lines (default: 2)
    point_size : int or float
        Size of the point circles (default: 12)
    
    Returns:
    --------
    Bokeh figure object
    """
    
    # Convert to list of dicts if needed
    if hasattr(data, 'to_dict'):
        data = data.to_dict('records')
    
    categories = [str(d[category_col]) for d in data]
    starts = [d[start_col] for d in data]
    ends = [d[end_col] for d in data]
    
    if orientation == 'horizontal':
        p = figure(y_range=categories, width=width, height=height, 
                   title=title, toolbar_location='above')
        
        # Draw lines
        for cat, start, end in zip(categories, starts, ends):
            p.line([start, end], [cat, cat], line_width=line_width, 
                   color=line_color, alpha=0.6)
        
        # Add glow effect if enabled
        if glow:
            # Outer glow layers for start points
            p.circle(starts, categories, size=point_size*2, color=start_color, 
                    alpha=0.1)
            p.circle(starts, categories, size=point_size*1.5, color=start_color, 
                    alpha=0.2)
            p.circle(starts, categories, size=point_size*1.17, color=start_color, 
                    alpha=0.3)
            
            # Outer glow layers for end points
            p.circle(ends, categories, size=point_size*2, color=end_color, 
                    alpha=0.1)
            p.circle(ends, categories, size=point_size*1.5, color=end_color, 
                    alpha=0.2)
            p.circle(ends, categories, size=point_size*1.17, color=end_color, 
                    alpha=0.3)
        
        # Draw main circles on top
        p.circle(starts, categories, size=point_size, color=start_color, 
                legend_label=start_label, alpha=0.9)
        
        p.circle(ends, categories, size=point_size, color=end_color, 
                legend_label=end_label, alpha=0.9)
        
        p.xaxis.axis_label = 'Value'
        p.yaxis.axis_label = 'Category'
        
    else:  # vertical
        p = figure(x_range=categories, width=width, height=height, 
                   title=title, toolbar_location='above')
        
        # Draw lines
        for cat, start, end in zip(categories, starts, ends):
            p.line([cat, cat], [start, end], line_width=line_width, 
                   color=line_color, alpha=0.6)
        
        # Add glow effect if enabled
        if glow:
            # Outer glow layers for start points
            p.circle(categories, starts, size=point_size*2, color=start_color, 
                    alpha=0.1)
            p.circle(categories, starts, size=point_size*1.5, color=start_color, 
                    alpha=0.2)
            p.circle(categories, starts, size=point_size*1.17, color=start_color, 
                    alpha=0.3)
            
            # Outer glow layers for end points
            p.circle(categories, ends, size=point_size*2, color=end_color, 
                    alpha=0.1)
            p.circle(categories, ends, size=point_size*1.5, color=end_color, 
                    alpha=0.2)
            p.circle(categories, ends, size=point_size*1.17, color=end_color, 
                    alpha=0.3)
        
        # Draw main circles on top
        p.circle(categories, starts, size=point_size, color=start_color, 
                legend_label=start_label, alpha=0.9)
        
        p.circle(categories, ends, size=point_size, color=end_color, 
                legend_label=end_label, alpha=0.9)
        
        p.xaxis.axis_label = 'Category'
        p.yaxis.axis_label = 'Value'
        p.xaxis.major_label_orientation = 0.8
    
    # Add hover tool
    hover = HoverTool(tooltips=[
        ('Category', '@y' if orientation == 'horizontal' else '@x'),
        ('Value', '@x{0.0}' if orientation == 'horizontal' else '@y{0.0}')
    ])
    p.add_tools(hover)
    
    p.legend.location = 'center_right'
    p.legend.click_policy = 'hide'
    p.add_layout(p.legend[0], 'right')
    
    return p


# Example 1: WITH glow effect
print("Example 1: Horizontal with Glow Effect")
sales_data = [
    {'product': 'Product A', 'q1': 45, 'q4': 78},
    {'product': 'Product B', 'q1': 62, 'q4': 55},
    {'product': 'Product C', 'q1': 38, 'q4': 91},
    {'product': 'Product D', 'q1': 71, 'q4': 82},
    {'product': 'Product E', 'q1': 54, 'q4': 49},
]

output_file("dumbbell_with_glow.html")
p1 = dumbbell_plot(
    data=sales_data,
    start_col='q1',
    end_col='q4',
    category_col='product',
    orientation='horizontal',
    title='Sales Change: Q1 vs Q4 (With Glow)',
    start_label='Q1',
    end_label='Q4',
    start_color='orange',
    end_color='#2ecc71',
    width=700,
    height=400,
    glow=True
)
show(p1)


# Example 2: Vertical WITH glow
print("\nExample 2: Vertical with Glow Effect")
performance_data = [
    {'department': 'Sales', 'goal': 85, 'actual': 92},
    {'department': 'Marketing', 'goal': 75, 'actual': 68},
    {'department': 'Support', 'goal': 90, 'actual': 88},
    {'department': 'Engineering', 'goal': 80, 'actual': 95},
    {'department': 'HR', 'goal': 70, 'actual': 73},
]

output_file("dumbbell_vertical_glow.html")
p2 = dumbbell_plot(
    data=performance_data,
    start_col='goal',
    end_col='actual',
    category_col='department',
    orientation='vertical',
    title='Department Performance: Goal vs Actual (With Glow)',
    start_label='Goal',
    end_label='Actual',
    start_color='#f39c12',
    end_color='#9b59b6',
    width=600,
    height=500,
    glow=True
)
show(p2)


# Example 3: WITHOUT glow
print("\nExample 3: Horizontal without Glow")
temp_data = [
    {'city': 'New York', 'jan_temp': 32, 'jul_temp': 77},
    {'city': 'Los Angeles', 'jan_temp': 58, 'jul_temp': 73},
    {'city': 'Chicago', 'jan_temp': 26, 'jul_temp': 75},
    {'city': 'Houston', 'jan_temp': 53, 'jul_temp': 84},
    {'city': 'Phoenix', 'jan_temp': 58, 'jul_temp': 94},
    {'city': 'Miami', 'jan_temp': 68, 'jul_temp': 83},
]

output_file("dumbbell_no_glow.html")
p3 = dumbbell_plot(
    data=temp_data,
    start_col='jan_temp',
    end_col='jul_temp',
    category_col='city',
    orientation='horizontal',
    title='Temperature Range: January vs July (No Glow)',
    start_label='January',
    end_label='July',
    start_color='#3498db',
    end_color='#e74c3c',
    width=800,
    height=400,
    glow=False
)
show(p3)


# Example 4: Vertical WITHOUT glow
print("\nExample 4: Vertical without Glow")
revenue_data = [
    {'quarter': 'Q1 2024', 'budget': 120, 'actual': 135},
    {'quarter': 'Q2 2024', 'budget': 130, 'actual': 125},
    {'quarter': 'Q3 2024', 'budget': 140, 'actual': 155},
    {'quarter': 'Q4 2024', 'budget': 150, 'actual': 148},
]

output_file("dumbbell_vertical_no_glow.html")
p4 = dumbbell_plot(
    data=revenue_data,
    start_col='budget',
    end_col='actual',
    category_col='quarter',
    orientation='vertical',
    title='Quarterly Revenue: Budget vs Actual (No Glow)',
    start_label='Budget',
    end_label='Actual',
    start_color='#1abc9c',
    end_color='#e67e22',
    width=600,
    height=450,
    glow=False,
    line_width=6,
    line_color='red',
    point_size=33
)
p4.background_fill_color = 'deepskyblue'
p4.background_fill_alpha = 0.5
p4.border_fill_color = 'navy'
show(p4)


2 Likes