Repository: enthought/chaco

Issue:
CursorTool does not work with LogMapper for both axes
Using LogMapper for both axes

```
mag = LinePlot(index = ArrayDataSource(w, sort_order='ascending'),
                        value = ArrayDataSource(H),
                        color = curve_color,
                        name = curve_name,
                        index_mapper = LogMapper(range=view_mag.index_range),
                        value_mapper = LogMapper(range=view_mag.value_range))
```

leads to the error below, when adding a cursor with following lines

```
cursor1 = CursorTool(mag,
                        drag_button="left",
                        show_value_line=False,
                        show_marker=True,
                        line_width=2.0,
                        color='black')
mag.overlays.append(cursor1)
```

The problem is, that LogMapper.map_screen returns a 1-element array, while LinearMapper returns a scalar value, if only one data point has to be mapped.

Following subclass of LogMapper works for me as a workaround

```
class LogMapperCorr(LogMapper):
    """
    Corrects chaco.log_mapper.LogMapper for the usage with cursors. Cursors need
    a 1D-array iterable to unpack the x and y values and the LogMapper returns
    a 2D-array. Therefore LogMapper.map_screen has to return a scalar, if the length 
    of the data array is 1.
    """

    def __init__(self, *args, **kwargs):
        LogMapper.__init__(self, *args, **kwargs)

    def map_screen(self, data_array):
        arr = LogMapper.map_screen(self, data_array)

        if len(arr)==1:
            return arr[0]
        else:
            return arr
```

```
Traceback (most recent call last):
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\qt4\base_window.py", line 198, in paintEvent
    self.handler.paintEvent(event)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\qt4\base_window.py", line 50, in paintEvent
    self._enable_window._paint(event)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\abstract_window.py", line 467, in _paint
    self.component.draw(gc, view_bounds=(0, 0, size[0], size[1]))
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\component.py", line 424, in draw
    self._draw(gc, view_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\component.py", line 776, in _draw
    self._dispatch_draw(layer, gc, view_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\container.py", line 272, in _dispatch_draw
    component._dispatch_draw(layer, gc, new_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\container.py", line 272, in _dispatch_draw
    component._dispatch_draw(layer, gc, new_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\component.py", line 796, in _dispatch_draw
    handler(gc, view_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\enable\component.py", line 874, in _draw_overlay
    overlay.overlay(self, gc, view_bounds, mode)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\chaco\tools\line_inspector.py", line 106, in overlay
    self.draw(gc, view_bounds)
  File "C:\Users\speschda\AppData\Local\Enthought\Canopy\User\lib\site-packages\chaco\tools\cursor_tool.py", line 183, in draw
    sx, sy = plot.map_screen(self.current_position)
ValueError: need more than 1 value to unpack
```



Discussion:
I am still able to reproduce this using the `cursor_tool_demo` by replacing:
https://github.com/enthought/chaco/blob/f5cd0aba7bb296c452e0bad39770f5e9e43adb09/examples/demo/cursor_tool_demo.py#L55-L61

with 
```
line = LinePlot(
    index=ArrayDataSource(index, sort_order='ascending'),
    value=ArrayDataSource(value),
    index_mapper=LogMapper(range=DataRange1D()),
    value_mapper=LogMapper(range=DataRange1D()),
    orientation='h',
    color='red',
    bgcolor='transparent',
    line_width=1.0,
    line_style='solid',
    border_visible=False,
)
```

Resolving patch:
diff --git a/chaco/linear_mapper.py b/chaco/linear_mapper.py
--- a/chaco/linear_mapper.py
+++ b/chaco/linear_mapper.py
@@ -59,6 +59,8 @@ def map_screen(self, data_array):
             else:
                 return array([self.low_pos])
         else:
+            if not isinstance(data_array, ndarray):
+                data_array = array(data_array, ndmin=1)
             return (data_array - self.range.low) * self._scale + self.low_pos
 
     def map_data(self, screen_val):
