How to Create a Lollipop Chart in ggplot2

Lollipop charts are an elegant alternative to bar charts that display the same information with less visual weight. Instead of solid bars, they use a line (the 'stem') extending from a baseline to a...

Key Insights

  • Lollipop charts reduce visual clutter compared to bar charts by replacing solid bars with points connected to a baseline, making them ideal for displaying ranked data with many categories
  • The core technique combines geom_segment() for the stem and geom_point() for the circle, with reorder() being essential for creating impactful sorted visualizations
  • Diverging lollipop charts excel at showing positive/negative comparisons like survey sentiment or performance variance, using conditional coloring to emphasize the direction of values

Introduction to Lollipop Charts

Lollipop charts are an elegant alternative to bar charts that display the same information with less visual weight. Instead of solid bars, they use a line (the “stem”) extending from a baseline to a point (the “lollipop”), which marks the actual value. This design reduces ink-to-data ratio while maintaining readability.

Use lollipop charts when you have ranked categorical data with many items. They shine when displaying 10-20+ categories where traditional bar charts become visually overwhelming. They’re particularly effective for showing rankings, survey results, or any scenario where the endpoint value matters more than the area under the curve.

The main advantages: reduced visual clutter, emphasis on the actual data point, and a modern aesthetic that works well in dashboards and reports. Let’s compare the same data visualized both ways:

library(ggplot2)
library(dplyr)

# Sample data: programming language popularity
languages <- data.frame(
  language = c("Python", "JavaScript", "Java", "C#", "C++", "PHP", "TypeScript", "Ruby"),
  popularity = c(28.1, 18.2, 16.5, 7.8, 6.9, 5.2, 4.8, 2.5)
)

# Bar chart
bar_plot <- ggplot(languages, aes(x = reorder(language, popularity), y = popularity)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Bar Chart", x = NULL, y = "Popularity (%)") +
  theme_minimal()

# Lollipop chart
lollipop_plot <- ggplot(languages, aes(x = reorder(language, popularity), y = popularity)) +
  geom_segment(aes(x = language, xend = language, y = 0, yend = popularity)) +
  geom_point(size = 4, color = "steelblue") +
  coord_flip() +
  labs(title = "Lollipop Chart", x = NULL, y = "Popularity (%)") +
  theme_minimal()

# Display both (using patchwork or gridExtra)
library(patchwork)
bar_plot | lollipop_plot

The lollipop version immediately feels lighter and draws your eye to the data points themselves rather than the mass of the bars.

Basic Lollipop Chart Setup

Creating a lollipop chart requires two geometric layers working together. The geom_segment() creates the stem, while geom_point() adds the circle at the end. The key is understanding how geom_segment() works: it needs starting coordinates (x, y) and ending coordinates (xend, yend).

For a vertical lollipop chart, the stem starts at y=0 (the baseline) and extends to your data value, while x remains constant for each category. Here’s the basic structure:

library(ggplot2)

# Create sample dataset
tech_adoption <- data.frame(
  technology = c("Cloud Computing", "AI/ML", "IoT", "Blockchain", "5G"),
  adoption_rate = c(78, 65, 52, 38, 29)
)

# Basic lollipop chart
ggplot(tech_adoption, aes(x = technology, y = adoption_rate)) +
  geom_segment(aes(x = technology, xend = technology, y = 0, yend = adoption_rate)) +
  geom_point() +
  labs(
    title = "Technology Adoption Rates",
    x = NULL,
    y = "Adoption Rate (%)"
  ) +
  theme_minimal()

This creates a functional lollipop chart, but it’s basic. Notice how x = technology and xend = technology keep the stem vertical, while y = 0 and yend = adoption_rate define the stem’s length.

Customizing the Lollipop Elements

Now let’s add visual polish. You can control stem thickness with the size parameter in geom_segment(), point size and color in geom_point(), and flip the orientation with coord_flip() for better label readability.

Horizontal lollipop charts are generally more readable because category labels don’t need rotation. Here’s an enhanced version:

ggplot(tech_adoption, aes(x = technology, y = adoption_rate)) +
  geom_segment(
    aes(x = technology, xend = technology, y = 0, yend = adoption_rate),
    color = "gray60",
    size = 1.5
  ) +
  geom_point(
    color = "#FF6B6B",
    size = 5
  ) +
  coord_flip() +
  labs(
    title = "Technology Adoption Rates in Enterprise",
    subtitle = "Percentage of organizations using each technology",
    x = NULL,
    y = "Adoption Rate (%)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    panel.grid.major.y = element_blank(),
    panel.grid.minor = element_blank()
  )

The thicker stems (size = 1.5) and larger points (size = 5) make the chart more readable. Using gray for stems and a distinct color for points creates visual hierarchy. Removing horizontal gridlines reduces clutter since the category labels provide sufficient reference.

Sorting and Ordering Data

Unsorted lollipop charts waste their potential. The power of this visualization comes from clearly showing rankings. Always sort your data by value unless there’s a compelling reason not to (like maintaining a natural order for time periods or categories).

The reorder() function sorts one variable by another. For factors, forcats::fct_reorder() provides more control:

library(forcats)

# Unsorted (poor practice)
unsorted <- ggplot(tech_adoption, aes(x = technology, y = adoption_rate)) +
  geom_segment(aes(x = technology, xend = technology, y = 0, yend = adoption_rate)) +
  geom_point(size = 4, color = "steelblue") +
  coord_flip() +
  labs(title = "Unsorted - Harder to Compare") +
  theme_minimal()

# Sorted by value (best practice)
sorted <- ggplot(tech_adoption, aes(x = reorder(technology, adoption_rate), y = adoption_rate)) +
  geom_segment(aes(x = technology, xend = technology, y = 0, yend = adoption_rate)) +
  geom_point(size = 4, color = "steelblue") +
  coord_flip() +
  labs(title = "Sorted - Clear Ranking", x = NULL) +
  theme_minimal()

# Using forcats for more control
sorted_forcats <- ggplot(tech_adoption, aes(x = fct_reorder(technology, adoption_rate), y = adoption_rate)) +
  geom_segment(aes(x = technology, xend = technology, y = 0, yend = adoption_rate)) +
  geom_point(size = 4, color = "steelblue") +
  coord_flip() +
  labs(title = "Sorted with forcats", x = NULL) +
  theme_minimal()

The sorted version immediately reveals the ranking pattern. Your eye can trace the descending (or ascending) order effortlessly. This is where lollipop charts truly outperform bar charts—the linear arrangement of points creates a clear visual path.

Advanced Styling and Annotations

For publication-ready charts, add value labels and refine the theme. Use geom_text() to display exact values, and customize theme elements for a professional appearance:

ggplot(tech_adoption, aes(x = reorder(technology, adoption_rate), y = adoption_rate)) +
  geom_segment(
    aes(x = technology, xend = technology, y = 0, yend = adoption_rate),
    color = "gray70",
    size = 1.5
  ) +
  geom_point(
    color = "#4ECDC4",
    size = 6
  ) +
  geom_text(
    aes(label = paste0(adoption_rate, "%")),
    hjust = -0.5,
    size = 3.5,
    color = "gray30"
  ) +
  coord_flip() +
  scale_y_continuous(
    limits = c(0, 90),
    breaks = seq(0, 80, 20),
    expand = c(0, 0)
  ) +
  labs(
    title = "Enterprise Technology Adoption in 2024",
    subtitle = "Based on survey of 500+ organizations",
    x = NULL,
    y = "Adoption Rate (%)",
    caption = "Source: Tech Trends Report 2024"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 16, margin = margin(b = 5)),
    plot.subtitle = element_text(size = 11, color = "gray40", margin = margin(b = 15)),
    plot.caption = element_text(size = 9, color = "gray50", hjust = 0),
    axis.text = element_text(size = 10),
    panel.grid.major.y = element_blank(),
    panel.grid.major.x = element_line(color = "gray90"),
    panel.grid.minor = element_blank(),
    plot.margin = margin(15, 15, 15, 15)
  )

