0

I want to create a subplot using matplotlib with pysimplegui where each time i select some signals-checkbox(list of values) i should get a plot corresponding to it which will dynamically increase or decrease in size based on my selection so when i select a checkbox the respective plot will be plotted and eqaully spaced and when i deselect a plot it should be gone and other plots should automatically occupy the space Programming Language - Python3 Matplotlib - 3.6.2

My Requirement:

  1. There will always be maximum 10 rows and 1 column in my subplot (matplotlib)
  2. So lets say iam making a call to "plt.show()" after making 1 plot my result should have only one plot at that moment occupying the whole figure
  3. Now iam adding one more plot and then calling "plt.show()", now it should have 2 plots occupying the fig equally this would be the same for 3,4,5 ... 10 plots but all these plots should have a single shared axis whichever is at the bottom
  4. And also a way to delete a plot at any instance of time so for instance i make a plot say axis1 and then I make a plot axis2 now I want axis1 to be deleted after which only my axis2 plot will be there occupying the whole figure

The below code satisfies most of my requirement but its just that the axis are not shared with each other and also the sizing of each plots are different

reff1: Changing matplotlib subplot size/position after axes creation reff2: Dynamically add/create subplots in matplotlib

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
 
number_of_plots = 2
fig = plt.figure()
 
ax = fig.add_subplot(1, 1, 1)
 
gs = gridspec.GridSpec(number_of_plots + 1, 1)
plot_space = gs[0:number_of_plots].get_position(fig)
print(plot_space,
      [plot_space.x0, plot_space.y0, plot_space.width, plot_space.height])
ax.set_position(plot_space)
ax.set_subplotspec(gs[0:number_of_plots])              # only necessary if using tight_layout()
fig.tight_layout()                # not strictly part of the question
ax = fig.add_subplot(gs[2], sharex=ax)
 
plt.show()
Sreenath R
  • 13
  • 6

1 Answers1

0

Here is a basis for the interactive part.

I remove all axes from the figure at each new selection and add the newly selected ones on a relevant GridSpec.

Still axes are not redrawn every time thanks to @lru_cache.

Edit: Added shared x axes

from functools import lru_cache

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.gridspec import GridSpec
from matplotlib.ticker import FuncFormatter, NullFormatter

plt.ion()

NULL_FORMATTER = NullFormatter()
FUNC_FORMATTER = FuncFormatter(lambda x, pos: f"{x:,.0f}")


def main():

    # Emulate data and user selection
    np.random.seed(0)
    datas = generate_data(num_datas=10)
    states = generate_states(list(datas.keys()), num_states=6, max_key_per_state=5)

    fig = plt.figure(figsize=(6, 8))

    @lru_cache
    def get_data_ax(data_id):
        """Create a new ax on fig with corresponding data plotted"""
        # thanks to @lru_cache, axes are not redrawn each time
        print(f"Drawing ax for {data_id}")
        ax = fig.add_subplot()
        ax = plot_data(*datas[data_id], y_label=data_id, ax=ax)
        return ax

    for data_state in states:
        axes = [get_data_ax(data_id) for data_id in data_state]
        update_figure_with_axes(fig, axes)
        plt.pause(1)


def plot_data(x, y, *, y_label, ax=None, marker_sym=None, color_idx=0):
    if ax is None:
        # fig, ax = plt.subplots()  # New figure
        # ax = plt.gca()  # Current axis
        ax = plt.gcf().add_subplot()  # New axis on current figure
    ax.plot(x, y)
    ax.set_ylabel(y_label)
    # Use `marker_sym` and `color_idx`
    return ax


def update_figure_with_axes(fig, axes):
    """Every ax in `axes` should have already been plotted on `fig`"""

    # Remove all previous axes, add relevant ones later
    for ax in fig.axes:
        fig.delaxes(ax)

    # Create a GridSpec for the axes to be plot,
    # with at least 3 rows so plots are not too stretched
    gs = GridSpec(max(3, len(axes)), 1, hspace=0.4)

    # Add each ax one by one
    for ax, sgs in zip(axes, gs):
        ax.set_subplotspec(sgs)  # Place ax at the right location
        ax.xaxis.set_major_formatter(NULL_FORMATTER)  # Remove x ticks labels
        fig.add_subplot(ax)

    # Add back the x ticks labels for the bottom ax
    axes[-1].xaxis.set_major_formatter(FUNC_FORMATTER)

    # Share all axes with the bottom ax
    axes[-1].get_shared_x_axes().join(*axes)
    axes[-1].autoscale()


