Prevent labels from overlapping in matplotlib vertical timeline plot - python

The methods I normally use for prevent overlap of data labels don't seem to apply to this matplotlib-based vertical timeline. Normally I would use something like autofmt_xdate.
I don't actually care how "tall" (long) the chart needs to be or if the vertical distance between the items perfectly preserves the ratio of the date gap between items. I just don't want the text labels to overlap.
I am not very experience with matplotlib but thusfar I've determined that the code that controls the text labels is
_ = ax.text(label_offsets[i], d, l, ha=align, fontfamily='serif', fontweight='bold', color='royalblue',fontsize=12)
where d is a timestamp being used as a y-axis coordinate. I tried creating a loop counter and a list to contain the value of d from each loop iteration, so that I could make a comparison between the current and prior date, then add a fixed value (e.g. 15 days) to the current value of d if the prior value of d was too close. That didn't seem to do the trick.
The plot code:
chartdata = pd.read_csv(
"data/Events.csv"
)
chartdata = chartdata.query('Year > 1939')
dates = pd.to_datetime(chartdata['Date_Clean_Approx'])
min_date = date(np.min(dates).year - 2, np.min(dates).month, np.min(dates).day)
max_date = date(np.max(dates).year + 2, np.max(dates).month, np.max(dates).day)
labels = chartdata['Name']
# labels with associated dates
labels = ['{0:%d %b %Y}:\n{1}'.format(d, l) for l, d in zip (labels, dates)]
fig, ax = plt.subplots(figsize=(6, 32), constrained_layout=True)
_ = ax.set_xlim(-20, 20)
_ = ax.set_ylim(min_date, max_date)
_ = ax.axvline(0, ymin=0.05, ymax=0.95, c='deeppink', zorder=1)
_ = ax.scatter(np.zeros(len(dates)), dates, s=120, c='palevioletred', zorder=2)
_ = ax.scatter(np.zeros(len(dates)), dates, s=30, c='darkmagenta', zorder=3)
label_offsets = np.repeat(2.0, len(dates))
label_offsets[1::2] = -2.0
for i, (l, d) in enumerate(zip(labels, dates)):
d = d - timedelta(days=90)
align = 'right'
if i % 2 == 0:
align = 'left'
_ = ax.text(label_offsets[i], d, l, ha=align, fontfamily='serif', fontweight='bold', color='royalblue',fontsize=12)
stems = np.repeat(2.0, len(dates))
stems[1::2] *= -1.0
x = ax.hlines(dates, 0, stems, color='darkmagenta')
# hide lines around chart
for spine in ["left", "top", "right", "bottom"]:
_ = ax.spines[spine].set_visible(False)
# hide tick labels
_ = ax.set_xticks([])
_ = ax.set_yticks([])
_ = ax.set_title('UAP (UFO) Milestones, 1940 - Present',
fontweight="bold",
fontfamily='serif',
fontsize=16,
color='royalblue')
pyplot(fig)
As you can see in the image, I increased the height of the plot to reduce overlap in the labels. The plot got very long but still has overlap in text. Once a solution is determined I think that embedding the logic to "adjust relative gap size and chart height as needed to avoid text label overlap" into a convenient plot function would be a large contribution to the matplotlib library.

