How to plot scatter pie chart using matplotlib - python

I find the code example for drawing scatter pie chat
In this example, the size of each pie slices is identical across all three scatters. I would like to know if it is possible to make each pie chart unique (different number of slices and different pie proportions)

Yes, it's totally possible. Here's a function that plot a pie chart at given position with a given size:
def draw_pie(dist,
xpos,
ypos,
size,
ax=None):
if ax is None:
fig, ax = plt.subplots(figsize=(10,8))
# for incremental pie slices
cumsum = np.cumsum(dist)
cumsum = cumsum/ cumsum[-1]
pie = [0] + cumsum.tolist()
for r1, r2 in zip(pie[:-1], pie[1:]):
angles = np.linspace(2 * np.pi * r1, 2 * np.pi * r2)
x = [0] + np.cos(angles).tolist()
y = [0] + np.sin(angles).tolist()
xy = np.column_stack([x, y])
ax.scatter([xpos], [ypos], marker=xy, s=size)
return ax
Using that function, we can draw, say three pie charts:
fig, ax = plt.subplots(figsize=(10,8))
draw_pie([1,2,1],1,1,10000,ax=ax)
draw_pie([2,2,2,2], 2, 1, 20000, ax=ax)
draw_pie([1,1,1,1,1], 1.5,1.5, 30000, ax=ax)
plt.xlim(0.6,2.5)
plt.ylim(0.8, 1.8)
plt.show()
gives:

you could implement it like this:
import numpy as np
import matplotlib.pyplot as plt
def drawPieMarker(xs, ys, ratios, sizes, colors):
assert sum(ratios) <= 1, 'sum of ratios needs to be < 1'
markers = []
previous = 0
# calculate the points of the pie pieces
for color, ratio in zip(colors, ratios):
this = 2 * np.pi * ratio + previous
x = [0] + np.cos(np.linspace(previous, this, 10)).tolist() + [0]
y = [0] + np.sin(np.linspace(previous, this, 10)).tolist() + [0]
xy = np.column_stack([x, y])
previous = this
markers.append({'marker':xy, 's':np.abs(xy).max()**2*np.array(sizes), 'facecolor':color})
# scatter each of the pie pieces to create pies
for marker in markers:
ax.scatter(xs, ys, **marker)
fig, ax = plt.subplots()
drawPieMarker(xs=np.random.rand(3),
ys=np.random.rand(3),
ratios=[.3, .2, .5],
sizes=[80, 60, 100],
colors=['cyan', 'orange', 'teal'])
drawPieMarker(xs=np.random.rand(2),
ys=np.random.rand(2),
ratios=[.33, .66],
sizes=[100, 120],
colors=['blue', 'yellow'])
drawPieMarker(xs=np.random.rand(2),
ys=np.random.rand(2),
ratios=[.33, .25],
sizes=[50, 75],
colors=['maroon', 'brown'])
plt.show()

Related

How to Add another subplot to show Solid of Revolution toward x-axis?

