Repository: enthought/chaco

Issue:
ImagePlot.draw generates a fatal error when the window showing a plot was resized too small
**Problem Description**
I have a plot shown in a window. There are a few tools added to the plot: a hovering tool, a selection tool, a zoom tool. When I resize the window so small that the plot does not show the application crashes. According to the traceback the error comes from Chaco.

**Reproduction Steps:**

I tried to reproduce with simple plots, but I cannot. My first thought is that the bug is related to the presence of the tools. I can share the traceback

```
Traceback (most recent call last):
  File "lib/python3.6/site-packages/enable/qt4/base_window.py", line 213, in paintEvent
    self.handler.paintEvent(event)
  File "lib/python3.6/site-packages/enable/qt4/base_window.py", line 65, in paintEvent
    self._enable_window._paint(event)
  File lib/python3.6/site-packages/enable/abstract_window.py", line 471, in _paint
    self.component.draw(gc, view_bounds=(0, 0, size[0], size[1]))
  File "lib/python3.6/site-packages/enable/component.py", line 427, in draw
    self._draw(gc, view_bounds, mode)
  File "lib/python3.6/site-packages/enable/component.py", line 779, in _draw
    self._dispatch_draw(layer, gc, view_bounds, mode)
  File "lib/python3.6/site-packages/enable/container.py", line 273, in _dispatch_draw
    component._dispatch_draw(layer, gc, new_bounds, mode)
  File "lib/python3.6/site-packages/enable/component.py", line 799, in _dispatch_draw
    handler(gc, view_bounds, mode)
  File "lib/python3.6/site-packages/chaco/base_2d_plot.py", line 204, in _draw_image
    self._render(gc)
  File "lib/python3.6/site-packages/chaco/image_plot.py", line 124, in _render
    self._compute_cached_image()
  File "lib/python3.6/site-packages/chaco/image_plot.py", line 255, in _compute_cached_image
    col_min, col_max, row_min, row_max = index_bounds
TypeError: 'NoneType' object is not iterable
Fatal Python error: Aborted
```

**Expected behavior:**

I would expect the plot to simply not show when the window is too small and that it would not crash my application. I would also expect that the tools won't be available.

**OS, Python version:** [Enter OS name and Python version]
The bug can be reproduced on Linux and MacOS, Python 3.6


Discussion:
The same bug (same trace at least) happens when I use a ZoomTool on a plot and that I use a box to select an area on which to zoom. If that box has a size of 0 in one direction then I got the same fatal error. I'll try to create a simple code sample that reproduce the bug.
By testing some more the initial bug I posted, I realized that it is not due to resizing, but it is a bug that happens when I try to have a specific size for the window. A width of 201 is what makes my UI crash. That explains why when I do a quick resize I do not see the bug. Someone else tried on MacOS and reproduce the bug at the same width of 201. I expect that the specific width at which the bug appear is related to my specific plot. It is at the limit when the plot is displayed or not displayed.
Bringing this up again as the workaround we had no longer works.

We had subclassed `Plot.draw` and added a condition to avoid drawing when `view_bounds` reach the values where the bug would be triggered. However, with the introduction of "HiDPI support for the Qt toolkit" in `enable v5.0.1`, the `view_bounds` vary depending on the display and their limiting values can no longer be hardcoded.

I also found a way to replicate the bug. If resizing the example `examples/demo/image_plot_origin_and_orientation.py` to the point where the window is too small, it crashes with the same error:
```
Traceback (most recent call last):
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/qt4/base_window.py", line 243, in paintEvent
    self.handler.paintEvent(event)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/qt4/base_window.py", line 78, in paintEvent
    self._enable_window._paint(event)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/abstract_window.py", line 506, in _paint
    self.component.draw(gc, view_bounds=(0, 0, size[0], size[1]))
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/component.py", line 432, in draw
    self._draw(gc, view_bounds, mode)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/component.py", line 803, in _draw
    self._dispatch_draw(layer, bb, view_bounds, mode)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/container.py", line 280, in _dispatch_draw
    component._dispatch_draw(layer, gc, new_bounds, mode)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/container.py", line 280, in _dispatch_draw
    component._dispatch_draw(layer, gc, new_bounds, mode)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/enable/component.py", line 831, in _dispatch_draw
    handler(gc, view_bounds, mode)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/chaco/base_2d_plot.py", line 204, in _draw_image
    self._render(gc)
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/chaco/image_plot.py", line 118, in _render
    self._compute_cached_image()
  File ".edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/chaco/image_plot.py", line 250, in _compute_cached_image
    col_min, col_max, row_min, row_max = index_bounds
TypeError: 'NoneType' object is not iterable
Abort trap: 6
```
I ran the example in the `chaco` test environment from current master (2d36abda52fbe92c7fdfc4d8c7f99cce42913afc) with the addition of a `scipy v1.4.1` package. Runtime Python 3.6, toolkit `pyqt5`.