I'm definitely open to better, more programmatic, solutions but for now I replaced the dates with a same-length vector of consecutive integers and used it in place of the actual dates everywhere except within the text of the labels.
###### Timeline#
chartdata = pd.read_csv(
"/home/kodachi/Documents/ET/aliendb/www/app/data/Events.csv"
)
chartdata=chartdata.query('Year > 1939')
dates = pd.to_datetime(chartdata['Date_Clean_Approx'])
min_date = date(np.min(dates).year - 2, np.min(dates).month, np.min(dates).day)
max_date = date(np.max(dates).year + 2, np.max(dates).month, np.max(dates).day)
###
# fake date
fake_d=np.c_[1:len(dates)]
###
labels = chartdata['Name']
# labels with associated dates
labels = ['{0:%d %b %Y}:\n{1}'.format(d, l) for l, d in zip (labels, dates)]
fig, ax = plt.subplots(figsize=(8, 28))#, constrained_layout=True)
_ = ax.set_xlim(-20, 20)
#_ = ax.set_ylim(min_date, max_date)
_ = ax.set_ylim(1, 96)
_ = ax.axvline(0, ymin=0.05, ymax=.985, c='deeppink', zorder=1)#ymax=0.95
#_ = ax.scatter(np.zeros(len(dates)), dates, s=120, c='palevioletred', zorder=2)
#_ = ax.scatter(np.zeros(len(dates)), dates, s=30, c='darkmagenta', zorder=3)
_ = ax.scatter(np.zeros(len(fake_d)), fake_d, s=120, c='palevioletred', zorder=2)
_ = ax.scatter(np.zeros(len(fake_d)), fake_d, s=30, c='darkmagenta', zorder=3)
#label_offsets = np.repeat(2.0, len(dates))
label_offsets = np.repeat(2.0, len(fake_d))
label_offsets[1::2] = -2.0
for i, (l, d) in enumerate(zip(labels, fake_d)): #dates
#d = d - timedelta(days=90)
align = 'right'
if i % 2 == 0:
align = 'left'
_ = ax.text(label_offsets[i], d, l, ha=align, fontfamily='serif',
fontweight='bold', color='royalblue',fontsize=12)
#stems = np.repeat(2.0, len(dates))
stems = np.repeat(2.0, len(fake_d))
stems[1::2] *= -1.0
#x = ax.hlines(dates, 0, stems, color='darkmagenta')
x = ax.hlines(fake_d, 0, stems, color='darkmagenta')
# hide lines around chart
for spine in ["left", "top", "right", "bottom"]:
_ = ax.spines[spine].set_visible(False)
# hide tick labels
_ = ax.set_xticks([])
_ = ax.set_yticks([])
_ = ax.set_title('UAP (UFO) Milestones, 1940 - Present',
fontweight="bold",
fontfamily='serif',
fontsize=16,
color='darkgreen')

Related

ValueError: Image size of 154660x122 pixels is too large. It must be less than 2^16 in each direction. I am trying to make timeline and getting error

fig, ax = plt.subplots(figsize=(1, 1), constrained_layout=True)
_ = ax.set_ylim(-2, 1.75)
_ = ax.set_xlim("1989", "2021")
_ = ax.axhline(0, xmin=0.05, xmax=0.50, c='deeppink', zorder=1)
_ = ax.scatter(dates, np.zeros(len(dates)), s=120, c='palevioletred', zorder=2)
_ = ax.scatter(dates, np.zeros(len(dates)), s=30, c='darkmagenta', zorder=3)
label_offsets = np.zeros(len(dates))
label_offsets[::2] = 0.25
label_offsets[1::2] = -0.4
for i, (l, d) in enumerate(zip(names, dates)):
_ = ax.text(d, label_offsets[i], l, ha='center', fontfamily='serif', fontweight='bold', color='royalblue',fontsize=8)
I want labels to show up but it shows value error.
UserWarning: constrained_layout not applied because axes sizes collapsed to zero. Try making figure larger or axes decorations smaller.
fig.canvas.print_figure(bytes_io, **kw)
It says value error i have tried changing dimensions but result is same

Matplotlib Annotations disappear during animation when updating xlim

