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()
asked Apr 15, 2024 at 21:02
\$\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.

answered Apr 15, 2024 at 23:19
\$\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\$ Commented Apr 16, 2024 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.

answered Apr 15, 2024 at 23:07
\$\endgroup\$
1
  • 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\$ Commented Apr 16, 2024 at 7:02

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.