How to Create a Beautiful Polar Histogram With Python and Matplotlib
Tags: python, datascience, tutorial, datavisualization
Published: 2023-08-28 07:59:55

Hi, and welcome to this Python + Matplotlib tutorial, where I will show you how to create the beautiful polar histogram you see above.

Polar histograms are great when you have too many values for a standard bar chart. The circular shape where each bar gets thinner towards the middle allows us to cram more information into the same area.

I’m using data from the [World Happiness Report](https://worldhappiness.report/) and information about income levels from the [World Bank](https://data.worldbank.org/).

![Screenshot by the author](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3ry0fy9yyuh7vknwgv7.png)

You can find the code and data I’m using in this [GitHub repository](https://github.com/oscarleoo/polar-histogram-tutorial).

Let’s get started.

---

## Step 1: Preparations

Let's start with a few preperations.

### Importing libraries

We only need standard Python libraries familiar to everyone. PIL is not mandatory, but it’s my preferred choice for handling images which we do when adding flags.

```python
import math
import numpy as np
import pandas as pd

import seaborn as sns
import matplotlib.pyplot as plt

from PIL import Image
from matplotlib.lines import Line2D
from matplotlib.patches import Wedge
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
```

The only thing that stands out is a few specific Matplotlib imports at the end. I’ll cover those components later in the tutorial.

I use pandas to load the data.

```python
df = pd.read_csv("./hapiness_report_2022.csv", index_col=None)
df = df.sort_values("score").reset_index(drop=True)
```

### Seaborn style settings

Next, I use Seaborn to create a base style by defining the background, text color, and font.

```python
font_family = "PT Mono"
background_color = "#F8F1F1"
text_color = "#040303"

sns.set_style({
    "axes.facecolor": background_color,
    "figure.facecolor": background_color,
    "font.family": font_family,
    "text.color": text_color,
})
```

There are several more parameters for set_style, but these four are the only ones I need in this tutorial.

I use websites such as [Colorhunt](https://colorhunt.co/) and [Coolors](https://coolors.co/) to create beautiful color palettes.

### Global settings

I’m also adding a few global settings to control the general look. The first four define the range, size, and width of the wedges in the histogram.

```python
START_ANGLE = 100 # At what angle to start drawing the first wedge
END_ANGLE = 450 # At what angle to finish drawing the last wedge
SIZE = (END_ANGLE - START_ANGLE) / len(df) # The size of each wedge
PAD = 0.2 * SIZE # The padding between wedges

INNER_PADDING = 2 * df.score.min()
LIMIT = (INNER_PADDING + df.score.max()) * 1.3 # Limit of the axes
```

Inner padding creates distance between the origo and the start of each wedge. It opens a space in the middle of the graph where I can add a title.

### Boilerplate code

As a software engineer, I strive to write reusable code, and it’s the same when I’m working on data visualizations.

That’s why I always start by creating a few lines of boilerplate code that I can extend with reusable functions.

```python
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(30, 30))
ax.set(xlim=(-LIMIT, LIMIT), ylim=(-LIMIT, LIMIT))

for i, row in df.iterrows():
    bar_length = row.score
    name = row.country
    length = bar_length + INNER_PADDING
    start = 100 + i*SIZE + PAD
    end = 100 + (i+1)*SIZE
    angle = (end + start) / 2
    
    # Create variables here
    
    # Add wedge functions here

# Add general functions here

plt.axis("off")
plt.tight_layout()
plt.show()
```

For the rest of the tutorial, I will create and add functions and variables under one of the three comments.

---

## Step 2: Drawing wedges

To get more power over the visuals in Matplotlib, it helps to use the underlying components rather than the built-in graph functions.

### Drawing a wedge

For example, instead of using `plt.pie()` to create a pie chart, you can use `plt.patches.Wedge()` to draw the individual pieces.

That’s why I created the following function, which draws a wedge based on angles, length, bar length, and color.

```python
def draw_wedge(ax, start_angle, end_angle, length, bar_length, color):
    ax.add_artist(
        Wedge((0, 0),
            length, start_angle, end_angle,
            color=color, width=bar_length
        )
    )
```

In the boilerplate code, I add `draw_wedge()` under the “Add functions here” comment as below.

```python
bar_length = row.score
length = bar_length # + INNER_PADDING
start = 100 + i*SIZE + PAD
end = 100 + (i+1)*SIZE
.
.
.

# Add functions here    
draw_wedge(ax, start, end, length, bar_length, "#000")
```

I use `row.score` to define `bar_length` so that the visible part of the bars has an accurate size relation to each other. 
For now, I’ve removed the `INNER_PADDING` to show you what it does.

When I run the code, I get the following figure.

![Initial chart with wedges](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lx8684cxbyv1kwgihcm2.png)

As you can see, we have a long way to go until we get something similar to the polar histogram that you saw at the beginning, but at least we’ve managed to draw the wedges.

We get a lot of visual artefacts close to the middle, so let’s uncomment `INNER_PADDING`.

Here’s what we get.

![Chart with inner padding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bqusep04ntehpdv333h.png)

Much better.

### Adding color

Next, I have a simple color function that decides the color for each wedge based on the income level of that country.

```python
def color(income_group):
    if income_group == "High income":
        return "#468FA8"
    elif income_group == "Lower middle income":
        return "#E5625E"
    elif income_group == "Upper middle income":
        return "#62466B"
    elif income_group == "Low income":
        return "#6B0F1A"
    else:
        return "#909090"
```

I use that function as input to the draw_wedge function.

```python
# Add functions here    
draw_wedge(ax, start, end, length, bar_length, color(row.income))
```

Here’s the result.

![Initial chart with wedges and color](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w2d1t5u0j3010sr9xuo.png)

With `INNER_PADDING` and `color()` there are no strange artifacts left. It’s time to add information that explains what we’re looking at.

---

## Step 3: Adding labels

Let’s add labels for each bar in the polar histogram. I want each bar to display the country’s flag, name, and happiness score.

### Defining the positions

When you add flags and text to a chart in Matplotlib, you need to calculate the correct positions.

That’s often tricky, especially when you have an unusual shape like we have in the polar histogram.

The function below takes the length of a wedge and its angle to calculate a position. Padding pushes the position away from the bar to add some visual space.

```python
def get_xy_with_padding(length, angle, padding):
    x = math.cos(math.radians(angle)) * (length + padding)
    y = math.sin(math.radians(angle)) * (length + padding)
    return x, y
```

We can use this function for both flags and text.

### Adding flags

For flags, I’m using these [rounded ones](https://www.flaticon.com/packs/countrys-flags) from FlatIcon. 

They require a license, so, unfortunately, I can’t share them, but you can find similar flags in other places.

Here’s my function to add a flag to the graph. It takes the position, the country’s name (which corresponds to the name of the correct file), zoom, and rotation.

```python
def add_flag(ax, x, y, name, zoom, rotation):
    flag = Image.open("<location>/{}.png".format(name.lower()))
    flag = flag.rotate(rotation if rotation > 270 else rotation - 180)
    im = OffsetImage(flag, zoom=zoom, interpolation="lanczos", resample=True, visible=True)

    ax.add_artist(AnnotationBbox(
        im, (x, y), frameon=False,
        xycoords="data",
    ))
```

I change how the flag rotates if the angle exceeds 270 degrees. That happens when we start adding bars on the right part of the chart. At that point, the flag is to the left of the text, and changing the rotation makes reading more natural.

Now, we can calculate the angle, use `get_xy_with_padding()` and put flags on the chart.

```python
bar_length = row.score
length = bar_length + INNER_PADDING
start = START_ANGLE + i*SIZE + PAD
end = START_ANGLE + (i+1)*SIZE

# Add variables here
angle = (end + start) / 2
flag_zoom = 0.004 * length
flag_x, flag_y = get_xy_with_padding(length, angle, 0.1 * length)

# Add functions here
...
add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)
```

The flag_zoom parameters decide the size of the flag and depend on the score. If a country has a low score, there’s less room for a flag, and we need to make it a bit smaller.

![Polar histogram with flags](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cwwtemje68pu27d76uu6.png)

Fantastic.

### Adding country names and scores

To add the name and score of the country, I’ve written the following function.

As with the flags, I change the rotation if the angle exceeds 270 degrees. Otherwise, the text would be upside down.

```python
def add_text(ax, x, y, country, score, angle):
    if angle < 270:
        text = "{} ({})".format(country, score)
        ax.text(x, y, text, fontsize=13, rotation=angle-180, ha="right", va="center", rotation_mode="anchor")
    else:
        text = "({}) {}".format(score, country)
        ax.text(x, y, text, fontsize=13, rotation=angle, ha="left", va="center", rotation_mode="anchor")
```

We calculate the position of the text in the same way as we did with the flags.

The only difference is that we add more padding since we want it further from the wedges.

```python
bar_length = row.score
length = bar_length + INNER_PADDING
start = START_ANGLE + i*SIZE + PAD
end = START_ANGLE + (i+1)*SIZE

# Add variables here
angle = (end + start) / 2
flag_zoom = 0.004 * length
flag_x, flag_y = get_xy_with_padding(length, angle, 0.1 * length)
text_x, text_y = get_xy_with_padding(length, angle, 16*flag_zoom)

# Add functions here
...
add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)
add_text(ax, text_x, text_y, row.country, bar_length, angle)
```

Now we have the following graph, and it’s starting to look much better.

![Polar histogram with flags and text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gzzk776yoryit2dw1k8j.png)

Now it’s time to tell the users what they are looking at.

---

## Step 4: Adding information

We have added all the data. It’s time to make the chart readable by adding helpful information and guidance.

### Drawing reference lines

An excellent type of visual helper is reference lines; they work just as well here as with standard bar charts.

The idea is to draw a line at a specific score, which indirectly helps us compare different countries.

Here’s my function to draw reference lines. I’m reusing the `draw_wedge()` function to draw a wedge from 0 to 360 degrees.

```python
def draw_reference_line(ax, point, size, padding, fontsize=18):
    draw_wedge(ax, 0, 360, point+padding+size/2, size, background_color)
    ax.text(-0.6, padding + point, point, va="center", rotation=1, fontsize=fontsize)
```

I run the function once for each score to draw multiple reference lines.

```python
# Add general functions here
draw_reference_line(ax, 2.0, 0.05, INNER_PADDING)
draw_reference_line(ax, 4.0, 0.05, INNER_PADDING)
draw_reference_line(ax, 6.0, 0.05, INNER_PADDING)
```

Here’s the result.

![Polar histogram with reference lines](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hytwmkgc1yfqdwmta8lo.png)

It makes a significant difference.

### Adding a title

The purpose of the gap in the center of the graph is to create a natural place for a title. Having the title in the center is unusual and can immediately capture a viewer’s interest.

The code for adding the title is standard Matplotlib functionality.

```python
# Add general functions here
...
plt.title(
  "World Happiness Report 2022".replace(" ", "\n"), 
  x=0.5, y=0.5, va="center", ha="center", 
  fontsize=64, linespacing=1.5
)
```

Here’s what it looks like.


![Polar histogram with title](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pflnil3zmwzbfk5q87qy.png)

It’s getting close, but we still have one more thing to do.

### Adding a legend

There’s no way for the viewer to understand what the colors mean, but we can fix that by adding a legend.

To add a legend, I’ve created the following function that takes the labels to add, their colors, and a title.

```python
def add_legend(labels, colors, title):
    lines = [
        Line2D([], [], marker='o', markersize=24, linewidth=0, color=c) 
        for c in colors
    ]

    plt.legend(
        lines, labels,
        fontsize=18, loc="upper left", alignment="left",
        borderpad=1.3, edgecolor="#E4C9C9", labelspacing=1,
        facecolor="#F1E4E4", framealpha=1, borderaxespad=1,
        title=title, title_fontsize=20,
    )
```

I add the function under “Add general functions here” and run it together with everything else.

```python
# Add general functions here
...

add_legend(
    labels=["High income", "Upper middle income", "Lower middle income", "Low income", "Unknown"],
    colors=["#468FA8", "#62466B", "#E5625E", "#6B0F1A", "#909090"],
    title="Income level according to the World Bank\n"
)
```

The final result looks like this.

![Polar histogram by the author](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2oe05tc9cyjh7x2nc79a.png)

That’s it. We have recreated the beautiful polar histogram you saw at the top.

Your entire main block of code should now look like this.

```python
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(30, 30))
ax.set(xlim=(-LIMIT, LIMIT), ylim=(-LIMIT, LIMIT))

for i, row in df.iterrows():
    bar_length = row.score
    length = bar_length + INNER_PADDING
    start = START_ANGLE + i*SIZE + PAD
    end = START_ANGLE + (i+1)*SIZE
    angle = (end + start) / 2
    
    # Add variables here
    flag_zoom = 0.004 * length
    flag_x, flag_y = get_xy_with_padding(length, angle, 8*flag_zoom)
    text_x, text_y = get_xy_with_padding(length, angle, 16*flag_zoom)
    
    # Add functions here
    draw_wedge(ax, start, end, length, bar_length, color(row.income))
    add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)
    add_text(ax, text_x, text_y, row.country, bar_length, angle)

ax.text(1-LIMIT, LIMIT-2, "+ main title", fontsize=58)

# Add general functions here
draw_reference_line(ax, 2.0, 0.06, INNER_PADDING)
draw_reference_line(ax, 4.0, 0.06, INNER_PADDING)
draw_reference_line(ax, 6.0, 0.06, INNER_PADDING)
plt.title("World Happiness Report 2022".replace(" ", "\n"), x=0.5, y=0.5, va="center", ha="center", fontsize=64, linespacing=1.5)

add_legend(
    labels=["High income", "Upper middle income", "Lower middle income", "Low income", "Unknown"],
    colors=["#468FA8", "#62466B", "#E5625E", "#6B0F1A", "#909090"],
    title="Income level according to the World Bank\n"
)

plt.axis("off")
plt.tight_layout()
plt.show()
```

That’s it for this tutorial; congratulations on reaching the end.

---

## Conclusion

Today, we learned to create a beautiful polar histogram using Matplotlib and Python.

Polar histograms are surprisingly easy to create, allowing us to cram more information into a single chart.

I used the World Happiness Report in this tutorial, but you can change it to another inspiring dataset.

I hope you learned a few techniques to help you bring your chart ideas to life.