I am having an issue with FuncAnimation where my annotations are removed after updating xlim. Here is the code with a preview underneath
You can try the code in a google colab here https://colab.research.google.com/drive/1NrM-ZnSQKhADccpjCbNeOC5PU8uXw-Sb?authuser=2#scrollTo=bcYtgNaTYJ3g
import os
import matplotlib.animation as ani
import matplotlib.pyplot as plt
from collections import deque
from typing import List
from IPython.display import Image
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
def create_animation_for_data(qty_lists: List[List[int]], gif_path, add_horizontal_guides=True, dynamic_y_axis=True,
dynamic_x_axis=True):
plt.style.use(['dark_background'])
fruits_to_color = ["red", "orange"]
qtys1, qty2 = qty_lists
times = [i for i in range(len(qtys1))]
artists = []
zoom = 0.5
first_image_path = 'assets/apple.png'
first_image = plt.imread(first_image_path)
first_offset_image = OffsetImage(first_image, zoom=zoom)
second_image_path = 'assets/orange.png'
second_image = plt.imread(second_image_path)
second_offset_image = OffsetImage(second_image, zoom=zoom)
# initializing a figure in
# which the graph will be plotted
my_dpi = 200
figsize_pixels = (650, 1000)
figsize = (int(figsize_pixels[0] / my_dpi), figsize_pixels[1] / my_dpi)
fig = plt.figure(figsize=figsize, dpi=my_dpi)
fig.set_tight_layout(True)
records_per_second = 1
seconds_show_on_screen = 30
max_width_on_screen = records_per_second * seconds_show_on_screen
ticks_every = 20
graph_max_y = 100
max_y_list = list(range(60, graph_max_y + 1, ticks_every * 2))
max_y_index = 0
max_y = max_y_list[max_y_index]
highest_max_y = max_y_list[- 1]
xlim_min = times[0]
if dynamic_x_axis:
xlim_max = times[max_width_on_screen - 1]
else:
xlim_max = times[-1]
axes_xlim = (xlim_min, xlim_max)
if dynamic_y_axis:
ylim_max = max_y
else:
ylim_max = highest_max_y
axes_ylim = (0, ylim_max)
ax1 = plt.axes(xlim=axes_xlim, ylim=axes_ylim)
# Set a title
plt.title('Qty over time', fontsize=20)
# Set axis labels
plt.xlabel('Time', fontsize=18)
plt.ylabel('Qty', fontsize=18)
plotlays, plotcols = [2], fruits_to_color
labels = ['Apple', 'Orange']
lines = []
for index in range(len(qty_lists)):
lobj = ax1.plot([], [], lw=5, color=plotcols[index],
label=labels[index])[0]
lines.append(lobj)
# Make sure your axis ticks are large enough to be easily read.
# You don't want your viewers squinting to read your plot.
plt.yticks(range(0, highest_max_y + 1, ticks_every), [str(x) for x in range(0, highest_max_y + 1, ticks_every)],
fontsize=14)
plt.xticks(fontsize=14)
# Provide tick lines across the plot to help your viewers trace along
# the axis ticks. Make sure that the lines are light and small so they
# don't obscure the primary data lines.
if add_horizontal_guides:
max_x = len(times)
for y in range(0, highest_max_y + 1, ticks_every):
plt.plot(times, [y] * max_x, "--",
lw=1, color="white", alpha=0.7)
# Do this after the plotting done above
ax1.set_ylim(*axes_ylim)
# Remove the tick marks; they are unnecessary with the tick lines we just plotted.
plt.tick_params(axis="both", which="both", bottom="off", top="off",
labelbottom="on", left="off", right="off", labelleft="on")
# empty list to store x and y axis values
xdata = deque()
ydata1 = deque()
ydata2 = deque()
first_image_annotation = AnnotationBbox(
first_offset_image, (times[0], 0), xycoords='data', frameon=False)
artists.append(ax1.add_artist(first_image_annotation))
second_image_annotation = AnnotationBbox(
second_offset_image, (times[0], 0), xycoords='data', frameon=False)
artists.append(ax1.add_artist(second_image_annotation))
ann_list = [
first_image_annotation,
second_image_annotation,
]
# animation function
def animate(i):
nonlocal max_y_index
nonlocal ann_list
# appending new points to x, y axes points list
x1_and_x2 = times[i]
xdata.append(x1_and_x2)
y1 = qty_lists[0][i]
ydata1.append(y1)
y2 = qty_lists[1][i]
ydata2.append(y2)
xlist = [xdata, xdata]
ylist = [ydata1, ydata2]
# If we have passed our max_width_on_screen
if len(xdata) > max_width_on_screen:
# Delete the oldest record
xdata.popleft()
max_x = max(xdata)
min_x = max_x - seconds_show_on_screen
# Update our x axis
if dynamic_x_axis:
ax1.set_xlim(min_x, max_x)
graph_max_y_data = max(ydata1[-1], ydata2[-1])
max_y_data = max_y_list[max_y_index]
while graph_max_y_data > max_y_data:
max_y_index += 1
max_y_data = max_y_list[max_y_index]
# Update our y axis
if dynamic_y_axis:
ax1.set_ylim(0, max_y_data)
ydata1.popleft()
ydata2.popleft()
first_image_annotation_xybox = (x1_and_x2, y1)
first_image_annotation.xybox = first_image_annotation_xybox
second_image_annotation_xybox = (x1_and_x2, y2)
second_image_annotation.xybox = second_image_annotation_xybox
for lnum, line in enumerate(lines):
# set data for each line separately.
line.set_data(xlist[lnum], ylist[lnum])
return lines, ann_list
# call the animator
anim = ani.FuncAnimation(fig, animate, frames=len(times), interval=30, blit=False)
# save the animation as gif file
anim.save(gif_path, writer='imagemagick', fps=2)
return os.path.abspath(gif_path)
static_axes_gif = 'FuncAnimation-annotated-static-axes.gif'
print(static_axes_gif)
animation_path_static_axes = create_animation_for_data(data_to_plot,
static_axes_gif,
dynamic_x_axis=False,
dynamic_y_axis=False)
Image(url=animation_path_static_axes)
static_x_gif = 'FuncAnimation-annotated-static-x.gif'
print(static_x_gif)
animation_path_static_x_axis = create_animation_for_data(data_to_plot,
static_x_gif,
dynamic_x_axis=False,
dynamic_y_axis=True)
Image(url=animation_path_static_x_axis)
static_y_gif = 'FuncAnimation-annotated-static-y.gif'
print(static_y_gif)
animation_path_static_y_axis = create_animation_for_data(data_to_plot,
static_y_gif,
dynamic_x_axis=True,
dynamic_y_axis=False)
Image(url=animation_path_static_y_axis)
dynamic_axes_gif = 'FuncAnimation-annotated-dynamic-axes.gif'
print(dynamic_axes_gif)
animation_path_dynamic_axes = create_animation_for_data(data_to_plot,
dynamic_axes_gif,
dynamic_x_axis=True,
dynamic_y_axis=True)
Image(url=animation_path_dynamic_axes)
I am making 4 graphs with variable dynamic axes (dynamic = updating axis during amination):
FuncAnimation-annotated-static-axes.gif
xlim and ylim are fixed
FuncAnimation-annotated-static-x.gif
xlim is fixed
ylim is dynamic
FuncAnimation-annotated-static-y.gif
xlim is dynamic
ylim is fixed
FuncAnimation-annotated-dynamic-axes.gif
xlim and ylim are dynamic
My annotations disappear in the two cases where the xlim is updated:
FuncAnimation-annotated-static-y.gif
FuncAnimation-annotated-dynamic-axes.gif
Note that when xlim is static this doesn't happen:
FuncAnimation-annotated-static-axes.gif
FuncAnimation-annotated-static-x.gif
Does anyone know why this happens or how to update the xlim without removing annotations?
Please let me know if something is unclear / worded poorly as I really need to solve this.
So the issue is with how I was moving my annotation.
This is the fix:
# Don't do this - updating xlim will causing the annotation do disappear
# first_image_annotation_xybox = (x1_and_x2, y1)
# first_image_annotation.xybox = first_image_annotation_xybox
#
# second_image_annotation_xybox = (x1_and_x2, y2)
# second_image_annotation.xybox = second_image_annotation_xybox
for lnum, line in enumerate(lines):
# set data for each line separately.
line.set_data(xlist[lnum], ylist[lnum])
# Do this - Update our annotations
for ann in ann_list:
ann.remove()
ann_list = []
first_image_annotation = AnnotationBbox(
first_offset_image, (x1_and_x2, y1), xycoords='data', frameon=False)
ann_list.append(ax1.add_artist(first_image_annotation))
second_image_annotation = AnnotationBbox(
second_offset_image, (x1_and_x2, y2), xycoords='data', frameon=False)
ann_list.append(ax1.add_artist(second_image_annotation))
return lines, ann_list
The rest of the code is the same. Wonder why this happens on updating xlim and not on updating ylim ¯\(ツ)/¯

