4
\$\begingroup\$

I have implemented a class in Python using Matplotlib for visualizing multiple subplots along with navigation buttons to move between different subsets of data. Each subplot displays a contour plot along with a colorbar.

While it kind of works, I am seeking advice on optimizing the code for better performance and efficiency. I want to improve the way subplots are updated and cleared when navigating between different subsets of data. I am open to suggestions. Any insights or alternative approaches to achieve the same functionality would be greatly appreciated.

import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np

class MultipleFigures: 
    def __init__(self, data):
        self.indices = [0, 9]  # 9 subplots
        self.data = data
        self.fig, self.ax = plt.subplots(3, 3, figsize=(15, 7))
        self.ax = self.ax.ravel()
        plt.subplots_adjust(wspace=0.5, hspace=0.5)
        self.cb = list()  # List to store colorbars
        self.plot_initial_data()  # Create initial plots
        nextax = plt.axes([0.8, 0.02, 0.1, 0.04])
        prevax = plt.axes([0.1, 0.02, 0.1, 0.04])
        self.button_next = Button(nextax, 'Next')
        self.button_prev = Button(prevax, 'Previous')
        self.button_next.on_clicked(self.next)
        self.button_prev.on_clicked(self.previous)
        
    def plot_initial_data(self):
        # Create initial plots for the first 9 subplots
        self.temp = self.data[slice(*self.indices)]
        for i, dataframe in enumerate(self.temp):
            axes, colorbar = self.contourplot(dataframe, ax=self.ax[i])  # Plot contour plot and get axes and colorbar
            self.cb.append(colorbar)  # Store colorbar
        self.clear_unused_plots(len(self.data)) # Hide unused plots
        
    def next(self, event): 
        if self.indices[1] >= len(self.data):  # If at the end of data, do nothing
            return 
        else: 
            self._clear_previous_axes()  # Clear previous plots
            self.indices = [i + 9 for i in self.indices]
            self.temp = self.data[self.indices[0]:self.indices[1]]
            self.update_plots(self.temp)
            self.clear_unused_plots(len(self.temp))   # Clear unused plots for the final rows
            
    def previous(self, event): 
        if self.indices[0] == 0:  # If at the beginning of data, do nothing
            return 
        else: 
            self._clear_previous_axes()  # Clear previous plots
            self.indices = [i - 9 for i in self.indices]
            self.temp = self.data[slice(*self.indices)]
            self.update_plots(self.temp)

    def update_plots(self, temp_df):
        # Update plots for the current set of data
        for i, dataframe in enumerate(temp_df):
            self.ax[i].set_visible(True)  # Make subplot visible
            _, colorbar = self.contourplot(dataframe, ax=self.ax[i]) 
            self.cb.append(colorbar)  
        self.fig.canvas.draw_idle()  

    def clear_unused_plots(self, num_plots_to_clear):
        for i in range(num_plots_to_clear, len(self.ax)):
            self.ax[i].clear() 
            self.ax[i].set_visible(False) 

    def _clear_previous_axes(self): 
        for i in range(len(self.temp)): 
            self.cb[i].remove() 
            self.ax[i].clear() 
        self.cb.clear()  

    @staticmethod
    def contourplot(data, ax):
        contour = ax.contourf(data)
        cb = plt.colorbar(contour, ax=ax)
        return ax, cb


%matplotlib qt

data = np.random.rand(20,10,10)
fl_ = MultipleFigures(data)
plt.show()
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

cached bitmaps

Consider asking matplotlib to save a .PNG copy of each displayed plot. Then you can rapidly switch between them upon encountering a cache hit.

Consider employing the streamlit library, which is really quite good at producing a responsive UX with low effort.

range

class MultipleFigures: 
    def __init__(self, data):
        self.indices = [0, 9]  # 9 subplots

I didn't understand that last line. That is, I have trouble seeing how 9 would be a valid index.

If you had assigned range(0, 9) (identical to range(9)), then the interpretation would have been straightforward. We're talking about closed vs half-open intervals, and community conventions about how to represent them.

            self.indices = [i + 9 for i in self.indices]

OIC, I my initial interpretation of "indices" was way off. Apparently they are "initial indices" for each displayed page. Consider using range() with a step=9 parameter. Consider introducing the concept of a page.

Also, I can't say that I'm entirely comfortable with the various plt side effects that the __init__ ctor produces. I would be happier to see them banished to some helper method. The point is that a ctor should set up this object, and not some object off in matplotlib land as well.

Consider introducing the "prev" button before "next", just because we read from left to right.

empty container

        self.cb = list()  # List to store colorbars

Thank you for the comment, it is very helpful. Now that we know how to pronounce it, the abbreviated identifier is just fine.

Conventionally we would assign [] rather than list(). In the perl community, there is more than one way to skin any given cat. In the python community, there is one obvious way to do it.

table of contents

    def plot_initial_data(self): ...      
        self.temp = ...

No, please don't do that.

It would be polite to assign .temp in the __init__ ctor before doing this assignment, just so the Gentle Reader knows the set of state variables we need to keep track of.

But this is obviously not a "temporary" variable, since that would be a local which goes out-of-scope upon return. You have an obligation to give this a proper name rather than some throwaway "pay no attention to this" name, since we have next() coupled to it. You're insisting that we reason about what this variable is doing over the object's lifetime, without offering the scaffolding to do so.

de-indent

    def next(self, event): 
        if ...
            return 
        else:
            [body]

Consider negating the condition: if not c: [body]

Better, keep that condition and elide the else, so the [body] can be de-indented.

Kudos on the nice contourplot helper, BTW.

odd suffix

fl_ = ...

I have no idea what that trailing _ underscore is all about.

Conventionally we would use it to invent a new name which won't shadow a builtin, e.g. map_ or list_. But there is no fl builtin.

Please elide the trailing underscore.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much. I will take these things into account. I am very new to OOP and felt like I was not using classes the right way. \$\endgroup\$
    – Cold Fire
    Apr 16 at 6:53
3
\$\begingroup\$

This line is a syntax error:

%matplotlib qt

% is a Matlab comment. Perhaps you mistakenly included it when you posted the code in the question.


The following code can be simplified by removing the return and the else if the comparison logic is inverted:

    if self.indices[1] >= len(self.data):  # If at the end of data, do nothing
        return 
    else: 

Change it to:

    if self.indices[1] < len(self.data):

Do the same for the other return .. else.


Add docstrings for the class and functions to describe the code.


When I run the code, I see an array of 9 graphs, but there is no description of what I'm looking at. Add the following information to each graph:

  • X-axis label
  • Y-axis label
  • Title

When I click on the Next button, I see a set of new graphs, and the graphs are updated very quickly. It seems efficient already.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you so much. Just a quick note, %matplotlib qt is used in Jupyter Notebooks to display matplotlib plots in a separate interactive window (using Qt for the backend) \$\endgroup\$
    – Cold Fire
    Apr 16 at 7:02
  • \$\begingroup\$ @ColdFire: You're welcome. Good to know about Jupyter. \$\endgroup\$
    – toolic
    Apr 16 at 10:23

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.