I have this code modified from the topic here:
How to produce a revolution of a 2D plot with matplotlib in Python
The plot contains a subplot in the XY plane and another subplot of the solid of revolution toward the y-axis.
I want to add another subplot that is the solid of revolution toward the x-axis + how to add a legend to each subplot (above them), so there will be 3 subplots.
This is my MWE:
# Compare the plot at xy axis with the solid of revolution
# For function x=(y-2)^(1/3)
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
n = 100
fig = plt.figure(figsize=(12,6))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122,projection='3d')
y = np.linspace(np.pi/8, np.pi*40/5, n)
x = (y-2)**(1/3) # x = np.sin(y)
t = np.linspace(0, np.pi*2, n)
xn = np.outer(x, np.cos(t))
yn = np.outer(x, np.sin(t))
zn = np.zeros_like(xn)
for i in range(len(x)):
zn[i:i+1,:] = np.full_like(zn[0,:], y[i])
ax1.plot(x, y)
ax2.plot_surface(xn, yn, zn)
plt.show()
Option 1:
Simply reverse x and y to switch the axes of the function.
x = np.linspace(np.pi/8, np.pi*40/5, n)
y = (x-2)**(1/3)
Option 2:
It is a little complicated. You can also accomplish this by finding the inverse of the original function.
The inverse of f(x) = y = x^3 + 2 is f^{-1}(y) = (y - 2)^(1/3).
I modified the code you provided.
import matplotlib.pyplot as plt
import numpy as np
n = 100
fig = plt.figure(figsize=(14, 7))
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222, projection='3d')
ax3 = fig.add_subplot(223)
ax4 = fig.add_subplot(224, projection='3d')
y = np.linspace(np.pi / 8, np.pi * 40 / 5, n)
x = (y - 2) ** (1 / 3)
t = np.linspace(0, np.pi * 2, n)
xn = np.outer(x, np.cos(t))
yn = np.outer(x, np.sin(t))
zn = np.zeros_like(xn)
for i in range(len(x)):
zn[i:i + 1, :] = np.full_like(zn[0, :], y[i])
ax1.plot(x, y)
ax1.set_title("$f(x)$")
ax2.plot_surface(xn, yn, zn)
ax2.set_title("$f(x)$: Revolution around $y$")
# find the inverse of the function
x_inverse = y
y_inverse = np.power(x_inverse - 2, 1 / 3)
xn_inverse = np.outer(x_inverse, np.cos(t))
yn_inverse = np.outer(x_inverse, np.sin(t))
zn_inverse = np.zeros_like(xn_inverse)
for i in range(len(x_inverse)):
zn_inverse[i:i + 1, :] = np.full_like(zn_inverse[0, :], y_inverse[i])
ax3.plot(x_inverse, y_inverse)
ax3.set_title("Inverse of $f(x)$")
ax4.plot_surface(xn_inverse, yn_inverse, zn_inverse)
ax4.set_title("$f(x)$: Revolution around $x$")
plt.tight_layout()
plt.show()

3D barplot in matplotlib, with scaled gradient colormap corresponding to a 4th dimension (range of values)