The geom_text() layer positions labels just beyond each point with hjust = -0.5. Setting scale_y_continuous() with appropriate limits ensures labels don’t get cut off. The theme customizations create visual hierarchy through font sizes and colors.

Practical Use Case: Diverging Lollipop Charts

Diverging lollipop charts excel at showing positive/negative comparisons. They center on zero with stems extending in both directions, making deviations immediately obvious. Perfect for survey sentiment, budget variance, or performance metrics:

# Survey results: net satisfaction scores
survey_results <- data.frame(
  feature = c("User Interface", "Performance", "Documentation", 
              "Customer Support", "Pricing", "Mobile App", "API"),
  net_score = c(45, 32, -15, 28, -38, 12, -8)
)

ggplot(survey_results, aes(x = reorder(feature, net_score), y = net_score)) +
  geom_segment(
    aes(x = feature, xend = feature, y = 0, yend = net_score, color = net_score > 0),
    size = 1.5,
    show.legend = FALSE
  ) +
  geom_point(
    aes(color = net_score > 0),
    size = 5,
    show.legend = FALSE
  ) +
  geom_hline(yintercept = 0, color = "gray30", size = 0.5) +
  scale_color_manual(values = c("#E74C3C", "#2ECC71")) +
  coord_flip() +
  labs(
    title = "Product Feature Satisfaction",
    subtitle = "Net satisfaction score (% satisfied - % dissatisfied)",
    x = NULL,
    y = "Net Score",
    caption = "Negative scores indicate more users dissatisfied than satisfied"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    panel.grid.major.y = element_blank(),
    panel.grid.minor = element_blank(),
    axis.line.x = element_line(color = "gray30")
  )

The conditional coloring (color = net_score > 0) automatically assigns colors based on whether values are positive or negative. Red for negative and green for positive creates immediate understanding. The geom_hline() at zero provides a clear reference line for the baseline.

This pattern works brilliantly for any scenario where you’re comparing performance against a baseline or showing bidirectional variance. The visual impact is immediate—you can see at a glance which items are above or below target.

Lollipop charts deserve a place in your visualization toolkit. They’re not appropriate for every dataset, but when you have ranked categorical data with many items, they provide clarity that bar charts can’t match. Start with the basic segment-and-point combination, sort your data meaningfully, and polish with thoughtful color and typography choices.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.