Basic Dumbbell Chart — Plotly

A dumbbell chart (also called a connected dot plot or Cleveland dot plot) compares two values for each category by displaying two dots connected by a line. It effectively visualizes differences, changes, or ranges between two data points such as before/after comparisons, gaps, or min/max values. The connected dots make it easy to see both the magnitude and direction of change.

Basic Dumbbell Chart rendered with Plotly

Python source (Plotly)

""" anyplot.ai
dumbbell-basic: Basic Dumbbell Chart
Library: plotly 6.8.0 | Python 3.13.14
Quality: 90/100 | Updated: 2026-06-30
"""

import os

import plotly.graph_objects as go


# Theme-adaptive chrome tokens (Imprint style guide)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette data colors (theme-independent)
BEFORE_COLOR = "#009E73"  # position 1 — first categorical series
AFTER_COLOR = "#C475FD"  # position 2 — second series
AMBER = "#DDCC77"  # semantic anchor — warning / regression flag

# Employee satisfaction scores before and after policy changes (deterministic)
categories = [
    "Engineering",
    "Sales",
    "Marketing",
    "Customer Support",
    "Finance",
    "Human Resources",
    "Operations",
    "Product",
    "Legal",
]
before = [62, 71, 58, 45, 68, 52, 64, 73, 70]
after = [78, 82, 75, 69, 74, 71, 79, 85, 67]

# Sort ascending by delta (regression at top, largest gain at bottom)
data = sorted(zip(categories, before, after, strict=True), key=lambda x: x[2] - x[1])
categories = [d[0] for d in data]
before = [d[1] for d in data]
after = [d[2] for d in data]
deltas = [a - b for a, b in zip(after, before, strict=True)]

fig = go.Figure()

# Connecting lines — amber for regression, subtle ink-soft for improvement
for i in range(len(categories)):
    is_regression = deltas[i] < 0
    fig.add_trace(
        go.Scatter(
            x=[before[i], after[i]],
            y=[categories[i], categories[i]],
            mode="lines",
            line={"color": AMBER if is_regression else INK_SOFT, "width": 2.5 if is_regression else 1.5},
            showlegend=False,
            hoverinfo="skip",
        )
    )

# "Before" markers — Imprint green (position 1)
fig.add_trace(
    go.Scatter(
        x=before,
        y=categories,
        mode="markers",
        marker={"size": 14, "color": BEFORE_COLOR, "line": {"color": PAGE_BG, "width": 2}},
        name="Before",
        hovertemplate="<b>%{y}</b><br>Before: %{x}/100<extra></extra>",
    )
)

# "After" markers — Imprint lavender (position 2)
fig.add_trace(
    go.Scatter(
        x=after,
        y=categories,
        mode="markers",
        marker={"size": 14, "color": AFTER_COLOR, "line": {"color": PAGE_BG, "width": 2}},
        name="After",
        hovertemplate="<b>%{y}</b><br>After: %{x}/100<extra></extra>",
    )
)

# Delta labels to the right of each dumbbell — amber for regression
annotations = []
for cat, b, a, delta in zip(categories, before, after, deltas, strict=True):
    sign = "+" if delta >= 0 else ""
    label_color = AMBER if delta < 0 else INK_SOFT
    annotations.append(
        {
            "x": max(b, a) + 1.5,
            "y": cat,
            "text": f"{sign}{delta} pts",
            "showarrow": False,
            "font": {"size": 11, "color": label_color},
            "xanchor": "left",
            "yanchor": "middle",
        }
    )

fig.update_layout(
    autosize=False,
    title={
        "text": "Employee Satisfaction · dumbbell-basic · python · plotly · anyplot.ai",
        "font": {"size": 16, "color": INK},
        "x": 0.5,
        "xanchor": "center",
    },
    xaxis={
        "title": {"text": "Satisfaction Score (out of 100)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "range": [35, 100],
        "gridcolor": GRID,
        "gridwidth": 1,
        "linecolor": INK_SOFT,
        "zerolinecolor": INK_SOFT,
        "showgrid": True,
    },
    yaxis={
        "title": {"text": "Department", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "linecolor": INK_SOFT,
        "zerolinecolor": INK_SOFT,
        "showgrid": False,
    },
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK},
    legend={
        "orientation": "h",
        "yanchor": "bottom",
        "y": 1.02,
        "xanchor": "center",
        "x": 0.5,
        "font": {"size": 10, "color": INK_SOFT},
        "bgcolor": ELEVATED_BG,
        "bordercolor": INK_SOFT,
        "borderwidth": 1,
    },
    annotations=annotations,
    margin={"l": 150, "r": 90, "t": 80, "b": 60},
)

fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Basic Dumbbell Chart on anyplot.ai.

Other implementations