Repository: bokeh/bokeh

Issue:
length_units has no effect for rays
# READ AND FOLLOW THESE INSTRUCTIONS CAREFULLY

*ISSUES THAT DO NOT CONTAIN NECESSARY INFORMATION MAY BE CLOSED, IMMEDIATELY*

The issue tracker is NOT the place for general support. For questions and
technical assistance, come ask the [Bokeh mailing list](https://groups.google.com/a/continuum.io/forum/#!forum/bokeh) or join the chat on [Gitter](https://gitter.im/bokeh/bokeh). For feature requests, please provide a detailed description or proposal of the new capability or behavior.

For defects or deficiencies, please provide ALL OF THE FOLLOWING:

#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
Using Bokeh 0.12.9 in Chrome 59.0 on Fedora 25

#### Description of expected behavior and the observed behavior
The ``length_units`` attribute to ``Figure.ray`` has no effect. All rays are plotted in ``data`` units. Furthermore, the documentation has conflicting messages claiming the length defaults to screen units while length_units defaults to data 
```
length (DistanceSpec) –
The length to extend the ray. Note that this length defaults to screen units.
```
while 
```
length_units (Enum ( SpatialUnits )) – (default: ‘data’)
```

#### Complete, minimal, self-contained example code that reproduces the issue

```python
from bokeh.plotting import figure, curdoc                                                                                                                                    
                                                                                                                                                                                                                                                                                                                                               
fig = figure(width=500, height=500)                                                                                                                                          
fig.ray(x=[0, 0], y=[0, 0], angle=[0, 0.5], length=[100, 100],                                                                                                               
        length_units='screen', color='red')                                                                                                                                  
                                                                                                                                                                             
fig.ray(x=[0, 0], y=[0, 0], angle=[-0.5, -1.5], length=[100, 100],                                                                                                           
        length_units='data', color='blue')                                                                                                                                   
                                                                                                                                                                             
curdoc().add_root(fig)                                                                                                                                                       
```

#### Stack traceback and/or browser JavaScript console output
No errors
#### Screenshots or screencasts of the bug in action
All rays are equal length at all zoom levels
![screenshot from 2017-09-18 17-23-38](https://user-images.githubusercontent.com/4625457/30570436-2cb5884c-9c96-11e7-812e-abbdbb5cda3d.png)



Discussion:

Resolving patch:
diff --git a/bokeh/models/glyphs.py b/bokeh/models/glyphs.py
--- a/bokeh/models/glyphs.py
+++ b/bokeh/models/glyphs.py
@@ -784,7 +784,7 @@ class Ray(XYGlyph):
 
     length = DistanceSpec(help="""
     The length to extend the ray. Note that this ``length`` defaults
-    to screen units.
+    to data units (measured in the x-direction).
     """)
 
     line_props = Include(LineProps, use_prefix=False, help="""


Test patch:
diff --git a/bokehjs/test/models/glyphs/index.coffee b/bokehjs/test/models/glyphs/index.coffee
--- a/bokehjs/test/models/glyphs/index.coffee
+++ b/bokehjs/test/models/glyphs/index.coffee
@@ -1,4 +1,5 @@
 require "./image"
 require "./image_rgba"
 require "./image_url"
+require "./ray"
 require "./rect"
diff --git a/bokehjs/test/models/glyphs/ray.coffee b/bokehjs/test/models/glyphs/ray.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/glyphs/ray.coffee
@@ -0,0 +1,90 @@
+{expect} = require "chai"
+utils = require "../../utils"
+sinon = require "sinon"
+
+{create_glyph_view} = require("./glyph_utils")
+{Ray, RayView} = utils.require("models/glyphs/ray")
+{LinearScale} = utils.require("models/scales/linear_scale")
+{LogScale} = utils.require("models/scales/log_scale")
+{Range1d} = utils.require("models/ranges/range1d")
+
+describe "Ray", ->
+
+  describe "RayView", ->
+
+    afterEach ->
+      utils.unstub_canvas()
+
+    beforeEach ->
+      utils.stub_canvas()
+
+      @glyph = new Ray({
+        x: {field: "x"}
+        y: {field: "y"}
+        length: {value: 10}
+      })
+
+      @set_scales = (glyph_view, type="linear") ->
+        if type == "linear"
+          scale = new LinearScale({
+            source_range: new Range1d({start: 0, end: 100})
+            target_range: new Range1d({start: 0, end: 200})
+          })
+        else if type == "reverse"
+          scale = new LinearScale({
+            source_range: new Range1d({start: 0, end: 100})
+            target_range: new Range1d({start: 200, end: 0})
+          })
+        else if type == "log"
+          scale = new LogScale({
+            source_range: new Range1d({start: 1, end: 1000})
+            target_range: new Range1d({start: 0, end: 200})
+          })
+        glyph_view.renderer.xscale = scale
+        glyph_view.renderer.yscale = scale
+        glyph_view.renderer.plot_view.frame.xscales['default'] = scale
+        glyph_view.renderer.plot_view.frame.yscales['default'] = scale
+
+    it "`_map_data` should correctly map data if length units are 'data'", ->
+      for angle in [0,1,2,3]
+        data = {x: [1], y: [2], length: [10]}
+        glyph_view = create_glyph_view(@glyph, data)
+
+        glyph_view.model.properties.length.units = "data"
+
+        @set_scales(glyph_view)
+        glyph_view.map_data()
+        expect(glyph_view.slength).to.be.deep.equal([20])
+
+    it "`_map_data` should correctly map data if length units are 'screen'", ->
+      for angle in [0,1,2,3]
+        data = {x: [1], y: [2], angle: [angle], length: [10]}
+        glyph_view = create_glyph_view(@glyph, data)
+
+        glyph_view.model.properties.length.units = "screen"
+
+        @set_scales(glyph_view)
+        glyph_view.map_data()
+        expect(glyph_view.slength).to.be.deep.equal([10])
+
+    it "`_map_data` should correctly map data if length units are 'data' and scale is reversed", ->
+      for angle in [0,1,2,3]
+        data = {x: [1], y: [2], length: [10]}
+        glyph_view = create_glyph_view(@glyph, data)
+
+        glyph_view.model.properties.length.units = "data"
+
+        @set_scales(glyph_view, "reverse")
+        glyph_view.map_data()
+        expect(glyph_view.slength).to.be.deep.equal([20])
+
+    it "`_map_data` should correctly map data if length units are 'screen' and scale is reversed", ->
+      for angle in [0,1,2,3]
+        data = {x: [1], y: [2], angle: [angle], length: [10]}
+        glyph_view = create_glyph_view(@glyph, data)
+
+        glyph_view.model.properties.length.units = "screen"
+
+        @set_scales(glyph_view, "reverse")
+        glyph_view.map_data()
+        expect(glyph_view.slength).to.be.deep.equal([10])