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()
2 Answers 2
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.
-
\$\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– Cold Fire2024年04月16日 06:53:11 +00:00Commented Apr 16, 2024 at 6:53
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.
-
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– Cold Fire2024年04月16日 07:02:30 +00:00Commented Apr 16, 2024 at 7:02
Explore related questions
See similar questions with these tags.