Customizing Nested Donut Charts - Python - python

I believe I am 90% of the way to solving this issue. As can be seen on the attached image, I am trying to create a nested donut chart based on two groups (rem and deep), the values have been plotted to show the proportion (in relation to 100%) a user is achieving. I want to display this in the "apple health ring style", in order for this to be effective, I want to highlight the two "types" (rem and deep) using two different colors. Within the image you can see the DF, the code applied to generate the existing view and the output. To summarize, I want to;
Assign a set color to "rem", and a different one for "deep"
Remove the axis labels and tick marks
Ideally (although I can probably do this), better format the labels (in some way).
Output to image file with black background
import re
# create donut plots
my_dpi=150
plt.figure(figsize=(1500/my_dpi, 900/my_dpi), dpi=my_dpi)
startingRadius = 0.7 + (0.3* (len(Ian_MitchellRD)-1))
for index, row in Ian_MitchellRD.iterrows():
scenario = row["index"]
percentage = row["Ian Mitchell"]
textLabel = scenario + ': ' + percentage+ '%'
print(startingRadius)
percentage = int(re.search(r'\d+', percentage).group())
remainingPie = 100 - percentage
donut_sizes = [remainingPie, percentage]
#colors = ['#FDFEFE','#AED6F1','#5cdb6f','#AED6F1']
plt.title('Proportion of Recommended Sleep Type being Achieved')
plt.text(0.05, startingRadius - 0.20, textLabel, horizontalalignment='left', verticalalignment='center', color='black')
plt.pie(donut_sizes, radius=startingRadius, startangle=90, colors=colors, frame=True,
wedgeprops={"edgecolor": "white", 'linewidth': 2}, )
startingRadius-=0.3
# equal ensures pie chart is drawn as a circle (equal aspect ratio)
plt.axis('equal')
# create circle and place onto pie chart
circle = plt.Circle(xy=(0, 0), radius=0.35, facecolor='white')
plt.gca().add_artist(circle)
plt.show()
See image of what the code current generates:
UPDATE:
I amended the code per the recommendation to look into the example suggested, code is now;
import matplotlib.lines as mlines
fig, ax = plt.subplots()
ax.axis('equal')
width = 0.25
fig.patch.set_facecolor('black')
fig.set_size_inches(10,10)
data_1 = Ian_MitchellRD.iloc[0]['Ian Mitchell']
data_2 = Ian_MitchellRD.iloc[1]['Ian Mitchell']
remainingPie_1 = 100 - data_1
remainingPie_2 = 100 - data_2
donut_sizes_1 = [remainingPie_1, data_1]
donut_sizes_2 = [remainingPie_2, data_2]
pie, _ = ax.pie(donut_sizes_1, radius=1, colors=['black','lightgreen'],startangle=90)
plt.setp( pie, width=width, edgecolor='black')
pie2, _ = ax.pie(donut_sizes_2, radius=1-width,startangle=90, colors=['black','pink'])
plt.setp( pie2, width=width, edgecolor='black')
plt.title("Ian Mitchell - Average % of REM + Deep Sleep vs Recommended", fontfamily='Consolas', size=16, color='white')
#setting up the legend
greenbar= mlines.Line2D([], [], color='lightgreen', marker='s', linestyle='None',
markersize=10, label='REM Sleep')
pinkbar = mlines.Line2D([], [], color='pink', marker='s', linestyle='None',
markersize=10, label='Deep Sleep')
plt.legend(handles=[greenbar, pinkbar],prop={'size': 12}, loc='lower right')
plt.show()
generating the following;
I would really appreciate guidance on adding labels either, directly on the relevant sections of the chart - i.e. the green chart has a label of around 94%, and the pink segment around 55%.
Thanks,
New Version

Related

Matplotlib: Fit plot with labels into subplot area

