Creating pie chart from dict and changing the arrows

Viewed 927

I have some count data in dict like this:

data = {'a_column': 20, 'b_column': 130, 
        'c_column': 140, 'd_column': 300, 'e_column': 150,
        'f_column': 170, 'g_column': 10, 'h_column': 20, 'i_column': 250,'j_column': 54}

I want to plot a pie chart, something like this:

enter image description here

What I have tried:

base_d = sum(list(data.values()))
final_data = {k:m/base_d*100 for k,m in data.items()}
final_data

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 5), subplot_kw=dict(aspect="equal"))
recipe = list(final_data.keys())
data = list(final_data.values())
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40)
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
          bbox=bbox_props, zorder=0, va="center")

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)
    kw["arrowprops"].update({"connectionstyle": connectionstyle})
    ax.annotate(recipe[i], xy=(x, y), xytext=(1*np.sign(x), 1.4*y),
                horizontalalignment=horizontalalignment, **kw)


plt.show()

But that is giving me like this:

enter image description here

How I can make the arrows like expected image and remove the border around the labels?

2 Answers

Part 1 (of 3)

As suggested by @JohanC commented out #bbox=bbox_props to remove boxes, changed 1*np.sign(x) to 1.4*np.sign(x) to spread labels wider along X, computed percentages perc[i] and added them to labels inside ax.annotate(...). Final picture looks like (code below picture):

enter image description here


Final Code:

Try it online!

data = {'a_column': 20, 'b_column': 130, 
        'c_column': 140, 'd_column': 300, 'e_column': 150,
        'f_column': 170, 'g_column': 10, 'h_column': 20, 'i_column': 250,'j_column': 54}

base_d = sum(list(data.values()))
final_data = {k:m/base_d*100 for k,m in data.items()}
final_data

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 5), subplot_kw=dict(aspect="equal"))
recipe = list(final_data.keys())
data = list(final_data.values())
perc = [str(round(e / s * 100., 1)) + '%' for s in (sum(data),) for e in data]
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40)
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
          #bbox=bbox_props,
          zorder=0, va="center")

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)
    kw["arrowprops"].update({"connectionstyle": connectionstyle})
    ax.annotate(recipe[i] + ' ' + perc[i], xy=(x, y), xytext=(1.4*np.sign(x), 1.4*y),
                horizontalalignment=horizontalalignment, **kw)

plt.show()

Part 2 (of 3)

If you have very many data entries then default coloring of pie chart doesn't look good, it has just 10-15 unique colors. I decided to make my own custom coloring based on rainbow colors. In my coloring even if you have 100 data entries then all colors are unique. In code below coloring is done inside ax.pie(... colors = ....). I made example image and code for rainbow coloring:

enter image description here

Try it online!

import numpy as np, math, random
import matplotlib.pyplot as plt
random.seed(0)

data = {f'dat_{str(i).zfill(2)}' : random.random() for i in range(49)}

base_d = sum(list(data.values()))
final_data = {k:m/base_d*100 for k,m in data.items()}

fig, ax = plt.subplots(figsize=(12, 5), subplot_kw=dict(aspect="equal"))
recipe = list(final_data.keys())
data = list(final_data.values())
perc = [str(round(e / s * 100., 1)) + '%' for s in (sum(data),) for e in data]
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40,
    colors = (lambda C = 7: [plt.cm.gist_rainbow((i % C) / C + i // C * 1 / C / math.ceil(len(data) / C)) for i in range(len(data))])())
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
          #bbox=bbox_props,
          zorder=0, va="center")

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)
    kw["arrowprops"].update({"connectionstyle": connectionstyle})
    ax.annotate(recipe[i] + ' ' + perc[i], xy=(x, y), xytext=(1.4*np.sign(x), 1.4*y),
                horizontalalignment=horizontalalignment, **kw)

plt.show()

Part 3 (of 3)

Also decided to improve the case of very many entries (around 100 entries). Did next improvements: 1) separated along X odd and even entries. 2) put all labels uniformly along Y (before they were uniform along angle). 3) made smaller font.

enter image description here

Try it online!

import numpy as np, math, random
import matplotlib.pyplot as plt
np.random.seed(0)
random.seed(0)

data = {f'dat_{str(i).zfill(2)}' : random.random() for i in range(98)}

base_d = sum(list(data.values()))
final_data = {k:m/base_d*100 for k,m in data.items()}

fig, ax = plt.subplots(figsize=(12, 5), subplot_kw=dict(aspect="equal"))
recipe = list(final_data.keys())
data = list(final_data.values())
perc = [str(round(e / s * 100., 2)) + '%' for s in (sum(data),) for e in data]
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40, radius = 0.8,
    colors = (lambda C = 7: [plt.cm.gist_rainbow((i % C) / C + i // C * 1 / C / math.ceil(len(data) / C)) for i in range(len(data))])())
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
          #bbox=bbox_props,
          zorder=0, va="center")

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))
    yc = np.arcsin(y) / (np.pi / 2)
    horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
    connectionstyle = f'angle,angleA=0,angleB={ang}'
    kw["arrowprops"].update({"connectionstyle": connectionstyle})
    ax.annotate(recipe[i] + ' ' + perc[i], xy = (0.8 * x, 0.8 * y), xytext = ((1.0 + (i % 2) * 0.4) * np.sign(x), 1.4 * yc),
                horizontalalignment = horizontalalignment, fontsize = 'x-small', **kw)

plt.show()

I did two thing.

1.Calculate the percentage add to label
2.remove the box style

import numpy as np
import matplotlib.pyplot as plt

def combine_column_names(column_name,cur_value,sums):
    percentage = round(cur_value/sums*100,2)
    name = "{} {}%".format(column_name,percentage)
    return name

data = {'a_column': 20, 'b_column': 130, 
        'c_column': 140, 'd_column': 300, 'e_column': 150,
        'f_column': 170, 'g_column': 10, 'h_column': 20, 'i_column': 250,'j_column': 54}
base_d = sum(list(data.values()))
final_data = {combine_column_names(k,m,base_d):m/base_d*100 for k,m in data.items()}


fig, ax = plt.subplots(figsize=(12, 5), subplot_kw=dict(aspect="equal"))
recipe = list(final_data.keys())
data = list(final_data.values())
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40)
kw = dict(arrowprops=dict(arrowstyle="-"),zorder=0, va="center")

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)
    kw["arrowprops"].update({"connectionstyle": connectionstyle})
    ax.annotate(recipe[i], xy=(x, y), xytext=(1*np.sign(x), 1.4*y),
                horizontalalignment=horizontalalignment, **kw)


plt.show()

Related