Suppose something comes up in my plot that mandates that I change the height ratio between two subplots that I've generated within my plot. I've tried changing GridSpec's height ratio to no avail.
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure()
gs = GridSpec(2, 1, height_ratios=[2, 1])
ax1 = fig.add_subplot(gs[0])
ax1 = fig.axes[0]
ax2 = fig.add_subplot(gs[1])
ax2 = fig.axes[1]
ax1.plot([0, 1], [0, 1])
ax2.plot([0, 1], [1, 0])
gs.height_ratios = [2, 5]
The last line has no effect on the plot ratio.
In my actual code, it is not feasible without major reworking to set the height_ratios to 2:5 ahead of time.
How do I get this to update like I want?
The axes of relevant subplots can be manipulated and adjusted to get new height ratios.
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure()
gs = GridSpec(2, 1, height_ratios=[2, 1]) #nrows, ncols
ax1 = fig.add_subplot(gs[0])
ax1 = fig.axes[0]
ax2 = fig.add_subplot(gs[1])
ax2 = fig.axes[1]
ax1.plot([0, 1], [0, 1])
ax2.plot([0, 1], [1, 0])
# new height ratio: 2:5 is required for the 2 subplots
rw, rh = 2, 5
# get dimensions of the 2 axes
box1 = ax1.get_position()
box2 = ax2.get_position()
# current dimensions
w1,h1 = box1.x1-box1.x0, box1.y1-box1.y0
w2,h2 = box2.x1-box2.x0, box2.y1-box2.y0
top1 = box1.y0+h1
#top2 = box2.y0+h2
full_h = h1+h2 #total height
# compute new heights for each axes
new_h1 = full_h*rw/(rw + rh)
new_h2 = full_h*rh/(rw + rh)
#btm1,btm2 = box1.y0, box2.y0
new_bottom1 = top1-new_h1
# finally, set new location/dimensions of the axes
ax1.set_position([box1.x0, new_bottom1, w1, new_h1])
ax2.set_position([box2.x0, box2.y0, w2, new_h2])
plt.show()
The output for ratio: (2, 5):
The output for (2, 10):
How can I add a colorbar scale to the 2nd & 3rd subplots, such that it is inline with my legends in the 1st and 4th subplots? Or, another way to say the question: how can I add a colorbar scale without changing the alignment/justification of the 2nd & 3rd subplots?
There are good examples available on setting colorbar locations (e.g., here on stackoverflow and in the matplotlib docs), but I still haven't been able to solve this.
Below is a reproducible example. The real data are more complicated, and this is part of a loop to produce many figures, so the "extra" stuff about setting axis limits and subplot aspect ratios is needed and will change with different datasets.
Using Python 3.8.
Reproducible example without colorbar
## Specify axes limits, tick intervals, and aspect ratio
xl, yl, xytick, ar = [-40000,120000], [-30000,10000], 20000, 0.8
## Global plot layout stuff
fig = plt.figure(figsize=(10, 7.5), constrained_layout=True)
gs = fig.add_gridspec(4, 1)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0], sharex = ax1, sharey = ax1)
ax3 = fig.add_subplot(gs[2, 0], sharex = ax1)
ax4 = fig.add_subplot(gs[3, 0], sharex = ax1, sharey = ax3)
fig.execute_constrained_layout()
fig.suptitle('Suptitle')
## First Plot
ax1.plot([-30000, 500], [-2000, -21000], c='red', label='A')
ax1.plot([80000, 110000], [-9000, 800], c='blue', label='B')
ax1.set_title('ax1', style='italic');
ax1.set_xlabel('x');
ax1.set_ylabel('beta');
ax1.set_xlim(xl)
ax1.set_ylim(yl)
ax1.xaxis.set_major_locator(ticker.MultipleLocator(xytick))
ax1.yaxis.set_major_locator(ticker.MultipleLocator(xytick))
ax1.legend(handles=leg, bbox_to_anchor=(1.05, 1), loc='upper left')
ax1.set_aspect(aspect=ar)
## Dummy data for plots 2/3/4
x = [-15000, -2000, 0, 5000, 6000, 11000, 18000, 21000, 25000, 36000, 62000]
beta = [1000, 200, -800, 100, 1000, -2000, -5000, -5000, -15000, -21000, -1500]
y = [0.01, 0.2, 1.3, 0.35, 0.88, 2.2, 2.5, 1.25, 3.4, 4.1, 2.1]
## Second Plot
vals = ax2.scatter(x, beta, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax2.set_title('ax2', style='italic');
ax2.set_xlabel('x');
ax2.set_ylabel('beta');
ax2.set_aspect(aspect=ar)
## Attempt to add colorbar
#cbar = fig.colorbar(vals, ax=ax2, format = '%1.2g', location='right', aspect=25)
#cbar.ax.set_ylabel('y')
#cbar.ax.yaxis.set_label_position('left')
#cbar_range = [min(y), max(y)]
#ticklabels = cbar.ax.get_ymajorticklabels()
#cbarticks = list(cbar.get_ticks())
#cbar.set_ticks(cbar_range + cbarticks)
## Third Plot
ax3.scatter(x, y, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax3.set_title('ax3', style='italic');
ax3.set_xlabel('x');
ax3.set_ylabel('y');
ax3.yaxis.set_major_formatter(FormatStrFormatter('%1.2g'))
## Fourth Plot
ax4.scatter(x, y, c='black', label='Dots')
ax4.set_title('ax4', style='italic');
ax4.set_xlabel('x');
ax4.set_ylabel('y');
ax4.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
## Clean-up, set aspect ratios
figW, figH = ax1.get_figure().get_size_inches()
_, _, w, h = ax1.get_position().bounds
disp_ratio = (figH * h) / (figW * w)
data_ratio = sub(*ax3.get_ylim()) / sub(*ax3.get_xlim())
ax3.set_aspect(aspect=disp_ratio / data_ratio )
ax4.set_aspect(aspect=disp_ratio / data_ratio)
## Clean-up, turn axis ticks back on after messing with cbar
#ax1.tick_params(axis='both', which='both', labelbottom='on')
#ax2.tick_params(axis='both', which='both', labelbottom='on')
#ax3.tick_params(axis='both', which='both', labelbottom='on')
Result when trying colorbar, note misalignment of second plot
Suggest you simplify your code and make sure it all works; for instance I have no idea what sub does.
A partial solution to your problem could be panchor=False, which is a bit of an obscure kwarg, but...
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
## Specify axes limits, tick intervals, and aspect ratio
ar = 1.2
## Global plot layout stuff
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 4), constrained_layout=True, sharex=True, sharey=True)
## First Plot
ax1.plot([-20_000, 20_000], [-20_000, 20_000] )
ax1.set_aspect(aspect=ar)
## Dummy data for plots 2/3/4
x = [-15000, -2000, 0, 5000, 6000, 11000, 18000, 21000, 25000, 36000, 62000]
beta = [1000, 200, -800, 100, 1000, -2000, -5000, -5000, -15000, -21000, -1500]
y = [0.01, 0.2, 1.3, 0.35, 0.88, 2.2, 2.5, 1.25, 3.4, 4.1, 2.1]
## Second Plot
vals = ax2.scatter(x, beta, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax2.set_aspect(aspect=ar)
cbar = fig.colorbar(vals, ax=ax2, format = '%1.2g', location='right',
aspect=25, panchor=False)
plt.show()
Depending on the size of the figure, this could comically place the colorbar far to the right. The problem here is the aspect ratio of your plots, which makes the actual axes more narrow than the figure. But the colorbar doesn't really know about that, and places itself on the outside of the space allocated for the axes.
If this is displeasing, then you can also specify an inset axes for the colorbar.
cbax = ax2.inset_axes([1.05, 0.2, 0.05, 0.6], transform=ax2.transAxes)
cbar = fig.colorbar(vals, cax=cbax, format = '%1.2g', orientation='vertical')
Using inset_axes() solves this, as suggested in the other answer, but the parameters relative to the transform were not explained in the example, but I was able to figure it out with some research.
The parameters in inset_axes are [x-corner, y-corner, width, height] and the transform is like a local reference. So, using [1,0,0.5,0.75] means: x = 100% or end of parent ax; y = 0% or bottom of parent ax; width = 50% of parent ax; and height = 75% of parent ax.
Here I wanted the colorbar to be the same height as the parent ax (ax2 and ax3), very thin, and offset a little bit to be more in line with the other legends. Using cbax = ax2.inset_axes([1.1, 0, 0.03, 1], transform=ax2.transAxes) achieves this.
This code works for any aspect ratio ar.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.colors as mcolors
from operator import sub
%matplotlib inline
plt.style.use('seaborn-whitegrid')
## Specify axes limits, tick intervals, and aspect ratio
xl, yl, ar = [-40000,120000], [-30000,10000], .5
## Global plot layout stuff
fig = plt.figure(figsize=(10, 7.5), constrained_layout=True)
gs = fig.add_gridspec(4, 1)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0], sharex = ax1, sharey = ax1)
ax3 = fig.add_subplot(gs[2, 0], sharex = ax1)
ax4 = fig.add_subplot(gs[3, 0], sharex = ax1, sharey = ax3)
fig.execute_constrained_layout()
fig.suptitle('Suptitle')
## First Plot
ax1.plot([-30000, 500], [-2000, -21000], c='red', label='A')
ax1.plot([80000, 110000], [-9000, 800], c='blue', label='B')
ax1.set_title('ax1', style='italic');
ax1.set_xlim(xl)
ax1.set_ylim(yl)
ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax1.set_aspect(aspect=ar)
## Dummy data for plots 2/3/4
x = [-15000, -2000, 0, 5000, 6000, 11000, 18000, 21000, 25000, 36000, 62000]
beta = [1000, 200, -800, 100, 1000, -2000, -5000, -5000, -15000, -21000, -1500]
y = [0.01, 0.2, 1.3, 0.35, 0.88, 2.2, 2.5, 1.25, 3.4, 4.1, 2.1]
## Second Plot
vals = ax2.scatter(x, beta, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax2.set_title('ax2', style='italic');
ax2.set_aspect(aspect=ar)
cbax = ax2.inset_axes([1.1, 0, 0.03, 1], transform=ax2.transAxes)
cbar2 = fig.colorbar(vals, cax=cbax, format = '%1.2g', orientation='vertical')
## Third Plot
ax3.scatter(x, y, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax3.set_title('ax3', style='italic');
cbax = ax3.inset_axes([1.1, 0, 0.03, 1], transform=ax3.transAxes)
cbar3 = fig.colorbar(vals, cax=cbax, format = '%1.2g', orientation='vertical')
## Fourth Plot
ax4.scatter(x, y, c='black', label='Dots')
ax4.set_title('ax4', style='italic');
ax4.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
## Clean-up, set aspect ratios
figW, figH = ax1.get_figure().get_size_inches()
_, _, w, h = ax1.get_position().bounds
disp_ratio = (figH * h) / (figW * w)
data_ratio = sub(*ax3.get_ylim()) / sub(*ax3.get_xlim())
ax3.set_aspect(aspect=disp_ratio / data_ratio )
ax4.set_aspect(aspect=disp_ratio / data_ratio)
## Colorbars
cbar2.ax.set_ylabel('y')
cbar2.ax.yaxis.set_label_position('left')
cbar3.ax.set_ylabel('y')
cbar3.ax.yaxis.set_label_position('left')
Result with aspect ratio = 0.5 for top 2 plots
Result with aspect ratio = 2 for top 2 plots
I am applying this strategy to place legend outside plot. The main difference here is that there are ax1 and ax2 twin axes.
The x value in bbox_to_anchor is set to 0.89 in the following MWE.
As can be seen, the legend box does not display the entire string labels for each color:
MWE:
import matplotlib.pyplot as plt
import numpy as np
suptitle_label = "rrrrrrrr # ttttt yyyyyyy. uuuuuuuuuuuuuuuuuu\n$[$Xx$_{2}$Yy$_{7}]^{-}$ + $[$XxYy$_{2}$(cccc)$_{2}]^{+}$ JjYy model"
# Plotting
fig, ax1 = plt.subplots()
ax1.set_xlabel('Time')
ax1.set_ylabel('y1label')
new_time = np.linspace(1, 8, 100)
j_data = [np.linspace(1, 4, 100), np.linspace(1, 5, 100), np.linspace(1, 6, 100), np.linspace(1, 7, 100)]
sorted_new_LABELS_fmt = ['$[$XxYy$_{2}$(cc)$_{2}]^{+}$', '$[$Xx$_{2}$Yy$_{7}]^{-}$', '$[$XxYy$_{4}]^{-}$', '$[$Xx$_{2}$Yy$_{5}$(cc)$_{2}]^{+}$']
sorted_new_LABELS_colors = ['green', 'red', 'blue', 'orange']
for j,k,c in zip(j_data, sorted_new_LABELS_fmt, sorted_new_LABELS_colors):
ax1.plot(new_time, j, label='%s' % k, color='%s' %c)
All_e_chunks_n = np.linspace(-850, -860, 100)
ax2 = ax1.twinx()
ax2.set_ylabel('y2label')
ax2.plot(new_time, All_e_chunks_n, 'grey', alpha=0.6, linewidth=2.5, label='y2')
# Shrink cccrent axis
box = ax1.get_position()
ax1.set_position([box.x0, box.y0, box.width * 0.9, box.height])
# Put the legend:
fig.legend(loc='center left', bbox_to_anchor=(0.89, 0.5))
fig.suptitle(suptitle_label, fontsize=15)
fig.savefig('mwe.pdf', bbox_inches='tight')
Decreasing this x value and commenting out thebbox_inches='tight' part, yields the following:
For bbox_to_anchor=(0.85, 0.5), this is the result:
For bbox_to_anchor=(0.80, 0.5), this is the result:
For bbox_to_anchor=(0.7, 0.5), this is the result:
I'd like to create a categorical plot of two pandas DataFrame columns a and b in the same figure with shared x and different y axis:
import pandas as pd
import seaborn as sns
example = [
('exp1','f0', 0.25, 2),
('exp1','f1', 0.5, 3),
('exp1','f2', 0.75, 4),
('exp2','f1', -0.25, 1),
('exp2','f2', 1, 2),
('exp2','f3', 0, 3)
]
df = pd.DataFrame(example, columns=['exp', 'split', 'a', 'b'])
mean_df = df.groupby('exp')['a'].mean()
g = sns.catplot(x='exp', y='a', data=df, jitter=False)
ax2 = plt.twinx()
sns.catplot(x='exp', y='b', data=df, jitter=False, ax=ax2)
In this implementation I have the problem that the colors are different for categories (x-values), not for the columns. Can I sole this or do I have to change the data structure?
I would also like to connect the means of the categorical values like in the image like this:
You may want to melt your data first:
data = df.melt(id_vars='exp', value_vars=['a','b'])
fig, ax = plt.subplots()
sns.scatterplot(data=data,
x='exp',
hue='variable',
y='value',
ax=ax)
(data.groupby(['exp','variable'])['value']
.mean()
.unstack('variable')
.plot(ax=ax, legend=False)
)
ax.set_xlim(-0.5, 1.5);
Output:
df = pd.DataFrame(example, columns=['exp', 'split', 'a', 'b'])
mean_df = df.groupby('exp').mean().reset_index()
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
sns.scatterplot(x='exp', y='a', data=df, color='C0', ax=ax1)
sns.scatterplot(x='exp', y='b', data=df, color='C1', ax=ax2)
sns.lineplot(x='exp',y='a', data=mean_df, color='C0', ax=ax1)
sns.lineplot(x='exp',y='b', data=mean_df, color='C1', ax=ax2)