diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b12301 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.prerendered/ diff --git a/MPLAnimator.py b/MPLAnimator.py index 4decba1..4badf20 100644 --- a/MPLAnimator.py +++ b/MPLAnimator.py @@ -38,7 +38,7 @@ def __init__(self, name=None, setup_cb=None): if name == None: self.tmpdir = tempfile.TemporaryDirectory() self.dir = self.tmpdir.name - self.name = 'animator_'+self.dir + self.name = 'animator_' + self.dir else: self.dir = '.prerendered/' + name if not os.path.exists(self.dir): @@ -47,7 +47,6 @@ def __init__(self, name=None, setup_cb=None): if setup_cb: setup_cb() - def initUI(self): """Initialize the UI.""" @@ -55,12 +54,14 @@ def initUI(self): self.w = QtWidgets.QWidget() self.layout = QtWidgets.QVBoxLayout() self.w.setLayout(self.layout) + self.w.resizeEvent = self._onResize # using a stacked layout for the figure # allows for quick exchange between pre-rendered image-view vs matplotlib figure self.stack = QtWidgets.QStackedLayout() self.layout.addLayout(self.stack) self.label = QtWidgets.QLabel() + self.label.setAlignment(Qt.AlignCenter) self.stack.addWidget(self.label) self.fig = plt.figure() self.canvas = FigureCanvas(self.fig) @@ -76,7 +77,6 @@ def initUI(self): self.prerender_checkbox = QtWidgets.QCheckBox('Pre-rendered') self.layout.addWidget(self.prerender_checkbox) - def setFrameCallback(self, frame_cb, max_frame): """Set frame-callback relevant attributes. @@ -90,7 +90,6 @@ def setFrameCallback(self, frame_cb, max_frame): self.max_frame = max_frame self.slider.setMaximum(max_frame - 1) - def setClickCallback(self, click_cb): """Set click-callback relevant attributes. @@ -100,7 +99,6 @@ def setClickCallback(self, click_cb): """ self.click_cb = click_cb - def prerender(self): """Prerender the animation.""" if len(os.listdir(self.dir)) == 0: @@ -110,13 +108,12 @@ def prerender(self): self.frame_cb(i) plt.savefig('{}/{}.png'.format(self.dir, i)) - def handleCanvasClick(self, event: matplotlib.backend_bases.MouseEvent): """Unpack canvas click event to click callback function.""" - self.click_cb(**(event.__dict__)) + if hasattr(self, 'click') and self.click is not None: + self.click_cb(**(event.__dict__)) self.visualize() - def visualize(self, i=None): """Update visualization for set frame. @@ -133,30 +130,30 @@ def visualize(self, i=None): self.prerender() if self.stack.currentWidget() != self.label: self.stack.setCurrentWidget(self.label) - pm = QtGui.QPixmap('{}/{}.png'.format(self.dir, i)) - self.label.setPixmap(pm) + self._renderFrameFromFile(i) else: if self.stack.currentWidget() != self.canvas: self.stack.setCurrentWidget(self.canvas) self.frame_cb(i) self.canvas.draw() - def clear(self): """Clear pre-rendered images.""" for file in os.listdir(self.dir): os.remove(self.dir + '/' + file) - - def run(self, clear=False, prerendered=True, initialFrame=0): + def run(self, clear=False, prerendered=False, initialFrame=0): """Start the animator. The function will block and also start PyQt in the background. Args: clear (bool): Whether to clear potentially existing pre-rendered images. - prerendered (bool): Whether to use pre-rendered images. If there are already images saved, these are used. + prerendered (bool): Whether to use pre-rendered images. + If there are already images saved, these are used. + Turned off by default because resizing the window before pre- + rendering will give better quality results. initialFrame (int): Frame number to start the animation with. """ @@ -173,3 +170,18 @@ def run(self, clear=False, prerendered=True, initialFrame=0): self.visualize(initialFrame) self.slider.setValue(initialFrame) self.qApp.exec() + + def _renderFrameFromFile(self, i=None): + """Render graphic from file given a frame number.""" + if i == None: + i = self.slider.value() + pm = QtGui.QPixmap('{}/{}.png'.format(self.dir, i)) + pm = pm.scaled(self.label.width(), self.label.height(), + Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.label.setPixmap(pm) + self.label.setMinimumSize(1, 1) # Allow downsizing the window. + + def _onResize(self, event): # pylint: disable=unused-argument + """Re-render prerendered graphics on window resize.""" + if self.prerender_checkbox.isChecked(): + self._renderFrameFromFile() diff --git a/example.py b/example.py index f0e060d..63cf5e8 100644 --- a/example.py +++ b/example.py @@ -4,31 +4,35 @@ import matplotlib.pyplot as plt from MPLAnimator import Animator + def naive_estimator(x, data, h): - n = sum(1 for d in data if x-h/2 < d <= x+h/2) + n = sum(1 for d in data if x - h / 2 < d <= x + h / 2) N = len(data) - return n/(N*h) + return n / (N * h) + data = [0.5, 0.7, 0.8, 1.9, 2.4, 6.1, 6.2, 7.3] xs = np.arange(0, 8, 0.01) h = 2 hist = [naive_estimator(x, data, h) for x in xs] + def setup(): - plt.gcf().set_size_inches(8,6) + plt.gcf() plt.suptitle("Naive Estimator for h = {}".format(h)) + def frame(i): plt.cla() # plot original data plt.plot(xs, hist) - plt.plot(data, [0]*len(data), 'xk') + plt.plot(data, [0] * len(data), 'xk') plt.axhline(0, color='k', linewidth=0.5) # calculate current interval x = i / 10 - x1, x2 = x-h/2, x+h/2 + x1, x2 = x - h / 2, x + h / 2 # calculate relative width for visualization axis_to_data = plt.gca().transAxes + plt.gca().transData.inverted() @@ -45,11 +49,11 @@ def frame(i): # highlight data in interval highlight_data = [d for d in data if x1 < d <= x2] - plt.plot(highlight_data, [0]*len(highlight_data), 'oC3') + plt.plot(highlight_data, [0] * len(highlight_data), 'oC3') plt.xlim(-0.5, 8.5) a = Animator(name='NaiveEstimator', setup_cb=setup) a.setFrameCallback(frame_cb=frame, max_frame=80) -a.run(clear=False, prerendered=True) +a.run(clear=False, prerendered=False)