Matplotlib nestest pie chart

Alexander Dunkel, Leibniz Institute of Ecological Urban and Regional Development,
Transformative Capacities & Research Data Centre (IÖR-FDZ)

No description has been provided for this image

The task in this notebook is to take this power point circle diagram and reproduce it with matplotlib.

input

I first tried pyCirclize, but it did not work well. Matplotlib donut charts, on the other hand, are quite simple.

Nested Donut Charts

Import dependencies

In [19]:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import patheffects
from typing import List

Define Parameters

Colors

In [2]:
yellow = "#F8E04C"
green = "#006571"
brown = "#BE651A"
lightgreen = "#86BC27"
blue = "#0970B9"

General matplotlib pie chart parameters

General properties of circles:

In [3]:
width = 0.18
wedge_properties = {
    "width": width,
    "edgecolor": "w",
    'linewidth': 2}

Properties of bbox text background

In [4]:
bbox_props = {
    "linewidth": 0,
    "alpha": 0.7}

Text/ label properties

In [5]:
textprops = {
    "size": 14,
    "color": "k",
    "alpha": 0.9,
    "horizontalalignment": 'center',
    "bbox": bbox_props,
}

Combine general properties

In [6]:
pie_props = {
    "wedgeprops": wedge_properties,
    "textprops": textprops,
    "startangle": 40,
}

Define content

Starting with the outermost circle. For labeling, see: https://matplotlib.org/stable/gallery/pie_and_polar_charts/pie_and_donut_labels.html

In [7]:
v1 = [1, 1, 1, 1, 1, 1, 1]
labels1 = [
    "Biologische Vielfalt \nentwickeln",
    "Umweltrisiken \nbegegnen",
    "Materialkreisläufe in \nStädten schließen",
    "Wandel der Flächennutzung \nverstehen",
    "Mensch-Natur-Beziehungen \nstärken",
    "Transformation \ngemeinsam gestalten",
    "Städte und Regionen der \nZukunft planen"]
colors1 = [yellow, yellow, yellow, yellow, yellow, yellow, yellow]

v2 = [1, 1, 1, 1, 1]
colors2 = [green, green, green, green, green, green, green]
labels2 = [
    "Zivilgesellschaft",
    "Journalismus",
    "Politik",
    "Verwaltung",
    "Wissenschaft"]

v3 = [1, 1, 1]
labels3 = [
    "Workshops",
    "Arbeitshilfen",
    "Showroom"]
colors3 = [brown, brown, brown]

v4 = [1, 1, 1, 1, 1, 1]
labels4 = [
    "Anwendungen",
    "Indikatoren",
    "Modelle",
    "Analysen",
    "Simulationen",
    "Daten"]
colors4 = [lightgreen, lightgreen, lightgreen, 
           lightgreen, lightgreen, lightgreen]

v5 = [1, 1, 1, 1]
labels5 = [
    "Website",
    "Schnittstellen",
    "Visualisierungen",
    "Repository"]
colors5 = [blue, blue, blue, blue]

Create pie plot

with 5 nested circles

In [8]:
fig, ax = plt.subplots(
    nrows=1, ncols=1, figsize=(12, 18))

# outer circle
bbox_props["color"] = yellow
textprops["color"] = "k"
ax.pie(
    v1, labels=labels1, labeldistance=1.15, colors=colors1,
    **pie_props),

# 4th circle
bbox_props["color"] = green
textprops["color"] = "w"

ax.pie(
    v2, labels=labels2, labeldistance=0.90,
    radius=1-width, colors=colors2,
    **pie_props)

# 3rd circle
bbox_props["color"] = brown
textprops["color"] = "w"
ax.pie(
    v3, labels=labels3, labeldistance=0.85,
    radius=1-width-width, colors=colors3,
    **pie_props)

# 2nd circle
bbox_props["color"] = lightgreen
textprops["color"] = "k"
ax.pie(
    v4, labels=labels4, labeldistance=0.80,
    radius=1-width-width-width, colors=colors4,
    **pie_props)

# inner circle
bbox_props["color"] = blue
textprops["color"] = "w"
ax.pie(
    v5, labels=labels5, labeldistance=0.60,
    radius=1-width-width-width-width, colors=colors5,
    **pie_props)

plt.show()
No description has been provided for this image
In [9]:
fig.savefig(
    "../resources/circle_v1.svg", format='svg',
    bbox_inches='tight', pad_inches=1, facecolor="white")

Customize text labels

