Lollipop plot with nice colormap, arrows and custom annotations

logo of a chart:Lollipop

We will create a Lollipop plot using the Matplotlib library to visualize data about temperature change.

This post will guide you on how to reproduce it step by step, from a very simple version to the final one with arrows, nice annotations and custom axis.

About

This plot is a lollipop plot. It shows the evolution of temperature variation compared to the average between 1951 and 1980.

The chart was made by Joseph Barbier. Thanks to him for accepting sharing his work here!

Let's see what the final picture will look like:

lollipop plot

Libraries

First, you need to install the following librairies:

And that's it!

import pandas as pd
import matplotlib.pyplot as plt
from pypalettes import load_cmap
from pyfonts import load_font
from highlight_text import fig_text, ax_text
from drawarrow import fig_arrow, ax_arrow

Dataset

For this reproduction, we're going to retrieve the data directly from the gallery's Github repo. This means we just need to give the right url as an argument to pandas' read_csv() function to retrieve the data.

path = 'https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/temperature-variation.csv'
df = pd.read_csv(path)
df.head()
Year Change Lowess(5)
0 1881 -0.09 -0.13
1 1882 -0.11 -0.17
2 1883 -0.18 -0.21
3 1884 -0.29 -0.24
4 1885 -0.34 -0.27

Basic lollipop

Although the stem() function in Matplotlib is designed for lollipop plots, we'll use plot() and scatter() instead to manually add the lines and points. This approach simplifies customizing the color of each lollipop in the next step.

We add each point and each line using a for loop, which iterates on each row of our dataframe.

fig, ax = plt.subplots(figsize=(15,8), dpi=300)

for i, row in df.iterrows():
   year = row['Year']
   change = row['Change']
   ax.scatter(x=year, y=change)
   ax.plot([year,year], [0,change])

plt.show()

Removes axis and custom color

In this step, we'll load a color map using load_cmap() from pypalettes, selecting the Coconut palette for its gradient from blue ("cold") to red ("hot").

Then, within the for loop, we'll retrieve the appropriate color based on the value (temperature change in this case) and apply it to both scatter() and plot().

And finally we remove all elements of the axis with ax.set_axis_off() since it's not very good looking and want to create our own.

cmap = load_cmap('Coconut', cmap_type='continuous', reverse=True)
fig, ax = plt.subplots(figsize=(15,8), dpi=300)
ax.set_axis_off()

for i, row in df.iterrows():
   year = row['Year']
   change = row['Change']
   color = cmap(change)
   ax.scatter(x=year, y=change, color=color)
   ax.plot([year,year], [0,change], color=color, alpha=0.8)

plt.show()

Create custom axis

To draw custom axes for this chart:

  • X-Axis Labels:

    • For each year divisible by 20 (if year % 20 == 0):

      • ax.text(x=year, y=-0.6, s=f'{year:.0f}', ...) adds the year as a label below the x-axis.
    • Adds an extra label for 1880 manually since it's not in the original data.

  • Horizontal Lines (Custom Y-Axis):

    • ax.hlines(y=h_lines, xmin=1881, xmax=2023, ...) draws horizontal lines at specified h_lines values.
    • xmin and xmax define the line's start and end on the x-axis.
  • Y-Axis Labels:

    • For each value in h_lines, ax.text(x=1877, y=value, s=f'{value}', ...) adds the value as a label beside the y-axis.
cmap = load_cmap('Coconut', cmap_type='continuous', reverse=True)
font = load_font('https://github.com/coreyhu/Urbanist/blob/main/fonts/ttf/Urbanist-Light.ttf?raw=true')
bold_font = load_font('https://github.com/coreyhu/Urbanist/blob/main/fonts/ttf/Urbanist-Medium.ttf?raw=true')

fig, ax = plt.subplots(figsize=(15,8), dpi=300)
ax.set_axis_off()

for i, row in df.iterrows():
   year = row['Year']
   change = row['Change']
   color = cmap(change)
   ax.scatter(x=year, y=change, color=color)
   ax.plot([year,year], [0,change], color=color, alpha=0.8)

   if year % 20 == 0:
      ax.text(x=year, y=-0.6, s=f'{year:.0f}', font=font, size=15, ha='left')
ax.text(x=1881, y=-0.6, s=f'{1880}', font=font, size=15, ha='left')

h_lines = [-0.4, 0, 0.4, 0.8]
ax.hlines(y=h_lines, xmin=1881, xmax=2023, colors=[cmap(val) for val in h_lines], linewidth=1.2, zorder=-1, alpha=0.5)
for value in h_lines:
   ax.text(x=1877, y=value, s=f'{value}', font=font, color=cmap(value), size=9, va='center')

plt.show()

Final chart with annotations

Once we have the core of the chart, we just need to add some annotations:

cmap = load_cmap('Coconut', cmap_type='continuous', reverse=True)
font = load_font('https://github.com/coreyhu/Urbanist/blob/main/fonts/ttf/Urbanist-Light.ttf?raw=true')
bold_font = load_font('https://github.com/coreyhu/Urbanist/blob/main/fonts/ttf/Urbanist-Medium.ttf?raw=true')
arrow_props = dict(color='black', width=0.5, head_width=2, head_length=5, radius=0.1)

fig, ax = plt.subplots(figsize=(15,8), dpi=300)
ax.set_axis_off()

for i, row in df.iterrows():
   year = row['Year']
   change = row['Change']
   color = cmap(change)
   ax.scatter(x=year, y=change, color=color)
   ax.plot([year,year], [0,change], color=color, alpha=0.8)
   
   if year % 20 == 0:
      ax.text(x=year, y=-0.6, s=f'{year:.0f}', font=font, size=15, ha='left')
ax.text(x=1881, y=-0.6, s=f'{1880}', font=font, size=15, ha='left')

h_lines = [-0.4, 0, 0.4, 0.8]
ax.hlines(y=h_lines, xmin=1881, xmax=2023, colors=[cmap(val) for val in h_lines], linewidth=1.2, zorder=-1, alpha=0.5)
for value in h_lines:
   ax.text(x=1877, y=value, s=f'{value}', font=font, color=cmap(value), size=9, va='center')

s = 'Global Land-Ocean Temperature Index'
ax_text(x=1881, y=1.1, s=s, font=font, size=35, ha='left')

s = 'Change in global surface temperature compared to the long-term average from 1951 to 1980'
ax_text(x=1881, y=0.94, s=s, font=font, size=16, ha='left', color='grey', alpha=0.7)

s = '<Graph>: barbierjoseph.com\n<Data Source>: NASA'
ax_text(x=1881, y=-0.7, s=s, font=font, size=8, ha='left', highlight_textprops=[{'font': bold_font}]*2)

s = 'Heat waves in Europe\nin the 1940s'
ax_text(x=1915, y=0.52, s=s, font=font, size=10, ha='left')
ax_arrow(
   tail_position=(1927, 0.43), head_position=(1938, 0.2), **arrow_props
)

s = 'Beginning of only positive\nvalues in global change'
ax_text(x=2018, y=-0.2, s=s, font=font, size=10, ha='right')
ax_arrow(
   tail_position=(1996, -0.22), head_position=(1980, -0.05), invert=True, **arrow_props
)

plt.savefig('../../static/graph/web-lollipop-with-colormap-and-arrow.png', dpi=300, bbox_inches='tight')
plt.show()

Going further

You might be interested in:

Contact & Edit


👋 This document is a work by Yan Holtz. You can contribute on github, send me a feedback on twitter or subscribe to the newsletter to know when new examples are published! 🔥

This page is just a jupyter notebook, you can edit it here. Please help me making this website better 🙏!