Note that the resizing has to happen slowly so that the specific size value which causes the error is triggered.

I believe that `index_bounds` become `None` in this condition:
https://github.com/enthought/chaco/blob/229720698cc6f00060e078b0fb5f215ad3d98113/chaco/image_plot.py#L304-L305
> If resizing the example examples/demo/image_plot_origin_and_orientation.py to the point where the window is too small, it crashes with the same error:

@ievacerny thanks for discovering a way to reproduce the issue! I am able to reproduce the error on my windows machine with pyqt5 + python 3.6.

Ideally, I guess we want to prevent the window from shrinking to such a size in the first place - or failing that, ensure that the window resizes dont crash chaco internals.
> Ideally, I guess we want to prevent the window from shrinking to such a size in the first place

That would be great, but we couldn't find a straight forward way to do that. Any suggestions?
@rahulporuri, I am not convinced that the solution is to prevent shrinking to such a size. From our experience, it was not very small. The solution might be to replace the figure with a small icon that would indicate that the figure is too small to be displayed. I think that it needs to be dealt with inside Chaco. When `_calc_zoom_coord` returns `(None, None)` the Chaco caller function should correctly deal with that results rather than trying to continue the process using `(None, None)`.

Resolving patch:
diff --git a/chaco/image_plot.py b/chaco/image_plot.py
--- a/chaco/image_plot.py
+++ b/chaco/image_plot.py
@@ -302,7 +302,7 @@ def _calc_zoom_coords(self, image_rect):
         """
         ix, iy, image_width, image_height = image_rect
         if 0 in (image_width, image_height) or 0 in self.bounds:
-            return (None, None)
+            return ((0, 0, 0, 0), (0, 0, 0, 0))
 
         array_bounds = self._array_bounds_from_screen_rect(image_rect)
         col_min, col_max, row_min, row_max = array_bounds


Test patch:
diff --git a/chaco/tests/test_image_plot.py b/chaco/tests/test_image_plot.py
--- a/chaco/tests/test_image_plot.py
+++ b/chaco/tests/test_image_plot.py
@@ -7,9 +7,21 @@
 
 import numpy as np
 
+from enable.api import Component, ComponentEditor
+from traits.api import HasTraits, Instance
 from traits.etsconfig.api import ETSConfig
-from chaco.api import (PlotGraphicsContext, GridDataSource, GridMapper,
-                       DataRange2D, ImageData, ImagePlot)
+from traitsui.api import Group, UItem, View
+from traitsui.testing.api import UITester
+from chaco.api import (
+    ArrayPlotData,
+    PlotGraphicsContext,
+    GridDataSource,
+    GridMapper,
+    DataRange2D,
+    ImageData,
+    ImagePlot,
+    Plot
+)
 
 
 # The Quartz backend rescales pixel values, so use a higher threshold.
@@ -152,3 +164,32 @@ def test_vertical_bottom_right(self):
         # that is flipped vertically and horizontally.
         self.verify_result_image(RGB, (IMAGE.T)[::-1, ::-1],
                                  origin='bottom right', orientation='v')
+
+    # regression test for enthought/chaco#528
+    @unittest.skipIf(ETSConfig.toolkit=='null', "Skip on 'null' toolkit")
+    def test_resize_to_zero(self):
+        class TestResize(HasTraits):
+            plot = Instance(Component)
+
+            # at the exact size of (101, 101) for the component editor the
+            # image is 0x0 on the screen and the failure was triggered
+            traits_view = View(
+                Group(
+                    UItem('plot', editor=ComponentEditor(size=(101, 101))),
+                ),
+                resizable=True,
+            )
+
+            def _plot_default(self):
+                pd = ArrayPlotData(image=RGB)
+                plot = Plot(pd)
+                plot.img_plot('image')
+
+                return plot
+        
+        test_obj = TestResize()
+        tester = UITester()
+
+         # should not fail
+        with tester.create_ui(test_obj) as ui:
+            pass