Modifying labels can be done by getting wedges and texts from the pie.plot(). See the given example from the MPL docs.

In [10]:
bbox_props_alt = {
    "boxstyle": "square,pad=0.3",
    "fc": yellow,
    "ec": "k",
    "lw": 0
}

textprops_alt = {
    "arrowprops": dict(arrowstyle="-"),
    "bbox": bbox_props_alt,
    "zorder": 0,
    "va": "center",
    "size": 14,
    "color": "k",
    "alpha": 0.9,
    "zorder": 100,
}
In [11]:
def mod_labels(ax, bbox_props_alt, textprops_alt, labels, wedges, yoffset):
    """Add and style labels to pie chart"""
    for i, p in enumerate(wedges):
        ang = (p.theta2 - p.theta1)/2. + p.theta1
        y = np.sin(np.deg2rad(ang))
        x = np.cos(np.deg2rad(ang))
        horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
        connectionstyle = "angle,angleA=0,angleB={}".format(ang)
        textprops_alt["arrowprops"].update(
            {"connectionstyle": connectionstyle})
        ax.annotate(
            labels[i],
            xy=(x, (y+yoffset)), xytext=(1.15*np.sign(x), 1.05*(y+yoffset)),
            horizontalalignment=horizontalalignment, **textprops_alt)
In [12]:
fig, ax = plt.subplots(
    nrows=1, ncols=1, figsize=(12, 18))

# outer circle
bbox_props_alt["fc"] = yellow
textprops_alt["color"] = "k"
wedges, texts = ax.pie(
    v1, colors=colors1, **pie_props)
mod_labels(ax, bbox_props_alt, textprops_alt, labels1, wedges, 0.02)

# 4th circle
bbox_props["color"] = green
textprops["color"] = "w"
ax.pie(
    v2, labels=labels2, labeldistance=0.90,
    radius=1-width, colors=colors2,
    **pie_props)

# 3rd circle
bbox_props["color"] = brown
textprops["color"] = "w"
ax.pie(
    v3, labels=labels3, labeldistance=0.85,
    radius=1-width-width, colors=colors3,
    **pie_props)

# 2nd circle
bbox_props["color"] = lightgreen
textprops["color"] = "k"
ax.pie(
    v4, labels=labels4, labeldistance=0.80,
    radius=1-width-width-width, colors=colors4,
    **pie_props)

# inner circle
bbox_props["color"] = blue
textprops["color"] = "w"
ax.pie(
    v5, labels=labels5, labeldistance=0.60,
    radius=1-width-width-width-width, colors=colors5,
    **pie_props)

plt.show()
No description has been provided for this image
In [13]:
fig.savefig(
    "../resources/circle_v2.svg", format='svg',
    bbox_inches='tight', pad_inches=1, facecolor="white")

Colorize path

Similarly, we can update the method to label/colorize different paths of the nested pie plot. As a start, we need to select the path by updating color lists per circle.

In [57]:
purple = "#6E2F9D"
In [58]:
colors1 = [yellow, yellow, yellow, purple, yellow, yellow, yellow]
colors2 = [purple, purple, purple, purple, green, purple, purple]
colors3 = [brown, brown, purple]
colors4 = [lightgreen, lightgreen, lightgreen,
           purple, lightgreen, purple]
colors5 = [purple, blue, purple, purple]
In [78]:
def mod_labels_color(ax, wedges, bbox_props, textprops, labels, colors, labeldistance, starting_textcolor):
    """Add and style labels to pie chart"""
    highlight = False
    if "#6E2F9D" in colors:
        highlight = True
    for i, p in enumerate(wedges):
        bbox_props_mod = bbox_props.copy()
        cur_col = colors[i]
        # p.set_path_effects(
        #     [patheffects.SimplePatchShadow(offset=(10, -10)),
        #      patheffects.Normal()])
        if cur_col == "#6E2F9D":
            # we want text labels in selected
            # path to be shown with purple
            # color background and white text
            textprops["alpha"] = 0.9
            textprops["color"] = "w"
            bbox_props_mod["color"] = "#6E2F9D"
            # also highlight selected path by
            # adding a shadow to patches
            p.set_path_effects(
                [patheffects.SimplePatchShadow(offset=(10, -10)),
                patheffects.Normal()])
        else:
            # we want text labels not in selected path
            # to be shown without background color
            # and reduced alpha
            if highlight:
                textprops["alpha"] = 0.3
                bbox_props_mod["color"] = "none"
            else:
                # if no path is selected
                # use default values
                textprops["alpha"] = 0.9
                # highlight all paths (excluding the first) by
                # adding a shadow to patches
                if i>1:
                    p.set_path_effects(
                        [patheffects.SimplePatchShadow(offset=(10, -10)),
                        patheffects.Normal()])
            textprops["color"] = starting_textcolor
        textprops["bbox"] = bbox_props_mod
        ang = (p.theta2 - p.theta1)/2. + p.theta1
        y = np.sin(np.deg2rad(ang))*labeldistance
        x = np.cos(np.deg2rad(ang))*labeldistance
        ax.annotate(
            labels[i],
            xy=(x, y), **textprops)
