This question already has an answer here:
Set subplot(or gridspec) with same size
(1 answer)
Closed 3 years ago.
I am trying to plot 3 graphs in a single row, but all plots should be the same size (at least the same height).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.gridspec as gridspec
x = np.random.uniform(size=2000) - 0.5
y = np.random.uniform(size=2000) - 0.5
z = np.random.uniform(size=2000) - 0.5
DPI = 106
fig = plt.figure(figsize=(900 / DPI, 350 / DPI))
gs = gridspec.GridSpec(1, 3, width_ratios=[1,1,1])
# 1D
r = np.abs(x)
# plot
ax1 = fig.add_subplot(gs[0])
plot = ax1.scatter(x, r, s = 1, c=r, cmap='jet', marker='.', alpha = 1, vmax = 0.5)
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')
ax1.set_aspect('equal')
# 2D
r = np.sqrt(x * x + y * y)
# plot
ax2 = fig.add_subplot(gs[1])
plot = ax2.scatter(x, y, s = 1, c=r, cmap='jet', marker='.', alpha = 1, vmax = 0.5)
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')
ax2.set_aspect('equal')
fig.colorbar(plot, shrink = 1, ax = ax2)
# 3D
r = np.sqrt(x * x + y * y + z * z)
ax3 = fig.add_subplot(gs[2], projection='3d')
plot = ax3.scatter(x, y, z, s = 10, c=r, cmap='jet', marker='.', alpha = 1, vmax = 0.5)
ax3.set_xlabel('$x$')
ax3.set_ylabel('$y$')
ax3.set_zlabel('$z$')
ax3.view_init(30, 240)
ax3.set_aspect('equal', 'box')
fig.colorbar(plot, shrink = 1,ax = ax3)
fig.tight_layout()
The code above does produce three columns of plots, first 1D, than 2D and lastly a 3D plot. However, as you can see from attached image, the plots are not of the same size eventhough I tried using gridspecas suggested here.
Any ideas on how to change the size of subplots?
Matplotlib.pyplot's auto-layout algorithm does not care if you are plotting a 3D object, 2D object or 1D (points). Of course the syntax for defining the object will change and a 3D object will take 3 parameters. But how the objects are placed in a row is not changed. I see a few possible reasons why your specific data may be causing you trouble.
The first image is 2D and has a smaller y-axis scale than the other 2 images. Also the 1st image x-axis scale is twice as wide as y-axis scale is tall. The 2nd and 3rd images include vertical colormaps, which makes these images taller in total.
1) You can change the 1st plot's y-axis to be taller than it currently is.
ax1.set_aspect('equal')
This code in your 1st plot is preventing you from changing the y-axis scale only. You can remove this line and manually set the scale of y-axis scale to be larger.
2) Make your entire row taller, so the vertical colormaps in 2nd and 3rd plots will not determine the overall height of the figure space. Set the figsize's x and y attribute to (12, 12) and see if that fixes the issue. The 2nd number in figsize sets height.
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(12, 12))
3) Alternatively, you can plot 1st graph in it's own 1st row, and 2nd and 3rd graphs in a separate 2nd row. Set nrows to 2 and ncols to 2, then add 1st plot to row 1 and col 1, and 2nd plot to row 2, col 1, and 3rd plot to row 2, col 2.
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 18))
You can refer to Matplotlib documentation for getting details setting layout parameters. Hope one of these will work. :-)
https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.axes.Axes.set_aspect.html
Related
I am plotting separate figures for each attribute and label for each data sample. Here is the illustration:
As illustrated in the the last subplot (Label), my data contains seven classes (numerically) (0 to 6). I'd like to visualize these classes using a different fancy colors and a legend. Please note that I just want colors for last subplot. How should I do that?
Here is the code of above plot:
x, y = test_data["x"], test_data["y"]
# determine the total number of plots
n, off = x.shape[1] + 1, 0
plt.rcParams["figure.figsize"] = (40, 15)
# plot all the attributes
for i in range(6):
plt.subplot(n, 1, off + 1)
plt.plot(x[:, off])
plt.title('Attribute:' + str(i), y=0, loc='left')
off += 1
# plot Labels
plt.subplot(n, 1, n)
plt.plot(y)
plt.title('Label', y=0, loc='left')
plt.savefig(save_file_name, bbox_inches="tight")
plt.close()
First, just to set up a similar dataset:
import matplotlib.pyplot as plt
import numpy as np
x = np.random.random((100,6))
y = np.random.randint(0, 6, (100))
fig, axs = plt.subplots(6, figsize=(40,15))
We could use plt.scatter() to give individual points different marker styles:
for i in range(x.shape[-1]):
axs[i].scatter(range(x.shape[0]), x[:,i], c=y)
Or we could mask the arrays we're plotting:
for i in range(x.shape[-1]):
for j in np.unique(y):
axs[i].plot(np.ma.masked_where(y!=j, x[:,i]), 'o')
Either way we get the same results:
Edit: Ah you've edited your question! You can do exactly the same thing for your last plot only, just modify my code above to take it out of the loop of subplots :)
As suggested, we imitate the matplotlib step function by creating a LineCollection to color the different line segments:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
from matplotlib.patches import Patch
#random data generation
np.random.seed(12345)
number_of_categories=4
y = np.concatenate([np.repeat(np.random.randint(0, number_of_categories), np.random.randint(1, 30)) for _ in range(20)])
#check the results with less points
#y = y[:10]
x = y[None] * np.linspace(1, 5, 3)[:, None]
x += 2 * np.random.random(x.shape) - 1
#your initial plot
num_plots = x.shape[0] + 1
fig, axes = plt.subplots(num_plots, 1, sharex=True, figsize=(10, 8))
for i, ax in enumerate(axes.flat[:-1]):
ax.plot(x[i,:])
#first we create the matplotlib step function with x-values as their midpoint
axes.flat[-1].step(np.arange(y.size), y, where="mid", color="lightgrey", zorder=-1)
#then we plot colored segments with shifted index simulating the step function
shifted_x = np.arange(y.size+1)-0.5
#and identify the step indexes
idx_steps, = np.nonzero(np.diff(y, prepend=np.inf, append=np.inf))
#create collection of plateau segments
colored_segments = np.zeros((idx_steps.size-1, 2, 2))
colored_segments[:, :, 0] = np.vstack((shifted_x[idx_steps[:-1]], shifted_x[idx_steps[1:]])).T
colored_segments[:, :, 1] = np.repeat(y[idx_steps[:-1]], 2).reshape(-1, 2)
#generate discrete color list
n_levels, idx_levels = np.unique(y[idx_steps[:-1]], return_inverse=True)
colorarr = np.asarray(plt.cm.tab10.colors[:n_levels.size])
#and plot the colored segments
lc_cs = LineCollection(colored_segments, colors=colorarr[idx_levels, :], lw=10)
lines_cs = axes.flat[-1].add_collection(lc_cs)
#scaling and legend generation
axes.flat[-1].set_ylim(n_levels.min()-0.5, n_levels.max()+0.5)
axes.flat[-1].legend([Patch(color=colorarr[i, :]) for i, _ in enumerate(n_levels)],
[f"cat {i}" for i in n_levels],
loc="upper center", bbox_to_anchor=(0.5, -0.15),
ncol=n_levels.size)
plt.show()
Sample output:
Alternatively, you can use broken barh plots or color this axis or even all axes using axvspan.
I have configured subplots of (5 x 1) format shown in Fig. 1 as given by Figure block A in the MWE. I am trying to repeat them n times such that they appear in a grid format with number of rows and columns given by the function fitPlots as mentioned here; to give output as shown in Fig. 2.
Fig. 1 Initial plot
Fig. 2 Repeated plot (desired output)
What would be the best way to repeat the code block to create a grid plot with inner subplots? The MWE creates multiple pages, I want all of them on a single page.
MWE
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib.pyplot as plt
import numpy as np
import math
x = np.arange(1, 100, 0.2)
y_a = np.sqrt(x)
y_b = np.sin(x)
y_c = np.sin(x)
y_d = np.cos(x) * np.cos(x)
y_e = 1/x
########## Figure block A #####################
with PdfPages('./plot_grid.pdf') as plot_grid_loop:
fig, (a, b, c, d, e) = plt.subplots(5, 1, sharex=True, gridspec_kw={'height_ratios': [5, 1, 1, 1, 1]})
a.plot(x, y_a)
b.plot(x, y_b)
c.plot(x, y_c)
d.plot(x, y_d)
e.plot(x, y_e)
plot_grid_loop.savefig()
plt.close
########## Figure block A #####################
# from https://stackoverflow.com/a/43366784/4576447
def fitPlots(N, aspect=(16,9)):
width = aspect[0]
height = aspect[1]
area = width*height*1.0
factor = (N/area)**(1/2.0)
cols = math.floor(width*factor)
rows = math.floor(height*factor)
rowFirst = width < height
while rows*cols < N:
if rowFirst:
rows += 1
else:
cols += 1
rowFirst = not(rowFirst)
return rows, cols
n_plots = 15
n_rows, n_cols = fitPlots(n_plots)
with PdfPages('./plot_grid.pdf') as plot_grid_loop:
for m in range(1, n_plots+1):
fig, (a, b, c, d, e) = plt.subplots(5, 1, sharex=True, gridspec_kw={'height_ratios': [5, 1, 1, 1, 1]})
a.plot(x, y_a)
b.plot(x, y_b)
c.plot(x, y_c)
d.plot(x, y_d)
e.plot(x, y_e)
plot_grid_loop.savefig()
plt.close
This can be done by generating a GridSpec object with gs_fig = fig.add_gridspec() that contains enough rows and columns to fit the five figure blocks (note that when you use plt.subplots a GridSpec is also generated and can be accessed with ax.get_gridspec()). Each empty slot in the GridSpec can then be filled with a sub-GridSpec with gs_sub = gs_fig[i].subgridspec() to hold the five subplots. The trickier part is sharing the x-axis. This can be done by generating an empty first Axes with which the x-axis of all the subplots can be shared.
The following example illustrates this with only three figure blocks, based on the code sample you have shared but with some differences regarding the figure design: the number of rows is computed based on the chosen number of columns, and the figure dimensions are set based on a chosen figure width and aspect ratio. The code for saving the figure to a pdf file is not included.
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.4
# Create variables to plot
x = np.arange(1, 100, 0.2)
y_a = np.sqrt(x)
y_b = np.sin(x)
y_c = np.sin(x)
y_d = np.cos(x)*np.cos(x)
y_e = 1/x
# Set parameters for figure dimensions
nplots = 3 # random number of plots for this example
ncols = 2
nrows = int(np.ceil(nplots/ncols))
subp_w = 10/ncols # 10 is the total figure width in inches
subp_h = 1*subp_w # set subplot aspect ratio
# Create figure containing GridSpec object with appropriate dimensions
fig = plt.figure(figsize=(ncols*subp_w, nrows*subp_h))
gs_fig = fig.add_gridspec(nrows, ncols)
# Loop through GridSpec to add sub-GridSpec for each figure block
heights = [5, 1, 1, 1, 1]
for i in range(nplots):
gs_sub = gs_fig[i].subgridspec(len(heights), 1, height_ratios=heights, hspace=0.2)
ax = fig.add_subplot(gs_sub[0, 0]) # generate first empty Axes to enable sharex
ax.axis('off') # remove x and y axes because it is overwritten in the loop below
# Loop through y variables to plot all the subplots with shared x-axis
for j, y in enumerate([y_a, y_b, y_c, y_d, y_e]):
ax = fig.add_subplot(gs_sub[j, 0], sharex=ax)
ax.plot(x, y)
if not ax.is_last_row():
ax.tick_params(labelbottom=False)
Reference: matplotlib tutorial GridSpec using SubplotSpec
I am attempting to produce a plot like this which combines a cartesian scatter plot and a polar histogram. (Radial lines optional)
A similar solution (by Nicolas Legrand) exists for looking at differences in x and y (code here), but we need to look at ratios (i.e. x/y).
More specifically, this is useful when we want to look at the relative risk measure which is the ratio of two probabilities.
The scatter plot on it's own is obviously not a problem, but the polar histogram is more advanced.
The most promising lead I have found is this central example from the matplotlib gallery here
I have attempted to do this, but have run up against the limits of my matplotlib skills. Any efforts moving towards this goal would be great.
I'm sure that others will have better suggestions, but one method that gets something like you want (without the need for extra axes artists) is to use a polar projection with a scatter and bar chart together. Something like
import matplotlib.pyplot as plt
import numpy as np
x = np.random.uniform(size=100)
y = np.random.uniform(size=100)
r = np.sqrt(x**2 + y**2)
phi = np.arctan2(y, x)
h, b = np.histogram(phi, bins=np.linspace(0, np.pi/2, 21), density=True)
colors = plt.cm.Spectral(h / h.max())
ax = plt.subplot(111, projection='polar')
ax.scatter(phi, r, marker='.')
ax.bar(b[:-1], h, width=b[1:] - b[:-1],
align='edge', bottom=np.max(r) + 0.2, color=colors)
# Cut off at 90 degrees
ax.set_thetamax(90)
# Set the r grid to cover the scatter plot
ax.set_rgrids([0, 0.5, 1])
# Let's put a line at 1 assuming we want a ratio of some sort
ax.set_thetagrids([45], [1])
which will give
It is missing axes labels and some beautification, but it might be a place to start. I hope it is helpful.
You can use two axes on top of each other:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6,6))
ax1 = fig.add_axes([0.1,0.1,.8,.8], label="cartesian")
ax2 = fig.add_axes([0.1,0.1,.8,.8], projection="polar", label="polar")
ax2.set_rorigin(-1)
ax2.set_thetamax(90)
plt.show()
Ok. Thanks to the answer from Nicolas, and the answer from tomjn I have a working solution :)
import numpy as np
import matplotlib.pyplot as plt
# Scatter data
n = 50
x = 0.3 + np.random.randn(n)*0.1
y = 0.4 + np.random.randn(n)*0.02
def radial_corner_plot(x, y, n_hist_bins=51):
"""Scatter plot with radial histogram of x/y ratios"""
# Axis setup
fig = plt.figure(figsize=(6,6))
ax1 = fig.add_axes([0.1,0.1,.6,.6], label="cartesian")
ax2 = fig.add_axes([0.1,0.1,.8,.8], projection="polar", label="polar")
ax2.set_rorigin(-20)
ax2.set_thetamax(90)
# define useful constant
offset_in_radians = np.pi/4
def rotate_hist_axis(ax):
"""rotate so that 0 degrees is pointing up and right"""
ax.set_theta_offset(offset_in_radians)
ax.set_thetamin(-45)
ax.set_thetamax(45)
return ax
# Convert scatter data to histogram data
r = np.sqrt(x**2 + y**2)
phi = np.arctan2(y, x)
h, b = np.histogram(phi,
bins=np.linspace(0, np.pi/2, n_hist_bins),
density=True)
# SCATTER PLOT -------------------------------------------------------
ax1.scatter(x,y)
ax1.set(xlim=[0, 1], ylim=[0, 1], xlabel="x", ylabel="y")
ax1.spines['right'].set_visible(False)
ax1.spines['top'].set_visible(False)
# HISTOGRAM ----------------------------------------------------------
ax2 = rotate_hist_axis(ax2)
# rotation of axis requires rotation in bin positions
b = b - offset_in_radians
# plot the histogram
bars = ax2.bar(b[:-1], h, width=b[1:] - b[:-1], align='edge')
def update_hist_ticks(ax, desired_ratios):
"""Update tick positions and corresponding tick labels"""
x = np.ones(len(desired_ratios))
y = 1/desired_ratios
phi = np.arctan2(y,x) - offset_in_radians
# define ticklabels
xticklabels = [str(round(float(label), 2)) for label in desired_ratios]
# apply updates
ax2.set(xticks=phi, xticklabels=xticklabels)
return ax
ax2 = update_hist_ticks(ax2, np.array([1/8, 1/4, 1/2, 1, 2, 4, 8]))
# just have radial grid lines
ax2.grid(which="major", axis="y")
# remove bin count labels
ax2.set_yticks([])
return (fig, [ax1, ax2])
fig, ax = radial_corner_plot(x, y)
Thanks for the pointers!
I am trying to make use the polar plot projection to make a radar chart. I would like to know how to put only one grid line in bold (while the others should remain standard).
For my specific case, I would like to highlight the gridline associated to the ytick "0".
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
#Variables
sespi = pd.read_csv("country_progress.csv")
labels = sespi.country
progress = sespi.progress
angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)
#Concatenation to close the plots
progress=np.concatenate((progress,[progress[0]]))
angles=np.concatenate((angles,[angles[0]]))
#Polar plot
fig=plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.plot(angles, progress, '.--', linewidth=1, c="g")
#ax.fill(angles, progress, alpha=0.25)
ax.set_thetagrids(angles * 180/np.pi, labels)
ax.set_yticklabels([-200,-150,-100,-50,0,50,100,150,200])
#ax.set_title()
ax.grid(True)
plt.show()
The gridlines of a plot are Line2D objects. Therefore you can't make it bold. What you can do (as shown, in part, in the other answer) is to increase the linewidth and change the colour but rather than plot a new line you can do this to the specified gridline.
You first need to find the index of the y tick labels which you want to change:
y_tick_labels = [-100,-10,0,10]
ind = y_tick_labels.index(0) # find index of value 0
You can then get a list of the gridlines using gridlines = ax.yaxis.get_gridlines(). Then use the index you found previously on this list to change the properties of the correct gridline.
Using the example from the gallery as a basis, a full example is shown below:
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # less radial ticks
ax.set_rlabel_position(-22.5) # get radial labels away from plotted line
ax.grid(True)
y_tick_labels = [-100, -10, 0, 10]
ax.set_yticklabels(y_tick_labels)
ind = y_tick_labels.index(0) # find index of value 0
gridlines = ax.yaxis.get_gridlines()
gridlines[ind].set_color("k")
gridlines[ind].set_linewidth(2.5)
plt.show()
Which gives:
It is just a trick, but I guess you could just plot a circle and change its linewidth and color to whatever could be bold for you.
For example:
import matplotlib.pyplot as plt
import numpy as np
Yline = 0
Npoints = 300
angles = np.linspace(0,360,Npoints)*np.pi/180
line = 0*angles + Yline
ax = plt.subplot(111, projection='polar')
plt.plot(angles, line, color = 'k', linewidth = 3)
plt.ylim([-1,1])
plt.grid(True)
plt.show()
In this piece of code, I plot a line using plt.plot between any point of the two vectors angles and line. The former is actually all the angles between 0 and 2*np.pi. The latter is constant, and equal to the 'height' you want to plot that line Yline.
I suggest you try to decrease and increase Npoints while having a look to the documentaion of np.linspace() in order to understand your problem with the roundness of the circle.
I found the following example on matplotlib:
import numpy as np
import matplotlib.pyplot as plt
x1 = np.linspace(0.0, 5.0)
x2 = np.linspace(0.0, 2.0)
y1 = np.cos(2 * np.pi * x1) * np.exp(-x1)
y2 = np.cos(2 * np.pi * x2)
plt.subplot(2, 1, 1)
plt.plot(x1, y1, 'ko-')
plt.title('A tale of 2 subplots')
plt.ylabel('Damped oscillation')
plt.subplot(2, 1, 2)
plt.plot(x2, y2, 'r.-')
plt.xlabel('time (s)')
plt.ylabel('Undamped')
plt.show()
My question is: What do i need to change, to have the plots side-by-side?
Change your subplot settings to:
plt.subplot(1, 2, 1)
...
plt.subplot(1, 2, 2)
The parameters for subplot are: number of rows, number of columns, and which subplot you're currently on. So 1, 2, 1 means "a 1-row, 2-column figure: go to the first subplot." Then 1, 2, 2 means "a 1-row, 2-column figure: go to the second subplot."
You currently are asking for a 2-row, 1-column (that is, one atop the other) layout. You need to ask for a 1-row, 2-column layout instead. When you do, the result will be:
In order to minimize the overlap of subplots, you might want to kick in a:
plt.tight_layout()
before the show. Yielding:
Check this page out: http://matplotlib.org/examples/pylab_examples/subplots_demo.html
plt.subplots is similar. I think it's better since it's easier to set parameters of the figure. The first two arguments define the layout (in your case 1 row, 2 columns), and other parameters change features such as figure size:
import numpy as np
import matplotlib.pyplot as plt
x1 = np.linspace(0.0, 5.0)
x2 = np.linspace(0.0, 2.0)
y1 = np.cos(2 * np.pi * x1) * np.exp(-x1)
y2 = np.cos(2 * np.pi * x2)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(5, 3))
axes[0].plot(x1, y1)
axes[1].plot(x2, y2)
fig.tight_layout()
When stacking subplots in one direction, the matplotlib documentation advocates unpacking immediately if you are just creating a few axes.
fig, (ax1, ax2) = plt.subplots(1,2, figsize=(20,8))
sns.histplot(df['Price'], ax=ax1)
sns.histplot(np.log(df['Price']),ax=ax2)
plt.show()
You can use - matplotlib.gridspec.GridSpec
Check - https://matplotlib.org/stable/api/_as_gen/matplotlib.gridspec.GridSpec.html
The below code displays a heatmap on right and an Image on left.
#Creating 1 row and 2 columns grid
gs = gridspec.GridSpec(1, 2)
fig = plt.figure(figsize=(25,3))
#Using the 1st row and 1st column for plotting heatmap
ax=plt.subplot(gs[0,0])
ax=sns.heatmap([[1,23,5,8,5]],annot=True)
#Using the 1st row and 2nd column to show the image
ax1=plt.subplot(gs[0,1])
ax1.grid(False)
ax1.set_yticklabels([])
ax1.set_xticklabels([])
#The below lines are used to display the image on ax1
image = io.imread("https://images-na.ssl-images- amazon.com/images/I/51MvhqY1qdL._SL160_.jpg")
plt.imshow(image)
plt.show()
Output image
Basically we have to define how many rows and columns we require.
Lets Say we have total 4 categorical columns to be plotted. Lets have total 4 plots in 2 rows and 2 columns.
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
sns.set_style("darkgrid")
%matplotlib inline
#15 by 15 size set for entire plots
plt.figure(figsize=(15,15));
#Set rows variable to 2
rows = 2
#Set columns variable to 2, this way we will plot 2 by 2 = 4 plots
columns = 2
#Set the plot_count variable to 1
#This variable will be used to define which plot out of total 4 plot
plot_count = 1
cat_columns = [col for col in df.columns if df[col].dtype=='O']
for col in cat_columns:
plt.subplot(rows, columns, plot_count)
sns.countplot(x=col, data=df)
plt.xticks(rotation=70);
#plot variable is incremented by 1 till 4, specifying which plot of total 4 plots
plot_count += 1