I want to make a plot with a grid of thumbnails on the left and a line plot on the right. Here is a minimal example
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
for i in range(n_grid):
for j in range(n_grid):
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(i,j))
plt.imshow(np.random.random((16,16)))
ax.set_axis_off()
### Line plot
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(0,n_grid), rowspan=n_grid-1, colspan=n_grid)
plt.plot(np.cumsum(np.random.random(100)), label='Random Sum')
plt.xlim([0, 100])
plt.ylim(0,50)
plt.xlabel('Number', fontsize=12)
plt.ylabel('Sum', fontsize=12)
plt.figtext(0.5, 0.01, f'Unique identifier', ha='center', va='baseline')
#plt.tight_layout()
plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_1.png', dpi=96)
The problem is that the yticklabels and ylabel stick over the center into the area of the thumbnails. The lineplot on the right is too wide.
One common solution found on the internet is using automatic resizing with tight_layout(), so I change the last three lines to
plt.tight_layout()
#plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_2.png', dpi=96)
This does not rescale the lineplot, but instead makes the wspace and hspace attributes so big I get way too much whitespace between the thumbnails.
I am looking for a solution to either
Set wspace and hspace of only the right subplot, not all of them together, or
resize the lineplot to fit into the designated area, without the labels sticking out
It would seem that this is an easy problem, but despite searching for about 2 hours and digging around in the object properties with iPython I found nothing suitable. All solutions seem to change the size and padding of the subplots, not fitting a plot into the area defined with subplot2grid. The only other solution I can think of is a hack that calculates a modified aspect from the value ranges to make the lineplot always a given percentage thinner.
You can play around with subfigures. For example, if you do:
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
# add 2 subfigures
subfigs = fig.subfigures(1, 2, wspace=0)
# add thumbnail grid into left subfig
gsLeft = subfigs[0].add_gridspec(n_grid, n_grid)
axLeft = []
for i in range(n_grid):
for j in range(n_grid):
axLeft.append(subfigs[0].add_subplot(gsLeft[i, j]))
axLeft[-1].imshow(np.random.random((16,16)))
axLeft[-1].set_axis_off()
### Line plot
gsRight = subfigs[1].add_gridspec(3, 1)
axRight = subfigs[1].add_subplot(gsRight[:2, 0])
axRight.plot(np.cumsum(np.random.random(100)), label='Random Sum')
axRight.set_xlim([0, 100])
axRight.set_ylim(0,50)
axRight.set_xlabel('Number', fontsize=12)
axRight.set_ylabel('Sum', fontsize=12)
# adjust subfigures here (play around with these to get the desired effect)
subfigs[0].subplots_adjust(wspace=0.03, hspace=0.03, bottom=0.05, top=0.95, left=0.05, right=0.95)
subfigs[1].subplots_adjust(left=0.01)
# add title (here I've had to add it to the left figure, so it's not centred,
# in my test adding it to the figure itself meant it was not visible, although
# the example in the Matplotlib docs suggests it should work!)
# fig.suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
subfigs[0].suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
fig.savefig("plot_1.png", dpi=150)
This gives:
but you can play around with the values to adjust it as you like.

matplotlib contourf plot adding an extra step size in levels

I'm trying to plot contourf of 7 clusters with min value in array of 0 and max of 6. However, when I plot it, there is an extra step size in the plot (the color pink outlining the brown cluster.Even though the colorbar labeled the pink as 5, it isn't supposed to be there so I'm not sure where it came from since label 5 is for the brown cluster and so on.
enter image description here
plt.clf() #clear figure before
fig=plt.figure(figsize=(10,8))
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=100.0, globe=None))
ax.set_extent([96,105,1,8]) # lon_left, lon_right, lat_below, lat_upper
ax.gridlines(linewidths=0.01, draw_labels=True, alpha= 0.3)
ax.xlocator = mticker.FixedLocator(np.arange(96.,105.,0.5))
ax.ylocator = mticker.FixedLocator(np.arange(1.,8.,0.5))
clevs = np.arange(0,7,1)
cs = ax.contourf(coords[0], coords[1], code_recons[0], vmin=0, vmax=6, cmap="Accent", transform=ccrs.PlateCarree())
ax.coastlines("50m") # avail:110m, 50m, 10m..... '10m' is better resolution than default
cb = plt.colorbar(cs)
n_clusters=7
tick_locs = (np.arange(n_clusters) + 0.5)*(n_clusters-1)/n_clusters
cb.set_ticks(tick_locs)
cb.set_ticklabels(np.arange(n_clusters))
plt.show()
Any help is deeply appreciated.

How to prevent overlapping labels in annotation in Matplotlib map?

I am trying to prevent the labels in the Northeast US map below from overlapping. I have tried to turn labels on and off for certain states in the region, but there definitely is a better way of doing it. Below is my code and output.
csv = pd.read_csv(r'C:\Downloads\Data.csv')
sf = r'C:\Downloads\s_11au16\s_11au16.shp'
US = gpd.read_file(sf)
#Merge them
data = gpd.GeoDataFrame(csv.merge(US))
#set projection
data = data.to_crs(epsg=6923)
#set up basemap
ax = data.plot(figsize = (12,8), column="soil_data", cmap="Greens", edgecolor='black', linewidth=.5, vmin=0, vmax=70,
missing_kwds={"color": "white", "edgecolor": "k", "label": "none"})
ax.set_title("Example", fontsize=18, fontweight='bold')
ax.set_axis_off()
#annotate data
label = data.dropna(subset='soil_data')
label.apply(lambda x: ax.annotate(text=int(x['soil_data']), xy=x.geometry.centroid.coords[0], color="black",
ha='center', fontsize=14, path_effects=[pe.withStroke(linewidth=3,
foreground="white")]), axis=1)
Obviously I cannot test it without your data but if you're willing to try again with adjustText you could try replacing your label.apply(...) with something like that:
texts = []
for i, row in label.iterrows():
texts.append(ax.annotate(text=int(row['soil_data']), xy=row.geometry.centroid.coords[0], color="black",
ha='center', fontsize=14, path_effects=[pe.withStroke(linewidth=3,
foreground="white")]))
adjust_text(texts)
I don't know how adjust_text deals with annotations, so if this doesn't work, you could try converting it to plt.text.
(The matplotlib class Annotation inherits from the Text class)

