简体   繁体   中英

Interactively resize figure and toggle plot visibility in Matplotlib?

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

出.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()

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

在此处输入图像描述

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:

出.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()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM