Lollipop plot with background image

logo of a chart:Joyplot

This guide demonstrates how to craft a lollipop plot featuring an image in the background and personalized annotations using matplotlib and highlight_text.

In this detailed walkthrough, we'll begin with a fundamental lollipop plot and incrementally integrate elements such as reference lines, images, legends, and annotations.

About

This graph is a lollipop plot, designed to showcase a single value for each category, akin to a barplot but substituting the bar with a line. This specific instance features an appealing background and includes annotations positioned to the right of the chart.

It has been originally designed by Joseph Barbier. Thanks to him for sharing his work!

As a teaser, here is the plot we’re gonna try building:

preview

Libraries

For creating this chart, we will need a whole bunch of libraries!

  • matplotlib: to customize the appearance of the chart
  • seaborn: to create the chart
  • pandas: to handle the data
  • highlight_text and textwrap: to add text annotations
  • PIL for the background image
# main libraries
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# image in the background
from PIL import Image
from mpl_toolkits.axes_grid1.inset_locator import inset_axes

# annotations
from highlight_text import fig_text, ax_text
from matplotlib.patches import FancyArrowPatch
import textwrap

Dataset

The data can be accessed using the url below.

path = 'https://raw.githubusercontent.com/holtzy/the-python-graph-gallery/master/static/data/rolling_stone.csv'
df = pd.read_csv(path)

# clean the data
replacements = {
   "Blues/Blues ROck": "Blues/Blues Rock",
   "Rock n' Roll/Rhythm & Blues": "Blues/Blues Rock"
}
df['genre_clean'] = df['genre'].replace(replacements)
df.drop(['spotify_url', 'sort_name', 'artist_birth_year_sum', 'genre'], axis=1, inplace=True)
df.dropna(subset=['differential'], inplace=True)
df = df[df['release_year']<=2003]
data = df.groupby('genre_clean')['differential'].mean().sort_values()
data = pd.DataFrame({
   'genre': data.index,
   'diff': data.values
})
data = data[~data['genre'].isin(['Afrobeat'])]
data.sort_values('diff', ascending=False, inplace=True)

# show first rows
data.head()
genre diff
13 Hip-Hop/Rap 156.955556
12 Electronic 83.222222
10 Indie/Alternative Rock 59.478261
9 Soul/Gospel/R&B 13.000000
8 Punk/Post-Punk/New Wave/Power Pop -13.925926

Add background image

  1. Load Background Image: we first load a background image from a specified path and converts it into a NumPy array for manipulation.
  2. Initialize Chart with Background: A figure and its axes are then initialized using matplotlib, where the loaded image is set as the background with a reduced opacity.
  3. Create Secondary Axes for Lollipop Chart: A secondary axes area (sub_ax) is created on top of the main axes. This secondary area is intended for placing a lollipop chart and is sized and positioned to occupy the majority of the figure.
# open image
path_bg = '../../static/graph/background.png'
image_bg = np.array(Image.open(path_bg))

# create chart with background image
fig, ax = plt.subplots(figsize=(12,8), dpi=300)
ax.imshow(image_bg, alpha=0.15)
ax.set_axis_off()

# create lollipop background
sub_ax = inset_axes(
    parent_axes=ax,
    width="80%",
    height="90%",
    loc='lower center',
    borderpad=3
)

# display plot
plt.show()

Add lollipop plot

Then we add the lollipop in the inside axes using the hlines() and plot() functions.

The hlines() function is used to draw the horizontal lines, while the plot() function is used to draw the circles at the end of each line.

# open image
path_bg = '../../static/graph/background.png'
image_bg = np.array(Image.open(path_bg))

# create chart with background image
fig, ax = plt.subplots(figsize=(12,8), dpi=300)
ax.imshow(image_bg, alpha=0.15)
ax.set_axis_off()

# create lollipop background
sub_ax = inset_axes(
    parent_axes=ax,
    width="80%",
    height="90%",
    loc='lower center',
    borderpad=3
)

# add lollipop chart
x = data['diff']
y = data['genre']
sub_ax.hlines(y=y, xmin=0, xmax=x, color='black')
sub_ax.plot(x, y, 'o', color='black', zorder=2)

# display plot
plt.show()

Custom lollipop axes

Now we customize how the inside axes look like. We remove the ticks, the labels, and the left, right and top spines. We also set the background color to be fully transparent.

# open image
path_bg = '../../static/graph/background.png'
image_bg = np.array(Image.open(path_bg))

# create chart with background image
fig, ax = plt.subplots(figsize=(12,8), dpi=300)
ax.imshow(image_bg, alpha=0.15)
ax.set_axis_off()

# create lollipop background
sub_ax = inset_axes(
    parent_axes=ax,
    width="80%",
    height="90%",
    loc='lower center',
    borderpad=3
)

# custom lollipop axes
sub_ax.set_xlim(-200, 200)
sub_ax.set_xticks([-200, -100, 0, 100, 200])
sub_ax.set_xticklabels(['-200', '-100', '0', '100', '200'])
sub_ax.set_yticklabels([])
sub_ax.set_yticks([])
sub_ax.spines[['top', 'left', 'right']].set_visible(False)
sub_ax.patch.set_alpha(0)

# add lollipop chart
x = data['diff']
y = data['genre']
sub_ax.hlines(y=y, xmin=0, xmax=x, color='black')
sub_ax.plot(x, y, 'o', color='black', zorder=2)

# display plot
plt.show()