def generate_data(num_datas):
    """
    Generate `num_datas` random data, with 100 to 200 points,
    starting somewhere in between 100 and 150 on the x axis.
    """
    return {
        f"data{data_idx:02d}": (
            np.arange(
                start := np.random.randint(100, 150),
                start + (num_points := np.random.randint(100, 200)),
            ),  # x
            np.random.randn(num_points),  # y
        )
        for data_idx in range(1, num_datas + 1)
    }


def generate_states(keys, num_states, max_key_per_state):
    """Generate `num_states` draws of `keys`, in between 1 and `max_key_per_state`"""
    return tuple(
        np.random.choice(
            keys,
            replace=False,
            size=np.random.randint(1, max_key_per_state + 1),
        )
        for _ in range(num_states)
    )


if __name__ == "__main__":
    main()

dynamic plots

paime
  • 2,901
  • 1
  • 6
  • 17
  • how do we make the plots shared in the x-axis i.e. there should not be any individual x-ticks for the all the plots above the bottom-most plot – Sreenath R Jan 24 '23 at 19:07
  • Please refer the below screenshot [link](https://lh6.googleusercontent.com/zMe6KUkyTQu-uRQ_g7Ml1IhT1ndKPN7O-j41YouZHhEec7DuzrSUQswaRCOl9quxvS4=w2400) Thanks for your dedicated support @paime – Sreenath R Jan 24 '23 at 19:15
  • I made an edit for that – paime Jan 25 '23 at 08:57
  • it keeps giving me this error when i decorate the function with @lru_cache axes = get_data_ax( TypeError: unhashable type: 'numpy.ndarray this error goes away if i dont use the "lru_cache" decorator Can you help me fix this between my input argument for the function call is `x = np.linspace(0, 20 * np.pi) y = np.sin(x) * 10` i want both the parameters to be passed so i added the extra argument to my function – Sreenath R Jan 25 '23 at 15:39
  • Don't. `lru_cache` uses a dictionary, so arguments must be hashable. But `get_data_ax` should be able to access data, that's why it is defined as a local function, which can thus use the `datas` and `fig` variables from its outer scope. – paime Jan 25 '23 at 17:24
  • So you mean to say instead of passing multiple arguments to the function pass a single dict i.e. **args Just to make things clear this is my function definition `def get_data_ax(x_data, y_data, y_label, marker_sym=None, color_idx=0)` my fig object is initialized outside this function By the way thanks a lot you were been extremely helpful – Sreenath R Jan 25 '23 at 21:47
  • You're welcome. Look at how `lru_cache` works: it creates a dict, and adds item to that dict every time you call the function decorated by `lru_cache`. It uses the arguments of the function as keys, hence the function arguments must be hashable (look what it means). Thus, when you call the function again with the same arguments, it just returns the corresponding dict item without calling the function. It only makes sense if for a given set of arguments the function would return the exact same object (deterministic function). That's why you need to identify the data by some kind of `data_id`. – paime Jan 26 '23 at 08:35
  • I don't know how you actually get and can identify you data, but I tried refactoring (see edit) with a `plot_data(x, y, *, y_label, ax=None, marker_sym=None, color_idx=0)` function. Still the `get_data_ax` function decorated by `lru_cache` needs to have hashable arguments. – paime Jan 26 '23 at 08:41
  • 1
    Hi @paime now it's clear now i understand whats happening, thanks again – Sreenath R Jan 27 '23 at 12:35
  • Hi @paime I am struck with small issue using MultiCursor for the same above implementation i will be happy if you can share your thoughts https://stackoverflow.com/questions/75379130/multicursor-in-matplotlib-over-multiple-subplot-does-not-work – Sreenath R Feb 07 '23 at 21:28
  • You other code is not reproducible. Trying to put a `MultiCursor` in the code above I found out you have to put `multi_cursor = MultiCursor(None, axes, color="r", lw=1)` after the `update_figure_with_axes(fig, axes)` line. For some reason it doesn't work if you put it inside the `update_figure_with_axes` function, and also doesn't work if the returned value is not assigned to `multi_cursor`, even if unused. – paime Feb 08 '23 at 08:01
  • sorry that i did not provide any reproducible code i will update my question and also you can refer the below comment in GitHub(scroll to the last comment) https://github.com/PySimpleGUI/PySimpleGUI/issues/2055#issuecomment-1423178870 – Sreenath R Feb 08 '23 at 20:12