0

I'm using matplotlib to display several data sets in a single plot. I would like to have multiple "external" y axes for the secondary data, and would like those axes to have the same range, but be scaled shorter than that of the main plot axis.

Current plot

Desired plot

I'm creating the additional y axes using twinx() as describes in this post

ax = day6Si.plot( 
  'time', 'pce_rolling'
)

dataPsc[ dataPsc.day == 5 ].plot( 
  'time', 'pce_rolling', ax = ax 
)

ax3 = ax.twinx()
ax3.fill_between( 
  day6Si.time, 0, day6Si.temperature
)

rspine = ax3.spines[ 'right' ]
rspine.set_position( ( 'axes', 1.15 ) )
ax3.set_frame_on( True )
ax3.patch.set_visible( False )
ax3.set_ylim( bottom = 0 )


ax2 = ax.twinx()
ax2.fill_between( 
  day6Si.time, 0, day6Si.intensity
)

rspine = ax2.spines[ 'right' ]
rspine.set_position( ( 'axes', 1.05 ) )
ax2.set_frame_on( True )
ax2.patch.set_visible( False )
ax2.set_ylim( bottom = 0 )

MCV Example

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' )

ax2 = ax.twinx()
ax2.fill_between( 
  df.index.values, 0, df.secondary, 
  facecolor = '#a05050', 
  label = 'Secondary'
)

rspine = ax2.spines[ 'right' ]
rspine.set_position( ( 'axes', 1.05 ) )
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' ] )
bicarlsen
  • 1,241
  • 10
  • 27

1 Answers1

0

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

  1. Create the main axes, ax.

  2. Get the Bbox of the axes, using ax.get_position(), then get the bounding points, axpos, using get_points().

  3. Create the secondary axes, ax2, using ax.twinx().

  4. 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.

  5. 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)

  6. Set the bounding box of the secondary axis using set_position().

  7. 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

  1. Create the main figure fig and axes ax using plt.subplots()

  2. Create the Bbox as before in steps 2, 4, and 5.

  3. Create the secondary axes ax2a using fig.add_axes(), with the desired bounding box.

  4. Create the right y-axis by twinning ax2a, ax2a.twinx().

  5. 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' ] )
bicarlsen
  • 1,241
  • 10
  • 27