How to plot a bar chart with multiple x-axis data?

I'd like to plot a bar chart in Python, similar to Excel. However, I am struggling to have two different x-axes. For example, for each size (like 8M), I want to plot the results of all 5 strategies. For each strategy, there are 3 metrics (Fit, boot, and exp).
You can download the original excel file here here.
This is my code so far:
df = pd.read_excel("data.xlsx",sheet_name="Sheet1")
r1= df['Fit']
r2= df['Boot']
r3= df['Exp']
x= df['strategy']
n_groups = 5
# create plot
fig, ax = plt.subplots()
index = np.arange(n_groups)
names = ["8M","16M","32M","64M","128M"]
bar_width = 0.1
opacity = 0.8
Fit8= [r1[0],r1[1],r1[2],r1[3],r1[4]]
Boot8= [r2[0],r2[1],r2[2],r2[3],r2[4]]
Exp8= [r3[0],r3[1],r3[2],r3[3],r3[4]]
Fit16= [r1[5],r1[6],r1[7],r1[8],r1[9]]
Boot16= [r2[5],r2[6],r2[7],r2[8],r2[9]]
Exp16= [r3[5],r3[6],r3[7],r3[8],r3[9]]
rects1 = plt.bar(
index, Fit8, bar_width,
alpha=opacity,
color='g',
label='Fit'
)
rects2 = plt.bar(
index + 0.1, Boot8, bar_width,
alpha=opacity,
color='b',
label='Boot'
)
rects3 = plt.bar(
index + 0.2, Exp8, bar_width,
alpha=opacity,
color='y',
label='EXP'
)
rects4 = plt.bar(
index + 0.5, Fit16, bar_width,
alpha=opacity,
color='g'
)
rects5 = plt.bar(
index + 0.6, Boot16, bar_width,
alpha=opacity,
color='b'
)
rects6 = plt.bar(
index + 0.7, Exp16, bar_width,
alpha=opacity,
color='y'
)
plt.xticks(index + 0.2, (names))
plt.legend()
plt.tight_layout()
plt.show()
Something like this?
Here the code:
import pandas as pd
import pylab as plt
# read dataframe, take advantage of Multiindex
df = pd.read_excel(
"data.xlsx",
sheet_name="Sheet1", engine='openpyxl',
index_col=[0, 1],
)
# plot the content of the dataframe
ax = df.plot.bar()
# Show minor ticks
ax.minorticks_on()
# Get location of the center of each bar
bar_locations = list(map(lambda x: x.get_x() + x.get_width() / 2., ax.patches))
# Set minor and major tick positions
# Minor are used for S1, ..., S5
# Major for sizes 8M, ..., 128M
# tick locations are sorted according to the 3 metrics, so first all the 25 bars for the fit, then the 25
# for the boot and at the end the 25 for the exp. We set the major tick at the position of the bar at the center
# of the size group, that is the third boot bar of each size.
ax.set_xticks(bar_locations[27:50:5], minor=False) # use the 7th bar of each size group
ax.set_xticks(bar_locations[len(df):2 * len(df)], minor=True) # use the bar in the middle of each group of 3 bars
# Labels for groups of 3 bars and for each group of size
ax.set_xticklabels(df.index.get_level_values(0)[::5], minor=False, rotation=0)
ax.set_xticklabels(df.index.get_level_values(1), minor=True, rotation=0)
# Set tick parameters
ax.tick_params(axis='x', which='major', pad=15, bottom='off')
ax.tick_params(axis='x', which='both', top='off')
# You can use a different color for each group
# You can comment out these lines if you don't like it
size_colors = 'rgbym'
# major ticks
for l, c in zip(ax.get_xticklabels(minor=False), size_colors):
l.set_color(c)
l.set_fontweight('bold')
# minor ticks
for i, l in enumerate(ax.get_xticklabels(minor=True)):
l.set_color(size_colors[i // len(size_colors)])
# remove x axis label
ax.set_xlabel('')
plt.tight_layout()
plt.show()
The main idea here is to use the Multiindex of Pandas, with some minor tweaks.
EDIT
If you want spaces between groups, you can add a dummy category (a.k.a strategy) in the dataframe to create an artificial space, obtaining:
Here the code:
import numpy as np
import pandas as pd
import pylab as plt
# read dataframe, take advantage of Multiindex
df = pd.read_excel(
"data.xlsx",
sheet_name="Sheet1", engine='openpyxl',
index_col=[0, 1],
)
# plot the content of the dataframe
sizes = list(df.index.get_level_values(0).drop_duplicates())
strategies = list(df.index.get_level_values(1).drop_duplicates())
n_sizes = len(sizes)
n_strategies = len(strategies)
n_metrics = len(df.columns)
empty_rows = pd.DataFrame(
data=[[np.nan] * n_metrics] * n_sizes, index=pd.MultiIndex.from_tuples([(s, 'SN') for s in sizes], names=df.index.names),
columns=df.columns,
)
old_columns = list(df.columns)
df = df.merge(empty_rows, how='outer', left_index=True, right_index=True, sort=False).drop(
columns=[f'{c}_y' for c in df.columns]
).sort_index(
ascending=True, level=0, key=lambda x: sorted(x, key=lambda y: int(y[:-1]))
)
df.columns = old_columns
# Update number of strategies
n_strategies += 1
# Plot with Pandas
ax = df.plot.bar()
# Show minor ticks
ax.minorticks_on()
# Get location of the center of each bar
bar_locations = list(map(lambda x: x.get_x() + x.get_width() / 2., ax.patches))
# Set minor and major tick positions
# Major for sizes 8M, ..., 128M
# Minor are used for S1, ..., S5, SN
# Tick locations are sorted according to the 3 metrics, so first 30 (5 sizes * 6 strategies) bars for the fit,
# then 30 (5 sizes * 6 strategies) for the boot and at the end 30 (5 sizes * 6 strategies) for the exp.
# We set the major tick at the position of the bar at the center of the size group (+7),
# that is the third boot bar of each size.
n_bars_per_metric = n_sizes * n_strategies
strategy_ticks = bar_locations[len(df):2 * len(df)]
strategy_ticks = np.concatenate([strategy_ticks[b * n_strategies:b * n_strategies + n_strategies - 1] for b in range(n_sizes)]) # get only positions of the first 5 bars
size_ticks = strategy_ticks[2::n_sizes] + 0.01
ax.set_xticks(size_ticks, minor=False) # use the 7th bar of each size group
ax.set_xticks(strategy_ticks, minor=True) # use the bar in the middle of each group of 3 bars
# Labels for groups of 3 bars and for each group of size
ax.set_xticklabels(sizes, minor=False, rotation=0)
ax.set_xticklabels(strategies * n_sizes, minor=True, rotation=0)
# Set tick parameters
ax.tick_params(axis='x', which='major', pad=15, bottom=False)
ax.tick_params(axis='x', which='both', top=False)
# You can use a different color for each group
# You can comment out these lines if you don't like it
size_colors = 'rgbym'
# major ticks
for l, c in zip(ax.get_xticklabels(minor=False), size_colors):
l.set_color(c)
l.set_fontweight('bold')
# minor ticks
for i, l in enumerate(ax.get_xticklabels(minor=True)):
l.set_color(size_colors[i // len(size_colors)])
# remove x axis label
ax.set_xlabel('')
plt.tight_layout()
plt.show()
As you can see, you have to play with the DataFrame, adding some extra code. Maybe there is a simpler solution, but it was the first that I can think of.

how to edit x-axis label on histogram? on matplotlib

x = np.log(df_pitcher['adj_salary_filled'][df_pitcher['year'] == 2010])
y = np.log(df_pitcher['adj_salary_filled'][df_pitcher['year'] == 2015])
z = np.log(df_pitcher['adj_salary_filled'][df_pitcher['year'] == 2019])
_ = plt.hist(x, bins=20, alpha=0.5, label='2010 Season')
_ = plt.hist(y, bins=20, alpha=0.5, label='2015 Season')
_ = plt.hist(z, bins=20, alpha=0.5, label='2019 Season')
_ = plt.xlabel('Salaries')
_ = plt.ylabel('Frequency')
_ = plt.legend()
_ = plt.title('Distribution of Salaries for Pitchers')
_ = plt.show()
It's basically returning the log of my salaries, anyway to make it return the actual salary amount? I am taking the log because it'll help show the actual distribution.
You can manually get/set the tick positions and the corresponding labels for the X axis using plt.xticks.
A easy way to fix your specific issue is to get the tick values and set their labels to be the square of each value:
tick_values, tick_labels = plt.xticks()
plt.xticks(tick_values, [round(value**2, 3) for value in tick_values])

Legend only for one of two pies in pandas

I made two pies: one is inside the other. I also want to make a legend but only for the inner circle.
One more significant thing: the inner circle has only two labels, that repeated 5 times, so when I make a legend for both pies, I get something like "paid, free, paid, free, etc"
...
titles = ['Free', 'Paid']
subgroup_names= 5*titles
subgroup_size = final.num.tolist()
a, b, c = [plt.cm.Blues, plt.cm.Reds, plt.cm.Greens]
#Outer ring
fig, ax = plt.subplots()
ax.axis('equal')
mypie, _ = ax.pie(group_size, radius = 2.5, labels = group_names,
colors = [a(0.7), a(0.6), a(0.5), a(0.4), a(0.3)])
plt.setp(mypie, width = 1, edgecolor = 'white')
#Inner ring
mypie2, _ = ax.pie(subgroup_size, radius = 1.6, labels = subgroup_names,
labeldistance = 0.7, colors = [b(0.5), c(0.5)])
plt.setp(mypie2, width = 0.8, edgecolor = 'white')
plt.legend()
plt.show()
plt.legend accepts a list of handles and labels as parameters. get_legend_handles_labels() conveniently gets a list of handles and of labels that would normally be used. Via list indexing you can grab the interesting part.
To center the labels inside the plot, the textprops= parameter of plt.pie accepts a horizontal and vertical alignment.
import matplotlib.pyplot as plt
import numpy as np
titles = ['Free', 'Paid']
subgroup_names = 5 * titles
subgroup_size = np.random.uniform(10, 30, len(subgroup_names))
group_size = subgroup_size.reshape(5, 2).sum(axis=1)
group_names = [f'Group {l}' for l in 'abcde']
a, b, c = [plt.cm.Blues, plt.cm.Reds, plt.cm.Greens]
# Outer ring
fig, ax = plt.subplots()
ax.axis('equal')
mypie, _ = ax.pie(group_size, radius=2.5, labels=group_names,
colors=[a(0.7), a(0.6), a(0.5), a(0.4), a(0.3)])
plt.setp(mypie, width=1, edgecolor='white')
# Inner ring
mypie2, _ = ax.pie(subgroup_size, radius=1.6, labels=subgroup_names,
labeldistance=0.7, colors=[b(0.5), c(0.5)],
textprops={'va': 'center', 'ha': 'center'})
plt.setp(mypie2, width=0.8, edgecolor='white')
handles, labels = plt.gca().get_legend_handles_labels()
labels_to_skip = len(group_names)
plt.legend(handles[labels_to_skip:labels_to_skip + 2], labels[labels_to_skip:labels_to_skip + 2])
plt.show()
PS: To leave out the labels from the pie chart and only have them in the legend, call plt.pie() without the labels= parameter. And create the legend from the patches returned by plt.pie() (limited to the first two in this case):
# Inner ring
mypie2, _ = ax.pie(subgroup_size, radius=1.6,
labeldistance=0.7, colors=[b(0.5), c(0.5)])
plt.setp(mypie2, width=0.8, edgecolor='white')
plt.legend(mypie2[:len(titles)], titles)

Categories