Python (twinx plot) Wacky graph

Edit: The graph is fixed now but I am having troubles plotting the legend. It only shows legend for 1 of the plots. As seen in the picture below
I am trying to plot a double axis graph with twinx but I am facing some difficulties as seen in the picture below.
Any input is welcomed! If you require any additional information, I am happy to provide them to you.
as compared to the original before plotting z-axis.
I am unsure why my graph is like that as initially before plotting my secondary y axis, (the pink line), the closing value graph can be seen perfectly but now it seems cut.
It may be due to my data as provided below.
Link to testing1.csv: https://filebin.net/ou93iqiinss02l0g
Code I have currently:
# read csv into variable
sg_df_merged = pd.read_csv("testing1.csv", parse_dates=[0], index_col=0)
# define figure
fig = plt.figure()
fig, ax5 = plt.subplots()
ax6 = ax5.twinx()
x = sg_df_merged.index
y = sg_df_merged["Adj Close"]
z = sg_df_merged["Singapore"]
curve1 = ax5.plot(x, y, label="Singapore", color = "c")
curve2 = ax6.plot(x, z, label = "Face Mask Compliance", color = "m")
curves = [curve1, curve2]
# labels for my axis
ax5.set_xlabel("Year")
ax5.set_ylabel("Adjusted Closing Value ($)")
ax6.set_ylabel("% compliance to wearing face mask")
ax5.grid #not sure what this line does actually
# set x-axis values to 45 degree angle
for label in ax5.xaxis.get_ticklabels():
label.set_rotation(45)
ax5.grid(True, color = "k", linestyle = "-", linewidth = 0.3)
plt.gca().legend(loc='center left', bbox_to_anchor=(1.1, 0.5), title = "Country Index")
plt.show();
Initially, I thought it was due to my excel having entire blank lines but I have since removed the rows which can be found here
Also, I have tried to interpolate but somehow it doesn't work. Any suggestions on this is very much welcomed
Only rows that where all NaN, were dropped. There’s still a lot of rows with NaN.
In order for matplotlib to draw connecting lines between two data points, the points must be consecutive.
The plot API isn't connecting the data between the NaN values
This can be dealt with by converting the pandas.Series to a DataFrame, and using .dropna.
See that x has been dropped, because it will not match the index length of y or z. They are shorter after .dropna.
y is now a separate dataframe, where .dropna is used.
z is also a separate dataframe, where .dropna is used.
The x-axis for the plot are the respective indices.
# read csv into variable
sg_df_merged = pd.read_csv("test.csv", parse_dates=[0], index_col=0)
# define figure
fig, ax5 = plt.subplots(figsize=(8, 6))
ax6 = ax5.twinx()
# select specific columns to plot and drop additional NaN
y = pd.DataFrame(sg_df_merged["Adj Close"]).dropna()
z = pd.DataFrame(sg_df_merged["Singapore"]).dropna()
# add plots with markers
curve1 = ax5.plot(y.index, 'Adj Close', data=y, label="Singapore", color = "c", marker='o')
curve2 = ax6.plot(z.index, 'Singapore', data=z, label = "Face Mask Compliance", color = "m", marker='o')
# labels for my axis
ax5.set_xlabel("Year")
ax5.set_ylabel("Adjusted Closing Value ($)")
ax6.set_ylabel("% compliance to wearing face mask")
# rotate xticks
ax5.xaxis.set_tick_params(rotation=45)
# add a grid to ax5
ax5.grid(True, color = "k", linestyle = "-", linewidth = 0.3)
# create a legend for both axes
curves = curve1 + curve2
labels = [l.get_label() for l in curves]
ax5.legend(curves, labels, loc='center left', bbox_to_anchor=(1.1, 0.5), title = "Country Index")
plt.show()

Matplotlib Make Center Circle Transparent

