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' 
5
  • what would the correct output look like in this case? Commented Jul 22, 2015 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) Commented Jul 23, 2015 at 0:09
  • Do you know the number of columns and total number of plots a priori? Commented Jul 23, 2015 at 0: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 Commented Jul 23, 2015 at 0: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. Commented Jul 23, 2015 at 0:30

4 Answers 4

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

Sign up to request clarification or add additional context in comments.

10 Comments

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'
Haha that's Python 2 vs 3 (I think). I'll fix in a second.
I should be on Python 3, and thanx!
@DirkHaupt oh wait, I misread the error. Which version of MPL are you using?
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)
|
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 ''))

Comments

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

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.