I am trying to create a 3D barplot using matplotlib in python, and apply a colormap which is tied some data (4th dimension) which is not explicitly plotted. I think what makes this even more complicated is that I want this 4th dimension to be a range of values as opposed to a single value.
So far I have managed to create the 3D bar plot with a colormap tied to the z-dimension thanks primarily to this post how to plot gradient fill on the 3d bars in matplotlib. The code can be found below.
import numpy as np
import glob,os
from matplotlib import pyplot as plt
import matplotlib.colors as cl
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D
os.chdir('./')
# axis details for the bar plot
x = ['1', '2', '3', '4', '5'] # labels
x_tick_locks = np.arange(0.1, len(x) + 0.1, 1)
x_axis = np.arange(len(x))
y = ['A', 'B']
y_tick_locks = np.arange(-0.1, len(y) - 0.1, 1)
y_axis = np.arange(len(y))
x_axis, y_axis = np.meshgrid(x_axis, y_axis)
x_axis = x_axis.flatten()
y_axis = y_axis.flatten()
x_data_final = np.ones(len(x) * len(y)) * 0.5
y_data_final = np.ones(len(x) * len(y)) * 0.5
z_axis = np.zeros(len(x)*len(y))
z_data_final = [[30, 10, 15, 20, 25], [10, 15, 15, 28, 40]]
values_min = [[5, 1, 6, 8, 3], [2, 1, 3, 9, 4]]
values_max = [[20, 45, 11, 60, 30], [11, 28, 6, 30, 40]]
cmap_max = max(values_max)
cmap_min = min(values_min)
############################### FOR 3D SCALED GRADIENT BARS ###############################
def make_bar(ax, x0=0, y0=0, width = 0.5, height=1 , cmap="plasma",
norm=cl.Normalize(vmin=0, vmax=1), **kwargs ):
# Make data
u = np.linspace(0, 2*np.pi, 4+1)+np.pi/4.
v_ = np.linspace(np.pi/4., 3./4*np.pi, 100)
v = np.linspace(0, np.pi, len(v_)+2 )
v[0] = 0 ; v[-1] = np.pi; v[1:-1] = v_
#print(u)
x = np.outer(np.cos(u), np.sin(v))
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))
xthr = np.sin(np.pi/4.)**2 ; zthr = np.sin(np.pi/4.)
x[x > xthr] = xthr; x[x < -xthr] = -xthr
y[y > xthr] = xthr; y[y < -xthr] = -xthr
z[z > zthr] = zthr ; z[z < -zthr] = -zthr
x *= 1./xthr*width; y *= 1./xthr*width
z += zthr
z *= height/(2.*zthr)
#translate
x += x0; y += y0
#plot
ax.plot_surface(x, y, z, cmap=cmap, norm=norm, **kwargs)
def make_bars(ax, x, y, height, width=1):
widths = np.array(width)*np.ones_like(x)
x = np.array(x).flatten()
y = np.array(y).flatten()
h = np.array(height).flatten()
w = np.array(widths).flatten()
norm = cl.Normalize(vmin=0, vmax=h.max())
for i in range(len(x.flatten())):
make_bar(ax, x0=x[i], y0=y[i], width = w[i] , height=h[i], norm=norm)
############################### FOR 3D SCALED GRADIENT BARS ###############################
# Creating graph surface
fig = plt.figure(figsize=(9,6))
ax = fig.add_subplot(111, projection= Axes3D.name)
ax.azim = 50
ax.dist = 10
ax.elev = 30
ax.invert_xaxis()
ax.set_box_aspect((1, 0.5, 1))
ax.zaxis.labelpad=7
ax.text(0.9, 2.2, 0, 'Group', 'x')
ax.text(-2, 0.7, 0, 'Class', 'y')
ax.set_xticks(x_tick_locks)
ax.set_xticklabels(x, ha='left')
ax.tick_params(axis='x', which='major', pad=-2)
ax.set_yticks(y_tick_locks)
ax.set_yticklabels(y, ha='right', rotation=30)
ax.tick_params(axis='y', which='major', pad=-5)
ax.set_zlabel('Number')
make_bars(ax, x_axis, y_axis, z_data_final, width=0.2, )
fig.colorbar(plt.cm.ScalarMappable(cmap = 'plasma'), ax = ax, shrink=0.8)
#plt.tight_layout() # doesn't seem to work properly for 3d plots?
plt.show()
As I mentioned, I don't want the colormap to be tied to the z-axis but rather a 4th dimension, which is a range. In other words, I want the colours of the colormap to range from cmap_min to cmap_max (so min is 1 and max is 60), then for the bar plot with a z_data_final entry of 30 for example, its colours should correspond with the range of 5 to 20.
Some other posts seem to provide a solution for a single 4th dimensional value, i.e. (python) plot 3d surface with colormap as 4th dimension, function of x,y,z or How to make a 4d plot using Python with matplotlib however I wasn't able to find anything specific to bar plots with a range of values as your 4th dimensional data.
I would appreciate any guidance in this matter, thanks in advance.
This is the 3D bar plot with colormap tied to the z-dimension

Add colorbar labels as text on scatter plot

