The key to making this work is to manipulate the Bbox of each of the axes. This solution builds off this advice for creating multiple y axes
mpl = matplotlib, np = numpy, pd = pandas
Create the main axes, ax
.
Get the Bbox of the axes, using ax.get_position()
, then get the bounding points, axpos
, using get_points()
.
Create the secondary axes, ax2
, using ax.twinx()
.
Create the Bbox for ax2
, ax2pos
, using mpl.transforms.Bbox( np.copy( axpos ) )
. Be sure to create a copy of the original Bbox points, otherwise both will be modified.
Scale the secondary bounding box. This can be done by setting the x1
property for horizontal scaling, or the y1
property for vertical scaling. (e.g. ax2pos.y1 = 0.5* ax2pos.y1
)
Set the bounding box of the secondary axis using set_position()
.
Turn off the top (or right, for horizontal scaling) spine using ax2.spines[ 'top' ].set_visible( False )
.
You may also have to consider:
Toggling each axes patch as to not paint over previously layers using ax.patch.set_visible()
.
Adjusting the drawing order of the axes using ax.set_zorder()
.
Due to the scaling you may have to adjust the bounding box used when saving the figure. This can be accomplished by creating another Bbox and passing it to savefig()
as the bbox_inches
parameter.
MCV Solution
td = np.linspace( 0, np.pi, 101 )
df = pd.DataFrame( data = {
'main': np.sin( td ),
'secondary': 10 * np.sin( td )
} )
ax = df.plot( df.index.values, 'main' )
axpos = ax.get_position()
ax2 = ax.twinx()
ax2pos = mpl.transforms.Bbox( np.copy( axpos.get_points() ) ) # clone bounding box
ax2pos.y1 = ax2pos.y1 * 0.5 # scale y axis
ax2.set_position( ax2pos ) # set bounding box
ax2.fill_between(
df.index.values, 0, df.secondary,
facecolor = '#a05050',
label = 'Secondary'
)
ax2.spines[ 'right' ].set_position( ( 'axes', 1.05 ) )
ax2.spines[ 'top' ].set_visible( False )
ax2.set_frame_on( True )
ax2.patch.set_visible( False )
ax2.set_ylim( bottom = 0 )
ax2.set_ylabel( 'Secondary' )
ax.set_ylim( 0, 1 )
ax.legend( [ 'Main', 'Secondary' ] )
Full Code Solution
ax = day6Si.plot(
'time', 'pce_rolling', kind = 'line', label = 'Si',
zorder = 40
)
dataPsc[ dataPsc.day == 5 ].plot(
'time', 'pce_rolling', kind = 'line', label = 'PSC',
zorder = 30, ax = ax
)
axpos = ax.get_position()
ax.set_zorder( 30 )
ax.patch.set_visible( False )
ax.set_xlim( 124.5, 141.5 )
ax.set_ylim( 10, 20 )
ax.set_ylabel( 'PCE (%)' )
ax.set_xlabel( 'Time' )
ax.set_xticklabels( [ '4', '6', '8', '10', '12', '14', '16', '18', '20' ] )
ax.legend( loc = 'upper right', bbox_to_anchor = ( 1, 1 ) )
# temperature
ax3 = ax.twinx()
ax3.fill_between(
day6Si.time, 0, day6Si.temperature,
facecolor = '#f0c5b5',
label = 'Temperature (Rel)'
)
ax3pos = mpl.transforms.Bbox( np.copy( axpos.get_points() ) ) # clone bounding box
ax3pos.y1 = ax3pos.y1 * 0.5 # scale y axis
ax3.set_position( ax3pos ) # set bounding box
ax3.set_zorder( 10 )
ax3.spines[ 'right' ].set_position( ( 'axes', 1.025 ) ) # shift y axis
ax3.set_frame_on( True )
ax3.spines[ 'top' ].set_visible( False ) # remove top frame line
ax3.patch.set_visible( True )
ax3.set_ylim( bottom = 0, top = 60 )
ax3.set_ylabel( 'Temperature (C)' )
# intensity
ax2 = ax.twinx()
ax2.fill_between(
day6Si.time, 0, day6Si.intensity,
facecolor = '#dddd99',
label = 'Intensity (Rel)'
)
ax2pos = mpl.transforms.Bbox( np.copy( axpos.get_points() ) ) # clone bounding box
ax2pos.y1 = ax2pos.y1 * 0.33 # scale y axis
ax2.set_position( ax2pos ) # set bounding box
ax2.set_zorder( 20 )
ax2.spines[ 'right' ].set_position( ( 'axes', 1.125 ) ) # shift y asix
ax2.set_frame_on( True )
ax2.spines[ 'top' ].set_visible( False ) # remove top frame
ax2.patch.set_visible( False )
ax2.set_ylim( bottom = 0, top = 1 )
ax2.set_ylabel( 'Intensity (suns)' )
savebox = mpl.transforms.Bbox( [ [ 0, 0 ], [ 10* 1.15, 8 ] ] ) # bounding box in inches for saving
plt.gcf().savefig( figloc + '/day6.svg', format = 'svg', bbox_inches = savebox )
Result
Update
Due to an update in the matplotlib library, a small change must be made for this to work. Performing ax.twinx()
no longer gives you control over the second axis, so it must be added to the figure manually.
mpl = matplotlib, plt = matplotlib.pyplot, np = numpy, pd = pandas
Create the main figure fig
and axes ax
using plt.subplots()
Create the Bbox as before in steps 2, 4, and 5.
Create the secondary axes ax2a
using fig.add_axes()
, with the desired bounding box.
Create the right y-axis by twinning ax2a
, ax2a.twinx()
.
Clean up the secondary axes.
You may also need to consider aligning the x-axes of your primary and secondary axes using ax.set_xlim()
for both of them.
MCV Solution
td = np.linspace( 0, np.pi, 101 )
df = pd.DataFrame( data = {
'main': np.sin( td ),
'secondary': 10 * np.sin( td )
} )
fig, ax = plt.subplots()
df.plot( df.index.values, 'main', ax = ax )
axpos = ax.get_position()
ax2pos = mpl.transforms.Bbox( np.copy( axpos.get_points() ) )
ax2pos.y1 = ax2pos.y1 * 0.5 # scale y axis
ax2a = fig.add_axes( ax2pos ) # create secondary axes
ax2 = ax2a.twinx() # create right y-axis
ax2.fill_between(
df.index.values, 0, df.secondary,
facecolor = '#a05050',
label = 'Secondary'
)
ax2.spines[ 'right' ].set_position( ( 'axes', 1.05 ) )
ax2.spines[ 'top' ].set_visible( False )
ax2.set_frame_on( True )
ax2.patch.set_visible( False )
ax2.set_ylim( bottom = 0 )
ax2.set_ylabel( 'Secondary' )
# clean up secondary axes tick marks and labels
ax2a.tick_params( left = False, bottom = False )
ax2.tick_params( left = False, bottom = False )
ax2a.set_xticklabels( [] )
ax2a.set_yticklabels( [] )
ax.set_ylim( 0, 1 )
ax.legend( [ 'Main', 'Secondary' ] )