diff --git a/chaco/tools/cursor_tool.py b/chaco/tools/cursor_tool.py
--- a/chaco/tools/cursor_tool.py
+++ b/chaco/tools/cursor_tool.py
@@ -200,7 +200,7 @@ def draw(self, gc, view_bounds=None):
         if plot is None:
             return
 
-        sx, sy = plot.map_screen(self.current_position)
+        sx, sy = plot.map_screen(self.current_position)[0]
         orientation = plot.orientation
 
         if orientation == "h" and sx is not None:
@@ -221,7 +221,7 @@ def is_draggable(self, x, y):
         plot = self.component
         if plot is not None:
             orientation = plot.orientation
-            sx, sy = plot.map_screen(self.current_position)
+            sx, sy = plot.map_screen(self.current_position)[0]
             if orientation == "h" and numpy.abs(sx - x) <= self.threshold:
                 return True
             elif orientation == "v" and numpy.abs(sy - y) <= self.threshold:


Test patch:
diff --git a/chaco/tools/tests/test_cursor_tool.py b/chaco/tools/tests/test_cursor_tool.py
new file mode 100644
--- /dev/null
+++ b/chaco/tools/tests/test_cursor_tool.py
@@ -0,0 +1,96 @@
+# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
+# All rights reserved.
+#
+# This software is provided without warranty under the terms of the BSD
+# license included in LICENSE.txt and may be redistributed only under
+# the conditions described in the aforementioned license. The license
+# is also available online at http://www.enthought.com/licenses/BSD.txt
+#
+# Thanks for using Enthought open source!
+import unittest
+
+import numpy as np
+
+from enable.api import ComponentEditor
+from enable.testing import EnableTestAssistant
+from traits.api import HasTraits, Instance
+from traits.etsconfig.api import ETSConfig
+from traitsui.api import Item, View
+from traitsui.testing.api import UITester
+
+from chaco.api import ArrayPlotData, Plot
+from chaco.tools.cursor_tool import CursorTool
+
+
+class TestCursorTool(unittest.TestCase, EnableTestAssistant):
+
+    # regression test for enthought/chaco#289
+    @unittest.skipIf(ETSConfig.toolkit == "null", "Skip on 'null' toolkit")
+    def test_use_with_log_mappers(self):
+        class TestCursor(HasTraits):
+            plot = Instance(Plot)
+
+            traits_view = View(
+                Item('plot', editor=ComponentEditor(), show_label=False),
+                width=500,
+                height=500,
+                resizable=True
+            )
+
+            def _plot_default(self):
+                arr = np.logspace(0, 10, num=10)
+                data = ArrayPlotData(x=arr, y=arr)
+                plot = Plot(data)
+                renderer = plot.plot(
+                    ("x", "y"),
+                    index_scale="log",
+                    value_scale="log"
+                )[0]
+
+                cursor = CursorTool(renderer)
+
+                renderer.overlays.append(cursor)
+
+                return plot
+
+        tester = UITester()
+        test_cursor = TestCursor()
+        with tester.create_ui(test_cursor):
+            # should not fail
+            self.mouse_move(
+                test_cursor.plot, 10, 10
+            )
+
+    @unittest.skipIf(ETSConfig.toolkit == "null", "Skip on 'null' toolkit")
+    def test_use_with_linear_mappers(self):
+        class TestCursor(HasTraits):
+            plot = Instance(Plot)
+
+            traits_view = View(
+                Item('plot', editor=ComponentEditor(), show_label=False),
+                width=500,
+                height=500,
+                resizable=True
+            )
+
+            def _plot_default(self):
+                arr = np.logspace(0, 10, num=10)
+                data = ArrayPlotData(x=arr, y=arr)
+                plot = Plot(data)
+                renderer = plot.plot(
+                    ("x", "y"),
+                )[0]
+
+                cursor = CursorTool(renderer)
+
+                renderer.overlays.append(cursor)
+
+                return plot
+
+        tester = UITester()
+        test_cursor = TestCursor()
+        with tester.create_ui(test_cursor):
+            # should not fail
+            self.mouse_move(
+                test_cursor.plot, 10, 10
+            )