I have a scatter plot generated using:
x = list(auto['umap1'])
y = list(auto['umap2'])
final_df2 = pd.DataFrame(list(zip(x,y,communities)), columns =['x', 'y', 'cluster'])
no_clusters = max(communities)
cluster_list = list(range (min(communities), no_clusters+1))
fig2, ax = plt.subplots(figsize = (20,15))
plt.scatter(x,y, c=final_df2['cluster'], cmap=plt.cm.get_cmap('hsv', max(cluster_list)), s = 0.5)
plt.title('Phenograph on UMAP - All Markers (auto)', fontsize=15)
plt.xlabel('umap_1', fontsize=15)
plt.ylabel('umap_2', fontsize=15)
plt.colorbar(extend='both',ticks = range(max(cluster_list)))
plt.show()
I wanted to know how can I add the colorbar labels (numbers from 1-31) to the actual clusters on the graph (as text) that each one corresponds to. This is because it is quite hard to tell this from the colours as they loop back to red.
I tried:
n = list(final_df2['cluster'])
for i, txt in enumerate(n):
ax.annotate(txt, (y[i], x[i]))
But this is giving me no luck.
Your code for the annotations is writing an annotation for each and every dot. This just ends in a sea of numbers.
Somehow, you should find a kind of center for each cluster, for example by averaging all the points that belong to the same cluster.
Then, you use the coordinates of the center to position the text. You can give it a background to make it easier to read.
As I don't have your data, the code below simulates some points already around a center.
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
# calculate some random points to serve as cluster centers; run a few steps of a relaxing algorithm to separate them a bit
def random_distibuted_centers():
cx = np.random.uniform(-10, 10, MAX_CLUST + 1)
cy = np.random.uniform(-10, 10, MAX_CLUST + 1)
for _ in range(10):
for i in range(1, MAX_CLUST + 1):
for j in range(1, MAX_CLUST + 1):
if i != j:
dist = np.linalg.norm([cx[i] - cx[j], cy[i] - cy[j]])
if dist < 4:
cx[i] += 0.4 * (cx[i] - cx[j]) / dist
cy[i] += 0.4 * (cy[i] - cy[j]) / dist
return cx, cy
N = 1000
MAX_CLUST = 31
cx, cy = random_distibuted_centers()
# for demonstration purposes, just generate some random points around the centers
x = np.concatenate( [np.random.normal(cx[i], 2, N) for i in range(1,MAX_CLUST+1)])
y = np.concatenate( [np.random.normal(cy[i], 2, N) for i in range(1,MAX_CLUST+1)])
communities = np.repeat(range(1,MAX_CLUST+1), N)
final_df2 = pd.DataFrame({'x':x, 'y':y, 'cluster': communities})
no_clusters = max(communities)
cluster_list = list(range (min(communities), no_clusters+1))
fig2, ax = plt.subplots(figsize = (20,15))
plt.scatter(x,y, c=final_df2['cluster'], cmap=plt.cm.get_cmap('hsv', max(cluster_list)), s=0.5)
plt.title('Phenograph on UMAP - All Markers (auto)', fontsize=15)
plt.xlabel('umap_1', fontsize=15)
plt.ylabel('umap_2', fontsize=15)
plt.colorbar(extend='both',ticks = cluster_list)
bbox_props = dict(boxstyle="circle,pad=0.3", fc="white", ec="black", lw=2, alpha=0.9)
for i in range(1,MAX_CLUST+1):
ax.annotate(i, xy=(cx[i], cy[i]), ha='center', va='center', bbox=bbox_props)
plt.show()

Generating Compound Pie, or Pie of Pie Charts

Below is an example of a compound pie chart, also known as a pie of pie chart drawn using Excel. Is it possible to create a figure like this using python?
Yes, this is possible with matplotlib - below is an example adapted from here.
Code:
import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
import numpy as np
# make figure and assign axis objects
fig = plt.figure(figsize=(9, 5.0625))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
fig.subplots_adjust(wspace=0)
# large pie chart parameters
ratios = [.27, .56, .17]
labels = ['Approve', 'Disapprove', 'Undecided']
explode = [0.1, 0, 0]
# rotate so that first wedge is split by the x-axis
angle = -180 * ratios[0]
ax1.pie(ratios, autopct='%1.1f%%', startangle=angle,
labels=labels, explode=explode)
# small pie chart parameters
ratios = [.20, .20, .60]
labels = ['Male', 'Female', 'Attack Helicopter']
width = .2
ax2.pie(ratios, autopct='%1.1f%%', startangle=angle,
labels=labels, radius=0.5, textprops={'size': 'smaller'})
ax1.set_title('Approval')
ax2.set_title('Gender')
# use ConnectionPatch to draw lines between the two plots
# get the wedge data
theta1, theta2 = ax1.patches[0].theta1, ax1.patches[0].theta2
center, r = ax1.patches[0].center, ax1.patches[0].r
# draw top connecting line
x = r * np.cos(np.pi / 180 * theta2) + center[0]
y = np.sin(np.pi / 180 * theta2) + center[1]
con = ConnectionPatch(xyA=(- width / 2, .5), xyB=(x, y),
coordsA="data", coordsB="data", axesA=ax2, axesB=ax1)
con.set_color([0, 0, 0])
con.set_linewidth(2)
ax2.add_artist(con)
# draw bottom connecting line
x = r * np.cos(np.pi / 180 * theta1) + center[0]
y = np.sin(np.pi / 180 * theta1) + center[1]
con = ConnectionPatch(xyA=(- width / 2, -.5), xyB=(x, y), coordsA="data",
coordsB="data", axesA=ax2, axesB=ax1)
con.set_color([0, 0, 0])
ax2.add_artist(con)
con.set_linewidth(2)
plt.show()
Output:

Matplotlib: Draw pie chart with wedge breakdown into barchart

