0

What I want to do is:

  • Start the figure with two subplots (stacked one above another)
  • Press 'x' on keyboard to: resize figure, and show a third plot on the right
  • Press 'x' again to: resize figure back to original, and hide the third plot (without leaving space for the third plot).

With the example code below, I got to this (matplotlib 3.1.2, Python3 in MINGW64, Windows 10):

out.gif

As it is shown on the gif - even in the starting state, there is some empty space on the right (since I didn't know any better way how to solve this, other than define a grid). Then, when the figure window extends/resizes, it is not "exactly" resized so it fits the third plot.

How can I achieve a toggling of this third plot, such that when it is hidden, there is no extra empty space on the right - and when it is shown, the figure extends exactly so the third plot fits (including margins) (EDIT: and the existing/initial two plots do not change in size)?

The code:

#!/usr/bin/env python3

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt
import numpy as np

default_size_inch = (9, 6)
showThird = False

def onpress(event):
  global fig, ax1, ax2, ax3, showThird
  if event.key == 'x':
    showThird = not showThird
    if showThird:
      fig.set_size_inches(default_size_inch[0]+3, default_size_inch[1], forward=True)
      plt.subplots_adjust(right=0.85) # leave a bit of space on the right
      ax3.set_visible(True)
      ax3.set_axis_on()
    else:
      fig.set_size_inches(default_size_inch[0], default_size_inch[1], forward=True)
      plt.subplots_adjust(right=0.9) # default
      ax3.set_visible(False)
      ax3.set_axis_off()
    fig.canvas.draw()


def main():
  global fig, ax1, ax2, ax3
  xdata = np.arange(0, 101, 1) # 0 to 100, both included
  ydata1 = np.sin(0.01*xdata*np.pi/2)
  ydata2 = 10*np.sin(0.01*xdata*np.pi/4)

  fig = plt.figure(figsize=default_size_inch, dpi=120)
  ax1 = plt.subplot2grid((3,3), (0,0), colspan=2, rowspan=2)
  ax2 = plt.subplot2grid((3,3), (2,0), colspan=2, sharex=ax1)
  ax3 = plt.subplot2grid((3,3), (0,2), rowspan=3)

  ax3.set_visible(False)
  ax3.set_axis_off()

  ax1.plot(xdata, ydata1, color="Red")
  ax2.plot(xdata, ydata2, color="Khaki")

  fig.canvas.mpl_connect('key_press_event', lambda event: onpress(event))
  plt.show()


# ENTRY POINT
if __name__ == '__main__':
  main()
sdbbs
  • 4,270
  • 5
  • 32
  • 87
  • 1
    You need to decide if you want to use a single gridspec or two, one for each state. In the first case you have equations `margin_left1 + axwidth1 + space + axwidth2 + margin_right1 = figwidth1` and `margin_left2 + axwidth1 + space + axwidth2 + margin_right2 = figwidth2`. In the second case, you have `margin_left1 + axwidth1 + margin_right1 = figwidth1` and `margin_left2 + axwidth1 + space + axwidth2 + margin_right2 = figwidth2`. From those you need to calculate the subplot parameters to apply. – ImportanceOfBeingErnest Nov 21 '19 at 16:10
  • @ImportanceOfBeingErnest - is it possible to somehow "change" the gridspec, without changing ax1/ax2 and (redrawing) the plots in them? EDIT: I guess I found an example (yours) of what you're talking about: https://stackoverflow.com/questions/43937066/matplotlib-hide-subplot-and-fill-space-with-other-subplots/43944246 – sdbbs Nov 22 '19 at 09:23
  • 1
    Yes, it's possible to change the gridspec via `.update`. It's also possible to use two different gridspecs, as shown in that example. – ImportanceOfBeingErnest Nov 22 '19 at 13:22

2 Answers2

1

As commented, you essentially have two options; use a single gridspec, or use two, one for each state. Let's look at the first option, using a single gridspec. To this end you would first define all needed parameters in inches, then calculate the subplot parameters (in relative units) for each of the two desired states.

When pressing x you would toggle between the states by updating the gridspec parameters via .update().

import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

w,h = plt.rcParams["figure.figsize"]
# Define dimensions in inches (could also just put numbers here)
left = plt.rcParams["figure.subplot.left"] * w
right = (1 - plt.rcParams["figure.subplot.right"]) * w
wspace = plt.rcParams["figure.subplot.wspace"] * w

figw1, figh1 = (7,5)
ax1width = figw1 - left - right
ax2width = 3.5

#calculate remaining free parameter, the figure width of the enlarged figure
figh2 = figh1
figw2 = left + ax1width + wspace + ax2width + right

#calculate subplot parameters for both cases
subplotpars1 = dict(left = left/figw1, right=(left + ax1width + wspace + ax2width)/figw1,
                    wspace=wspace/(ax1width+ax2width), )
subplotpars2 = dict(left = left/figw2, right=(left + ax1width + wspace + ax2width)/figw2,
                    wspace=wspace/(ax1width+ax2width), )

# create GridSpec
gs = GridSpec(2,2, width_ratios=(ax1width, ax2width), **subplotpars1)
# Create figure with 3 axes
fig = plt.figure(figsize=(figw1, figh1))
ax1 = fig.add_subplot(gs[0,0])
ax2 = fig.add_subplot(gs[1,0])
ax3 = fig.add_subplot(gs[:,1])

ax1.plot([2,4], color="C0")
ax2.plot([0,11], color="C1")
ax3.plot([5,15], color="C2")


# Updating machinery
current_state = [0]
subplotspars = [subplotpars1, subplotpars2]
figsizes = [(figw1, figh1), (figw2, figh2)]

def key_press(evt):
    if evt.key == "x":
        current_state[0] = (current_state[0] + 1) % 2
        gs.update(**subplotspars[current_state[0]])
        fig.set_size_inches(figsizes[current_state[0]], forward=True)
        fig.canvas.draw_idle()

fig.canvas.mpl_connect("key_press_event", key_press)


plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Many thanks, that looks great! I guess, the difficulty I had, was that "*you would first define all needed parameters in inches*" was not really obvious for me - thanks again for providing an example! – sdbbs Nov 25 '19 at 11:58
0

Here is an example that preserves the width of the initial plot - and attempts to scale the figure size once the plot on the right is added; however, since the margins are expressed relatively, and also there is a space between supblots, the main plot "moves" which I don't really want:

out.gif

Code:

#!/usr/bin/env python3

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

default_size_inch = (9, 6)
showThird = False

def onpress(event):
  global fig, ax1, ax2, ax3, showThird, gs1, gs2, xdata, main_plot_width_rel
  if event.key == 'x':
    showThird = not showThird
    if showThird:
      ax1.set_position(gs2[:-1, :-1].get_position(fig))
      ax2.set_position(gs2[2, 0:-1].get_position(fig))
      if 'ax3' not in globals():
        ax3 = fig.add_subplot(gs2[:, 2])
        ax3.plot(xdata, xdata, color="Green")
      ax3.set_visible(True)
      ax3.set_axis_on()
      ax3.set_position(gs2[:, 2].get_position(fig))
      print(vars(fig.subplotpars)) # does not change: {'validate': True, 'left': 0.125, 'right': 0.9, 'bottom': 0.11, 'top': 0.88, 'wspace': 0.2, 'hspace': 0.2}
      print(ax1.get_position()) # Bbox(x0=0.125, y0=0.3817647058823529, x1=0.6264705882352941, y1=0.88)
      print(ax2.get_position()) # Bbox(x0=0.125, y0=0.10999999999999999, x1=0.6264705882352941, y1=0.3364705882352941)
      print(ax3.get_position()) # Bbox(x0=0.6720588235294118, y0=0.10999999999999999, x1=0.9000000000000001, y1=0.88)
      main_plot_width_rel_third = ax1.get_position().x1 - ax1.get_position().x0
      widthfactor = main_plot_width_rel/main_plot_width_rel_third
      new_width_inch = default_size_inch[0]*widthfactor
      print(main_plot_width_rel_third, widthfactor, new_width_inch) # 0.5014705882352941 1.5454545454545452 13.909090909090907
      fig.set_size_inches(new_width_inch, default_size_inch[1], forward=True)
    else:
      ax3.set_visible(False)
      ax3.set_axis_off()
      ax1.set_position(gs1[:-1, :].get_position(fig))
      ax2.set_position(gs1[2, 0:2].get_position(fig))
      print(vars(fig.subplotpars)) # does not change: {'validate': True, 'left': 0.125, 'right': 0.9, 'bottom': 0.11, 'top': 0.88, 'wspace': 0.2, 'hspace': 0.2}
      print(ax1.get_position()) # Bbox(x0=0.125, y0=0.3817647058823529, x1=0.8999999999999999, y1=0.88)
      print(ax2.get_position()) # Bbox(x0=0.125, y0=0.10999999999999999, x1=0.8999999999999999, y1=0.3364705882352941)
      fig.set_size_inches(default_size_inch[0], default_size_inch[1], forward=True)
    fig.canvas.draw()


def main():
  global fig, ax1, ax2, ax3, gs1, gs2, xdata, main_plot_width_rel
  xdata = np.arange(0, 101, 1) # 0 to 100, both included
  ydata1 = np.sin(0.01*xdata*np.pi/2)
  ydata2 = 10*np.sin(0.01*xdata*3*np.pi/4)

  # https://stackoverflow.com/questions/43937066/
  fig = plt.figure(figsize=default_size_inch, dpi=120)
  gs1 = gridspec.GridSpec(3, 2, figure=fig) # , height_ratios=[5, 2, 1], hspace=0.3
  gs2 = gridspec.GridSpec(3, 3, figure=fig) # , height_ratios=[5,3]

  # instead of colspan, specify array elements ranges - see:
  # https://matplotlib.org/3.1.1/gallery/subplots_axes_and_figures/gridspec_multicolumn.html
  # note that e.g. "column 0 and 1" is specified as `0:2` (or, if that is all columns, just `:`); -1 is "last element"
  ax1 = fig.add_subplot(gs1[:-1, :])
  ax2 = fig.add_subplot(gs1[2, 0:2], sharex=ax1)

  ax1.plot(xdata, ydata1, color="Red")
  ax2.plot(xdata, ydata2, color="Khaki")

  # fig.subplotpars => SubplotParams: "All dimensions are fractions of the figure width or height"
  print(vars(fig.subplotpars)) # {'validate': True, 'left': 0.125, 'right': 0.9, 'bottom': 0.11, 'top': 0.88, 'wspace': 0.2, 'hspace': 0.2}
  #print(ax1.rect) # 'AxesSubplot' object has no attribute 'rect'
  #print(ax1.position) # 'AxesSubplot' object has no attribute 'position'
  # SO:58992207
  #print(ax1.get_rect()) # AttributeError: 'AxesSubplot' object has no attribute 'get_rect'
  print(fig.get_size_inches()) # [9. 6.]
  print(ax1.get_position()) # Bbox(x0=0.125, y0=0.3817647058823529, x1=0.8999999999999999, y1=0.88)
  print(ax2.get_position()) # Bbox(x0=0.125, y0=0.10999999999999999, x1=0.8999999999999999, y1=0.3364705882352941)
  #main_plot_width_rel = fig.subplotpars.right - fig.subplotpars.left
  main_plot_width_rel = ax1.get_position().x1 - ax1.get_position().x0
  main_plot_width_inch = main_plot_width_rel*fig.get_size_inches()[0]
  print(main_plot_width_rel, main_plot_width_inch) # 0.7749999999999999 6.975 (rather: 0.775 6.975)

  fig.canvas.mpl_connect('key_press_event', lambda event: onpress(event))
  #fig.canvas.draw()
  plt.show()


# ENTRY POINT
if __name__ == '__main__':
  main()
sdbbs
  • 4,270
  • 5
  • 32
  • 87