In [72]:
def plot_pie(colors: List[List[str]], highlight_color: str):
    """Plot pie chart, highlight selected path"""
    colors1 = colors[0]
    colors2 = colors[1]
    colors3 = colors[2]
    colors4 = colors[3]
    colors5 = colors[4]
    
    fig, ax = plt.subplots(
        nrows=1, ncols=1, figsize=(12, 18))
    # outer circle
    bbox_props["color"] = yellow
    wedges, texts = ax.pie(
        v1, colors=colors1, **pie_props)
    mod_labels_color(
        ax, wedges, bbox_props, textprops, labels1, colors1, 1.00, "k")
    # 4th circle
    bbox_props["color"] = green
    wedges, texts = ax.pie(
        v2, radius=1-width, colors=colors2, **pie_props)
    mod_labels_color(
        ax, wedges, bbox_props, textprops, labels2, colors2, 0.75, "w")
    # 3rd circle
    bbox_props["color"] = brown
    wedges, texts = ax.pie(
        v3, radius=1-width-width, colors=colors3, **pie_props)
    mod_labels_color(
        ax, wedges, bbox_props, textprops, labels3, colors3, 0.55, "w")
    # 2nd circle
    bbox_props["color"] = lightgreen
    wedges, texts = ax.pie(
        v4, radius=1-width-width-width, colors=colors4, **pie_props)
    mod_labels_color(
        ax, wedges, bbox_props, textprops, labels4, colors4, 0.35, "k")
    # inner circle
    bbox_props["color"] = blue
    textprops["color"] = "w"
    wedges, texts = ax.pie(
        v5, radius=1-width-width-width-width, colors=colors5, **pie_props)
    mod_labels_color(
        ax, wedges, bbox_props, textprops, labels5, colors5, 0.15, "w")
    plt.show()
    return fig
In [61]:
fig = plot_pie(
    colors=[colors1, colors2, colors3, colors4, colors5],
    highlight_color=purple)
No description has been provided for this image

Colorize different path

In [62]:
colors1 = [yellow, yellow, yellow, yellow, yellow, purple, yellow]
colors2 = [purple, green, green, purple, purple, purple, purple]
colors3 = [brown, brown, purple]
colors4 = [lightgreen, lightgreen, lightgreen,
           purple, lightgreen, purple]
colors5 = [purple, blue, purple, purple]
In [63]:
fig = plot_pie(
    colors=[colors1, colors2, colors3, colors4, colors5],
    highlight_color=purple)
No description has been provided for this image

Colorize no path

In [64]:
colors1 = [yellow, yellow, yellow, yellow, yellow, yellow, yellow]
colors2 = [green, green, green, green, green, green, green]
colors3 = [brown, brown, brown]
colors4 = [lightgreen, lightgreen, lightgreen,
           lightgreen, lightgreen, lightgreen]
colors5 = [blue, blue, blue, blue]
In [79]:
fig = plot_pie(
    colors=[colors1, colors2, colors3, colors4, colors5],
    highlight_color=purple)
No description has been provided for this image

Save to SVG and PNG

In [80]:
fig.savefig(
    "../resources/circle_v4.png", dpi=300, format='PNG',
    bbox_inches='tight', pad_inches=1, facecolor="white")
fig.savefig(
    "../resources/circle_v4.svg", format='svg',
    bbox_inches='tight', pad_inches=1, facecolor="white")

Curved Text

ToDo: see Link

Create notebook HTML

In [353]:
!jupyter nbconvert --to html_toc \
    --output-dir=../resources/html/ ./2024-11-21_donut_mpl.ipynb \
    --template=../nbconvert.tpl \
    --ExtractOutputPreprocessor.enabled=False >&- 2>&-
In [ ]:
 

IOER RDC Jupyter Base Template v0.10.0