I've got a pie chart (example) with following fracs = [10, 20, 50, 30]. Drawing this with matplotlib is no problem. How do I get a breakdown of the first wedge (10) into 6 and 4? Ideally, I want a second wedge for the 20, to breakdown into 10, 3, 7. This would be displayed as a barchart near the specific wedge or a pie chart (which would make it a pie of pie chart similar to the ones in Excel).
Here is one way of doing it (possibly not the best...). I've adapted some of the code found here, on the matplotlib site to make a little_pie function, that will draw small pie charts at arbitrary positions.
from pylab import *
import math
import numpy as np
def little_pie(breakdown,location,size):
breakdown = [0] + list(np.cumsum(breakdown)* 1.0 / sum(breakdown))
for i in xrange(len(breakdown)-1):
x = [0] + np.cos(np.linspace(2 * math.pi * breakdown[i], 2 * math.pi *
breakdown[i+1], 20)).tolist()
y = [0] + np.sin(np.linspace(2 * math.pi * breakdown[i], 2 * math.pi *
breakdown[i+1], 20)).tolist()
xy = zip(x,y)
scatter( location[0], location[1], marker=(xy,0), s=size, facecolor=
['gold','yellow', 'orange', 'red','purple','indigo','violet'][i%7])
figure(1, figsize=(6,6))
little_pie([10,3,7],(1,1),600)
little_pie([10,27,4,8,4,5,6,17,33],(-1,1),800)
fracs = [10, 8, 7, 10]
explode=(0, 0, 0.1, 0)
pie(fracs, explode=explode, autopct='%1.1f%%')
show()
I couldn't find a solution for this, so I hacked my own. I used the ConnectionPatch object in the matplotlib.patches module. This allows you to draw lines between different axes in the same figure. The following creates a pie chart on the left and a stacked bar on the right:
import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
import numpy as np
import math
# style choice
plt.style.use('fivethirtyeight')
# make figure and assign axis objects
fig = plt.figure(figsize=(15,7.5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
# pie chart parameters
ratios = [.4, .56, .04]
labels = ['Approve', 'Disapprove', 'Undecided']
explode=[0.1,0,0]
# rotate so that first wedge is split by the x-axis
angle = -180*ratios[0]
ax1.pie(ratios, autopct='%1.1f%%', startangle=angle,
labels=labels,explode=explode )
# bar chart parameters
xpos = 0
bottom = 0
ratios = [.33, .54, .07, .06]
width = .2
colors = ['y','m','#99ff99','#ffcc99']
for j in range(len(ratios)):
height = ratios[j]
ax2.bar(xpos, height, width, bottom=bottom, color=colors[j])
ypos = bottom + ax2.patches[j].get_height()/2
bottom += height
ax2.text(xpos,ypos, "%d%%" %
(ax2.patches[j].get_height()*100), ha='center')
plt.title('Gender of approvers')
plt.legend(('Women', 'Men', 'Gender Neutral', 'Alien'))
plt.axis('off')
plt.xlim(-2.5*width, 2.5*width)
Then I add two lines connected the first wedge of the pie chart with the top and bottom, respectively, of the stacked bar plot:
# use ConnectionPatch to draw lines between the two plots
# get the wedge data for the first group
theta1, theta2 = ax1.patches[0].theta1, ax1.patches[0].theta2
center, r = ax1.patches[0].center, ax1.patches[0].r
bar_height = sum([item.get_height() for item in ax2.patches])
x = r*np.cos(math.pi/180*theta2)+center[0]
y = np.sin(math.pi/180*theta2)+center[1]
con = ConnectionPatch(xyA=(-width/2,bar_height), xyB=(x,y),
coordsA="data", coordsB="data", axesA=ax2, axesB=ax1)
con.set_color([0,0,0])
con.set_linewidth(4)
ax2.add_artist(con)
x = r*np.cos(math.pi/180*theta1)+center[0]
y = np.sin(math.pi/180*theta1)+center[1]
con = ConnectionPatch(xyA=(-width/2,0), xyB=(x,y),
coordsA="data", coordsB="data", axesA=ax2, axesB=ax1)
con.set_color([0,0,0])
ax2.add_artist(con)
con.set_linewidth(4)
plt.show()
Here is the plot:
I haven't used it yet, but you could try: PyGal
In particular: http://pygal.org/en/stable/documentation/types/pie.html#multi-series-pie

Categories