I am plotting a pie chart making background in the png image looks transparent. How can I make the center circle also looks transparent instead of the white color?
import matplotlib.pyplot as plt
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = 'Correct', 'Wrong'
sizes = [20, 80]
fig1, ax1 = plt.subplots()
ax1.pie(sizes,colors=['green','red'], labels=labels,autopct='%1.1f%%',
shadow=True, startangle=90)
centre_circle = plt.Circle((0,0),0.75,edgecolor='black',
facecolor='white',fill=True,linewidth=0.25)
fig1 = plt.gcf()
fig1.gca().add_artist(centre_circle)
ax1.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
fig1.savefig('foo.png', transparent=True)
The way you create the white middle part in the above code is by obfuscating the center of the pie by a circle. This can of course not procude a transparent interior.
A solution to this would also be found in the more sophisticated question Double donut chart in matplotlib. Let me go into detail:
In order to produce a true donut chart with a hole in the middle, one would need to cut the wedges such that they become partial rings. Fortunately, matplotlib provides the tools to do so. A pie chart consists of several wedges.
From the
matplotlib.patches.Wedge documentation we learn
class matplotlib.patches.Wedge(center, r, theta1, theta2, width=None, **kwargs)
Wedge shaped patch.
[...] If width is given, then a partial wedge is drawn from inner radius r - width to outer radius r.
In order to give set the width to all wedges, an easy method is to use plt.setp
wedges, _ = ax.pie([20,80], ...)
plt.setp( wedges, width=0.25)
Complete example:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
fig.set_facecolor("#fff9c9") # set yellow background color to see effect
wedges, text, autotext = ax.pie([25, 40], colors=['limegreen','crimson'],
labels=['Correct', 'Wrong'], autopct='%1.1f%%')
plt.setp( wedges, width=0.25)
ax.set_aspect("equal")
# the produced png will have a transparent background
plt.savefig(__file__+".png", transparent=True)
plt.show()
The following would be a way to tackle the problem if the Wedge did not have a width argument.
Since the pie chart is centered at (0,0), copying the outer path coordinates, reverting them and multiplying by some number smaller 1 (called r for radius in below code), gives the coordinates of the inner ring. Joining those two list of coordinates and taking care of the proper path codes allows to create a ring shape as desired.
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
def cutwedge(wedge, r=0.8):
path = wedge.get_path()
verts = path.vertices[:-3]
codes = path.codes[:-3]
new_verts = np.vstack((verts , verts[::-1]*r, verts[0,:]))
new_codes = np.concatenate((codes , codes[::-1], np.array([79])) )
new_codes[len(codes)] = 2
new_path = mpath.Path(new_verts, new_codes)
new_patch = mpatches.PathPatch(new_path)
new_patch.update_from(wedge)
wedge.set_visible(False)
wedge.axes.add_patch(new_patch)
return new_patch
fig, ax = plt.subplots()
fig.set_facecolor("#fff9c9") # set yellow background color to see effect
wedges, text, autotext = ax.pie([25, 75], colors=['limegreen','indigo'],
labels=['Correct', 'Wrong'], autopct='%1.1f%%')
for w in wedges:
cutwedge(w)
# or try cutwedge(w, r=0.4)
ax.set_aspect("equal")
# the produced png will have a transparent background
plt.savefig(__file__+".png", transparent=True)
plt.show()
The problem is that you didnt really make a real donut chart. With this part of the code
centre_circle = plt.Circle((0,0),0.75,edgecolor='black',
facecolor='white',fill=True,linewidth=0.25)
you drew a circle in the middle of a pie chart. The problem is if you make this circle transparent you will once again see the middle of the pie chart. I recommend using a free photo editing program like pixlr to just make it transparent. Unless you can find a way to make a true donut chart which I unfortunantly do not know how to do it.
Similarly to the Double donut chart in matplotlib solution referenced by importanceofbeingearnest, you will need to use plt.setp(pie, width=width) to set the width of your pie chart, which will make it a true donut instead of a pie chart with a solid circle drawn on top.
import matplotlib.pyplot as plt
fig1, ax1 = plt.subplots()
ax1.axis('equal')
# Set the width of the pie slices;
# this is equivalent to (1.0-0.75), or
# (the radius of the pie chart - the radius of the inner circle)
width=0.25
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = ['Correct', 'Wrong']
sizes = [20., 80.]
# ax1.pie will return three values:
# 1. pie (the dimensions of each wedge of the pie),
# 2. labtext (the coordinates and text for the labels)
# 3. labpct (the coordinates and text of the "XX.X%"" labels)
pie, labtext, labpct = ax1.pie(x=sizes,
labels=labels,
colors=['green','red'],
startangle=90,
shadow=True,
autopct='%1.1f%%'
)
# apply "plt.setp" to set width property
plt.setp(pie, width=width)
# save the figure as transparent
fig1.savefig('foo.png', transparent=True)

Categories

Resources