13

How can I dynamically add a new plot to bunch of subplots if I'm using more than one column to display my subplots? This answers this question for one column, but I cant seem to modify the answers there to make it dynamically add to a subplot with x columns

I modified Sadarthrion's answer and attempted the following. Here, for sake of an example, I made number_of_subplots=11 and num_cols = 3.

import matplotlib.pyplot as plt

def plotSubplots(number_of_subplots,num_cols):
    # Start with one
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot([1,2,3])

    for j in range(number_of_subplots):
        if j > 0: 
            # Now later you get a new subplot; change the geometry of the existing
            n = len(fig.axes)
            for i in range(n):
                fig.axes[i].change_geometry(n+1, num_cols, i+1)

            # Add the new
            ax = fig.add_subplot(n+1, 1, n+1)
            ax.plot([4,5,6])

            plt.show() 

   plotSubplots(11,3) 

enter image description here

As you can see this isn't giving me what I want. The first plot takes up all the columns and the additional plots are smaller than they should be

EDIT:

('2.7.6 | 64-bit | (default, Sep 15 2014, 17:36:35) [MSC v.1500 64 bit (AMD64)]'

Also I have matplotlib version 1.4.3:

import matplotlib as mpl
print mpl.__version__
1.4.3

I tried Paul's answer below and get the following error message:

import math

import matplotlib.pyplot as plt
from matplotlib import gridspec

def do_plot(ax):
    ax.plot([1,2,3], [4,5,6], 'k.')


N = 11
cols = 3
rows = math.ceil(N / cols)

gs = gridspec.GridSpec(rows, cols)
fig = plt.figure()
for n in range(N):
    ax = fig.add_subplot(gs[n])
    do_plot(ax)

fig.tight_layout() 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-f74203b1c1bf> in <module>()
     15 fig = plt.figure()
     16 for n in range(N):
---> 17     ax = fig.add_subplot(gs[n])
     18     do_plot(ax)
     19 

C:\Users\user\AppData\Local\Enthought\Canopy\User\lib\site-packages\matplotlib\figure.pyc in add_subplot(self, *args, **kwargs)
    962                     self._axstack.remove(ax)
    963 
--> 964             a = subplot_class_factory(projection_class)(self, *args, **kwargs)
    965 
    966         self._axstack.add(key, a)

C:\Users\user\AppData\Local\Enthought\Canopy\User\lib\site-packages\matplotlib\axes\_subplots.pyc in __init__(self, fig, *args, **kwargs)
     73             raise ValueError('Illegal argument(s) to subplot: %s' % (args,))
     74 
---> 75         self.update_params()
     76 
     77         # _axes_class is set in the subplot_class_factory

C:\Users\user\AppData\Local\Enthought\Canopy\User\lib\site-packages\matplotlib\axes\_subplots.pyc in update_params(self)
    113         self.figbox, self.rowNum, self.colNum, self.numRows, self.numCols =     114             self.get_subplotspec().get_position(self.figure,
--> 115                                                 return_all=True)
    116 
    117     def is_first_col(self):

C:\Users\user\AppData\Local\Enthought\Canopy\User\lib\site-packages\matplotlib\gridspec.pyc in get_position(self, fig, return_all)
    423 
    424         figBottoms, figTops, figLefts, figRights = --> 425                     gridspec.get_grid_positions(fig)
    426 
    427 

C:\Users\user\AppData\Local\Enthought\Canopy\User\lib\site-packages\matplotlib\gridspec.pyc in get_grid_positions(self, fig)
    103             cellHeights = [netHeight*r/tr for r in self._row_height_ratios]
    104         else:
--> 105             cellHeights = [cellH] * nrows
    106 
    107         sepHeights = [0] + ([sepH] * (nrows-1))

TypeError: can't multiply sequence by non-int of type 'float' 
Community
  • 1
  • 1
Frikster
  • 2,755
  • 5
  • 37
  • 71
  • what would the correct output look like in this case? – Paul H Jul 22 '15 at 23:40
  • 1
    Oh sorry, I thought it was obvious. I just want all the plots tiled and taking as much space of the window as possible. So in the above example, I want the plot that appears at the bottom to appear in the second column. The final set of subplots should be displayed as a 4x3 (4 rows, 3 cols) grid with one empty space at the end (since we only have 11 plots) – Frikster Jul 23 '15 at 00:09
  • Do you know the number of columns and total number of plots *a priori*? – Paul H Jul 23 '15 at 00:10
  • The total number of plots is known from the length of a list that is global and constant but whose length might be different depending on initialization. The number of columns is under user-control. Does this change anything? Both are variables either way. I modified the question. I think it is clearer now – Frikster Jul 23 '15 at 00:26
  • 1
    See my answer. I was concerned that knowledge of the number of columns or the total numbers of plots were coming from say, a GUI event that could change midway through drawing. Things would be a lot more complicated then. – Paul H Jul 23 '15 at 00:30

4 Answers4

24

Assuming at least 1 dimension of your grid and the total number of plots is known, I would use the gridspec module and a little bit of math.

import math

import matplotlib.pyplot as plt
from matplotlib import gridspec

def do_plot(ax):
    ax.plot([1,2,3], [4,5,6], 'k.')


N = 11
cols = 3
rows = int(math.ceil(N / cols))

gs = gridspec.GridSpec(rows, cols)
fig = plt.figure()
for n in range(N):
    ax = fig.add_subplot(gs[n])
    do_plot(ax)

fig.tight_layout()

enter image description here

Paul H
  • 65,268
  • 20
  • 159
  • 136
  • I get the following error when I copy-paste your code to enthought canopy: TypeError: can't multiply sequence by non-int of type 'float' . Error occurs on "ax = fig.add_subplot(gs[n])" Naively changing it to "ax = fig.add_subplot(int(gs[n]))" results in another error: TypeError: int() argument must be a string or a number, not 'SubplotSpec' – Frikster Jul 23 '15 at 00:35
  • Haha that's Python 2 vs 3 (I think). I'll fix in a second. – Paul H Jul 23 '15 at 00:35
  • I should be on Python 3, and thanx! – Frikster Jul 23 '15 at 00:36
  • @DirkHaupt oh wait, I misread the error. Which version of MPL are you using? – Paul H Jul 23 '15 at 00:38
  • 1.4.2 (took me a while to figure out that the add comment button wasn't broken and that I needed to insert more characters) – Frikster Jul 23 '15 at 00:41
  • @DirkHaupt Weird. That should work then -- unless this was a bug fixed with v1.4.3, which I'm using from python 3. – Paul H Jul 23 '15 at 00:43
  • Well would you look at that, my package manager appears to be stuck behind a proxy firewall. Let me guess. That's an old version? *reads your comment* drats. Hmm, guess I'll try to get my package manager up again and see if that changes anything – Frikster Jul 23 '15 at 00:44
  • Thank you indeed! I was not aware of gridspec which makes all this a lot easier! – Sardathrion - against SE abuse Jul 23 '15 at 06:54
  • I am sad to report that I still get "TypeError: can't multiply sequence by non-int of type 'float' " copy-pasting the above code. I am running python 2.6.7 (I read 3 isn't supported yet in canopy) and I did update MPL to v1.4.3. As such my problem remains – Frikster Jul 23 '15 at 17:11
  • @DirkHaupt don't cast `gs[n]` to `int` -- that doesn't make any sense. Instead cast `rows` (`rows = math.ceil(N / cols)` -- see the edit) – Paul H Jul 23 '15 at 17:13
1

Here's the solution I ended up with. It lets you reference subplots by name, and adds a new subplot if that name has not been used yet, repositioning all previous subplots in the process.

Usage:

set_named_subplot('plot-a')  # Create a new plot
plt.plot(np.sin(np.linspace(0, 10, 100)))  # Plot a curve

set_named_subplot('plot-b')  # Create a new plot
plt.imshow(np.random.randn(10, 10))   # Draw image

set_named_subplot('plot-a')   # Set the first plot as the current one
plt.plot(np.cos(np.linspace(0, 10, 100)))  # Plot another curve in the first plot

plt.show()  # Will show two plots

The code:

import matplotlib.pyplot as plt
import numpy as np


def add_subplot(fig = None, layout = 'grid'):
    """
    Add a subplot, and adjust the positions of the other subplots appropriately.
    Lifted from this answer: http://stackoverflow.com/a/29962074/851699

    :param fig: The figure, or None to select current figure
    :param layout: 'h' for horizontal layout, 'v' for vertical layout, 'g' for approximately-square grid
    :return: A new axes object
    """
    if fig is None:
        fig = plt.gcf()
    n = len(fig.axes)
    n_rows, n_cols = (1, n+1) if layout in ('h', 'horizontal') else (n+1, 1) if layout in ('v', 'vertical') else \
        vector_length_to_tile_dims(n+1) if layout in ('g', 'grid') else bad_value(layout)
    for i in range(n):
        fig.axes[i].change_geometry(n_rows, n_cols, i+1)
    ax = fig.add_subplot(n_rows, n_cols, n+1)
    return ax


_subplots = {}


def set_named_subplot(name, fig=None, layout='grid'):
    """
    Set the current axes.  If "name" has been defined, just return that axes, otherwise make a new one.

    :param name: The name of the subplot
    :param fig: The figure, or None to select current figure
    :param layout: 'h' for horizontal layout, 'v' for vertical layout, 'g' for approximately-square grid
    :return: An axes object
    """
    if name in _subplots:
        plt.subplot(_subplots[name])
    else:
        _subplots[name] = add_subplot(fig=fig, layout=layout)
    return _subplots[name]


def vector_length_to_tile_dims(vector_length):
    """
    You have vector_length tiles to put in a 2-D grid.  Find the size
    of the grid that best matches the desired aspect ratio.

    TODO: Actually do this with aspect ratio

    :param vector_length:
    :param desired_aspect_ratio:
    :return: n_rows, n_cols
    """
    n_cols = np.ceil(np.sqrt(vector_length))
    n_rows = np.ceil(vector_length/n_cols)
    grid_shape = int(n_rows), int(n_cols)
    return grid_shape


def bad_value(value, explanation = None):
    """
    :param value: Raise ValueError.  Useful when doing conditional assignment.
    e.g.
    dutch_hand = 'links' if eng_hand=='left' else 'rechts' if eng_hand=='right' else bad_value(eng_hand)
    """
    raise ValueError('Bad Value: %s%s' % (value, ': '+explanation if explanation is not None else ''))
Peter
  • 12,274
  • 9
  • 71
  • 86
1

I wrote a function that automatically formats all of the subplots into a compact, square-like shape.

To go off of Paul H's answer, we can use gridspec to dynamically add subplots to a figure. However, I then made an improvement. My code automatically arranges the subplots so that the entire figure will be compact and square-like. This ensures that there will be roughly the same amount of rows and columns in the subplot.

The number of columns equals the square root of n_plots rounded down and then enough rows are created so there will be enough spots for all of the subplots.

Check it out:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import gridspec

def arrange_subplots(xs, ys, n_plots):
  """
  ---- Parameters ----
  xs (n_plots, d): list with n_plot different lists of x values that we wish to plot
  ys (n_plots, d): list with n_plot different lists of y values that we wish to plot
  n_plots (int): the number of desired subplots
  """

  # compute the number of rows and columns
  n_cols = int(np.sqrt(n_plots))
  n_rows = int(np.ceil(n_plots / n_cols))

  # setup the plot
  gs = gridspec.GridSpec(n_rows, n_cols)
  scale = max(n_cols, n_rows)
  fig = plt.figure(figsize=(5 * scale, 5 * scale))

  # loop through each subplot and plot values there
  for i in range(n_plots):
    ax = fig.add_subplot(gs[i])
    ax.plot(xs[i], ys[i])

Here are a couple of example images to compare

n_plots = 5

enter image description here

n_plots = 6

enter image description here

n_plots = 10

enter image description here

n_plots = 15

enter image description here

E. Turok
  • 106
  • 1
  • 7
0
from math import ceil
from PyQt5 import QtWidgets, QtCore
from matplotlib.gridspec import GridSpec
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar


class MplCanvas(FigureCanvas):
    """
    Frontend class. This is the FigureCanvas as well as plotting functionality.
    Plotting use pyqt5.
    """

    def __init__(self, parent=None):
        self.figure = Figure()

        gs = GridSpec(1,1)

        self.figure.add_subplot(gs[0])
        self.axes = self.figure.axes

        super().__init__(self.figure)

        self.canvas = self.figure.canvas
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        self.updateGeometry()
        self.setParent(parent)

    def add(self, cols=2):
        N = len(self.axes) + 1
        rows = int(ceil(N / cols))
        grid = GridSpec(rows, cols)

        for gs, ax in zip(grid, self.axes):
            ax.set_position(gs.get_position(self.figure))

        self.figure.add_subplot(grid[N-1])
        self.axes = self.figure.axes
        self.canvas.draw()

Was doing some PyQt5 work, but the add method shows how to dynamically add a new subplot. The set_position method of Axes is used to change the old position to new position. Then you add a new subplot with the new position.