Repository: bokeh/bokeh

Issue:
Whisker does not accept explicit negative values
#### Version info

Windows 10 x64
python 3.6.5
bokeh 1.0.2

#### Description

Annotations such as `Whisker` do not accept explicit negative values:

    from bokeh.models import Whisker
    Whisker(base=0, lower=-1, upper=1)

raises

	Traceback (most recent call last):
	  ...
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\model.py", line 261, in __init__
		super(Model, self).__init__(**kwargs)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\has_props.py", line 254, in __init__
		setattr(self, name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\has_props.py", line 281, in __setattr__
		super(HasProps, self).__setattr__(name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 1090, in __set__
		super(UnitsSpecPropertyDescriptor, self).__set__(obj, value, setter)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 546, in __set__
		self._internal_set(obj, value, setter=setter)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 767, in _internal_set
		value = self.property.prepare_value(obj, self.name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\dataspec.py", line 397, in prepare_value
		raise ValueError("Distances must be positive or None!")
	ValueError: Distances must be positive or None!

However,

    Whisker(base=0, lower=0, upper=1)

and

    from bokeh.models import ColumnDataSource
    source = ColumnDataSource({'base': [0], 'lower': [-1], 'upper': [1]})
    Whisker(base='base', lower='lower', upper='upper', source=source)

work as expected.
Whisker does not accept explicit negative values
#### Version info

Windows 10 x64
python 3.6.5
bokeh 1.0.2

#### Description

Annotations such as `Whisker` do not accept explicit negative values:

    from bokeh.models import Whisker
    Whisker(base=0, lower=-1, upper=1)

raises

	Traceback (most recent call last):
	  ...
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\model.py", line 261, in __init__
		super(Model, self).__init__(**kwargs)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\has_props.py", line 254, in __init__
		setattr(self, name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\has_props.py", line 281, in __setattr__
		super(HasProps, self).__setattr__(name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 1090, in __set__
		super(UnitsSpecPropertyDescriptor, self).__set__(obj, value, setter)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 546, in __set__
		self._internal_set(obj, value, setter=setter)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\descriptors.py", line 767, in _internal_set
		value = self.property.prepare_value(obj, self.name, value)
	  File "C:\Users\peter\Anaconda3\lib\site-packages\bokeh\core\property\dataspec.py", line 397, in prepare_value
		raise ValueError("Distances must be positive or None!")
	ValueError: Distances must be positive or None!

However,

    Whisker(base=0, lower=0, upper=1)

and

    from bokeh.models import ColumnDataSource
    source = ColumnDataSource({'base': [0], 'lower': [-1], 'upper': [1]})
    Whisker(base='base', lower='lower', upper='upper', source=source)

work as expected.


Discussion:
Seems like a Python API issue. It is hopefully just a matter of using the more general `NumberSpec` rather than `DistanceSpec` on the Python side. 

@phausamann If you are interested in trying things out (you might be able to get to it sooner than us), the place to edit is here:

https://github.com/bokeh/bokeh/blob/master/bokeh/models/annotations.py#L1071-L1081

If that works, we'd be happy to accept a PR
Thanks @bryevdv, that seems easy enough for a first issue to tackle, I'll give it a try.
@phausamann did you still want to try to take a look at this?
I'm sorry, I've been very busy the last couple of weeks. I will have some time to look into this in about two weeks if that's a reasonable time frame.
Seems like a Python API issue. It is hopefully just a matter of using the more general `NumberSpec` rather than `DistanceSpec` on the Python side. 

@phausamann If you are interested in trying things out (you might be able to get to it sooner than us), the place to edit is here:

https://github.com/bokeh/bokeh/blob/master/bokeh/models/annotations.py#L1071-L1081

If that works, we'd be happy to accept a PR
Thanks @bryevdv, that seems easy enough for a first issue to tackle, I'll give it a try.
@phausamann did you still want to try to take a look at this?
I'm sorry, I've been very busy the last couple of weeks. I will have some time to look into this in about two weeks if that's a reasonable time frame.

Resolving patch:
diff --git a/bokeh/models/annotations.py b/bokeh/models/annotations.py
--- a/bokeh/models/annotations.py
+++ b/bokeh/models/annotations.py
@@ -30,9 +30,9 @@
                           Orientation, RenderMode, SpatialUnits, VerticalAlign, TextAlign,
                           TooltipAttachment)
 from ..core.has_props import abstract
-from ..core.properties import (Angle, AngleSpec, Auto, Bool, ColorSpec, Datetime, Dict, DistanceSpec, Either,
+from ..core.properties import (Angle, AngleSpec, Auto, Bool, ColorSpec, Datetime, Dict, Either,
                                Enum, Float, FontSizeSpec, Include, Instance, Int, List, NumberSpec, Override,
-                               Seq, String, StringSpec, Tuple, value)
+                               Seq, String, StringSpec, Tuple, UnitsSpec, value)
 from ..core.property_mixins import FillProps, LineProps, TextProps
 from ..core.validation import error
 from ..core.validation.errors import BAD_COLUMN_NAME, NON_MATCHING_DATA_SOURCES_ON_LEGEND_ITEM_RENDERERS
@@ -579,16 +579,15 @@ class Band(Annotation):
     ''' Render a filled area band along a dimension.
 
     '''
-
-    lower = DistanceSpec(help="""
+    lower = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The coordinates of the lower portion of the filled area band.
     """)
 
-    upper = DistanceSpec(help="""
+    upper = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The coordinates of the upper portion of the filled area band.
     """)
 
-    base = DistanceSpec(help="""
+    base = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The orthogonal coordinates of the upper and lower values.
     """)
 
@@ -1063,7 +1062,7 @@ class Whisker(Annotation):
 
     '''
 
-    lower = DistanceSpec(help="""
+    lower = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The coordinates of the lower end of the whiskers.
     """)
 
@@ -1071,7 +1070,7 @@ class Whisker(Annotation):
     Instance of ``ArrowHead``.
     """)
 
-    upper = DistanceSpec(help="""
+    upper = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The coordinates of the upper end of the whiskers.
     """)
 
@@ -1079,7 +1078,7 @@ class Whisker(Annotation):
     Instance of ``ArrowHead``.
     """)
 
-    base = DistanceSpec(help="""
+    base = UnitsSpec(default=None, units_type=Enum(SpatialUnits), units_default="data", help="""
     The orthogonal coordinates of the upper and lower values.
     """)
 


Test patch:
diff --git a/bokeh/models/tests/test_annotations.py b/bokeh/models/tests/test_annotations.py
--- a/bokeh/models/tests/test_annotations.py
+++ b/bokeh/models/tests/test_annotations.py
@@ -430,6 +430,15 @@ def test_Whisker():
         "y_range_name"],
         LINE)
 
+def test_Whisker_and_Band_accept_negative_values():
+    whisker = Whisker(base=-1., lower=-1.5, upper=-0.5)
+    assert whisker.base == -1.
+    assert whisker.lower == -1.5
+    assert whisker.upper == -0.5
+    band = Band(base=-1., lower=-1.5, upper=-0.5)
+    assert band.base == -1.
+    assert band.lower == -1.5
+    assert band.upper == -0.5
 
 def test_can_add_multiple_glyph_renderers_to_legend_item():
     legend_item = LegendItem()