Repository: bokeh/bokeh

Issue:
NA values correctly skipped in linear scale but not in log scale
Hi,

I have an issue with plotting "NA" values (as imported with a CSV reader on R) as lines when using a log scale. Here is the code example in a Jupyter notebook:
```
from bokeh.plotting import figure, show, output_notebook
import numpy as np

# prepare some data
x = [1, 2, 3, 4, 5, 6, 7, 8]
y = [0.01, 0.03, 0.1, 'NA', 1, 3, 10, 30]
#chartData = {"x": x, "y": y}

# output to Jupyter notebook
output_notebook()

# create a plot
p = figure(plot_width=600, plot_height=400,
          )

p.line(x, y, line_width=2)
# show the results
show(p)
```
![bokeh_plot_na_linear](https://cloud.githubusercontent.com/assets/1568591/24194686/ebeb77f2-0eee-11e7-85ca-66777bc82a73.png)

The 'NA' value is correctly skipped and shows as a break in the line.

Now if I plot the same data but using a log scale for the Y axis:

```
# create a plot
p2 = figure(plot_width=300, plot_height=250,
y_axis_type="log",
          )

p2.line(x, y, line_width=2)
# show the results
show(p2)
```
![bokeh_plot_na_log](https://cloud.githubusercontent.com/assets/1568591/24194696/f17de862-0eee-11e7-969c-6b96f8a194da.png)

As you can see, the 'NA' value is arbitrarily plotted at the level of the X axis (whatever the Y axis range I can specify). It also happens when using numpy.nan instead of 'NA'.

Am I doing anything wrong? Is this a bug, and is there a workaround, other than removing the 'NA' data (which is a pain for large matrices and multiple line plots, where the 'NA' are not always consistently sampled in series)? Thanks in advance.


Discussion:
Seems like a bug. I'm not aware of any workarounds offhand except either to segment the data by hand or possibly pre-transform the data to a log scale yourself 
Thanks bryevdv. I wish I could help but am unfamiliar with Bokeh source code. Any pointers of where to look?
@zebulon2 sorry I was maybe unclear, those suggestions were for actions you could take as a user (in your code) to possibly work around the bug until it is fixed.

Though if you are interested in possibly working on a fix in Bokeh itself, and asking about where to get started, I'm happy to advise and answer questions to help you get started (just let me know)
I really meant I would like to help fix this issue in Bokeh code, but am unsure I have the skills for it. If you can advise where to look it would be great. There must be some code that is responsible for the gaps in the line (when plotting with a linear scale), which is not used (or working properly) when the axis is switched to log scale.

I also tried plotting with a 0 in the y data series. I get the same issue in log scale, suggesting there may be a simple fix for 0/NA/None value in the code, but it is not appropriate. I am more than happy to look but cannot do any promise :)
I think the problem is here:

https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L41-L42

For some reason the log mapper is explicitly mapping non-finite values to 0. So a quick fix would be to change that behavior. But testing will require building and running a local BokehJS. There's some more info at 

http://bokeh.pydata.org/en/dev/docs/dev_guide.html

about getting set up and building locally. I'd say take a look at that and let me know what other questions I can answer. 
Thanks a lot, I will see what I can do. Just two comments:

1. What I am curious about is why the gap works fine in linear scale: is there some code that leads to skipping the data before plotting? And therefore, is it a problem with the order in applying the data reading and the log mapper? If the log mapper is replacing NaN with 0s, and then the data reading is done, then it would not skip that point. Would it be possible to reverse the processes here?

1. I just noticed the problem also occurs with negative values. If I use:
```
y = [0.01, 0.03, -1, 0.3, 1, 3, 10, 30]
```
then the plot looks like this:
![bokeh_plot](https://cloud.githubusercontent.com/assets/1568591/24203618/2a60bdd4-0f0e-11e7-8647-0636a81dbb5d.png)
Is a negative value caught by the isFinite test?
I was browing past issues and found this: https://github.com/bokeh/bokeh/issues/2162
Apparently this is the same issue, fixed for points but not for lines. The screenshots in the last post look to have the same issue than the one I reported. In this thread and the code they mention two hotfixes for it: https://github.com/bokeh/bokeh/pull/5477 and then another one that fixes the 5477 fix: https://github.com/bokeh/bokeh/pull/5561. Could these be the ones to fix?

https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L39-L42

The line above calls `Math.log` to produce `value` so any non-positive starting value will be non-finite. That would explain all the behavior described in this issue. 

Regarding linear vs log, the linear mapper (presumably) preserves non-finite values when mapping from "data space" to "screen space". Then when actually rendering, the line `_render` (which uses already mapped data) interprets non-finite screen coordinates as a gap:

https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/glyphs/line.coffee#L6-L25
OK. I am not a coffee developer, but can understand the intent of the code here. The gap i caused by the
``drawing = false`` statement if I understand well. Am I correct? So why in log scale the ``if !isFinite(sx[i]+sy[i]) and drawing`` statement on line https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/glyphs/line.coffee#L11 not true? This "trick" of not drawing would be acceptable as well in log scale, no?
Those issues were different. One was about axis rendering and the other was about a but that propagated non-finite values in a buggy and unintended way (causing finite values to mis-render)
OK, sorry for interspersing the issues. So the question is why the https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/glyphs/line.coffee#L6-L25 tests responsible for stopping drawing behave differently for linear and log scales? If we can answer this question, we may have a fix.
One thing I try to understand: why is it necessary to replace a NaN or infinite value with 0 aith
if https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L39-L42? Why not returning the NaN that would be caught by https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/glyphs/line.coffee#L6-L25? Am I right to think this is the problem? The log mapper replaces NaN with 0, and then the line code is applied, but since it finds a 0, it draws it?
What about this fix in place of https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L38-L49:
```
      for i in [0...xs.length]
        value = (Math.log(xs[i]) - inter_offset) / inter_scale

        if not isNaN(value) and isFinite(value)
          result[i] = value*scale + offset
        else
          result[i] = value
return result
```

Here we keep a NaN/infinite value untouched, otherwise if value is positive the scale/offset is applied. I suppose the replacement with a 0 was used for the subsequent scaling loop in https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L46-47.
> The gap i caused by the drawing = false statement if I understand well. Am I correct? 

Exactly, that is correct

> The log mapper replaces NaN with 0, and then the line code is applied, but since it finds a 0, it draws it?

Yes I believe that's the crux of the problem.

Your above solution might work, it might also work to just not do the test for non-finite at all in the original code (just always use `value` as-is. If you can build locally (and run examples with the env var
`BOKEH_RESOURCES=inline` set) then you can see if either or both solves the immediate problem. 

Then the question would be to make sure there is not something else depending on the weird "turn nan's to zero" behavior

> Your above solution might work, it might also work to just not do the test for non-finite at all in the original code (just always use value as-is.)

I initially thought that it needed to replace NaN with 0 not to fail the transform ``value*scale + offset``. But maybe that calculation works fine with NaNs?

> Then the question would be to make sure there is not something else depending on the weird "turn nan's to zero" behavior

Sure, but anyway this should be corrected. One cannot replace NaN with an arbitrary value, this is recipe for further trouble.
> I initially thought that it needed to replace NaN with 0 not to fail the transform value*scale + offset. But maybe that calculation works fine with NaNs?

Well, `NaN` values should be propagated, per IEEE 754, so if `value` is `NaN`, I'd expect `value*scale + offset` to also be `NaN`. 

> Sure, but anyway this should be corrected. One cannot replace NaN with an arbitrary value, this is recipe for further trouble.

I definitely agree. 
Hi, I have tested my patch and it works.

![bokeh_plot](https://cloud.githubusercontent.com/assets/1568591/24258610/38bbbf36-0fe6-11e7-9299-440fc23a9961.png)

I have also commented out https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/mappers/log_mapper.coffee#L24-L25 which look to have the same purpose but for single values instead of vectors.

Note that the crucial part of test is not isNaN, but isFinite: when using 'NA' as a value, it tests positive only to the not isFinite test. Probably it divides by 0.

I tried the BokehJS tests and got 4 failures linked to the changes. It expects -Infinity to be equal to 10 or 0 depending on the test, and Infinity to be equal to 100. This is with ColorBar._tick_coordinates method and Range1d ranges vector map. Now I was wondering: is the test correct? Or does it pass under the assumption that Nan and Infinites are equal to some values?

@zebulon2 Awesome! I'm not sure offhand about the tests, I think the next best step would now be to make a PR with the proposed changes. It will be easier for me and others to see the test failures on our automated CI machinery. Then we can discuss whether the tests or something else needs to change in the PR itself. 
Sure. I have done a fork already and will submit a PR soon.

Resolving patch:
diff --git a/bokeh/models/annotations.py b/bokeh/models/annotations.py
--- a/bokeh/models/annotations.py
+++ b/bokeh/models/annotations.py
@@ -311,7 +311,10 @@ class ColorBar(Annotation):
 
     .. warning::
         If the `low` and `high` attributes of the ColorMapper aren't set, ticks
-        and tick labels won't be rendered.
+        and tick labels won't be rendered. Additionally, if a LogTicker is
+        passed to the `ticker` argument and either or both of the logarithms
+        of `low` and `high` values of the color_mapper are non-numeric
+        (i.e. `low=0`), the tick and tick labels won't be rendered.
     """)
 
     margin = Int(30, help="""


Test patch:
diff --git a/bokehjs/test/models/annotations/color_bar.coffee b/bokehjs/test/models/annotations/color_bar.coffee
--- a/bokehjs/test/models/annotations/color_bar.coffee
+++ b/bokehjs/test/models/annotations/color_bar.coffee
@@ -213,26 +213,35 @@ describe "ColorBar module", ->
         expect(tick_coords.major_labels).to.be.deep.equal([10, 12, 14, 16, 18, 20])
 
       it "Should correctly determine tick coords and labels for LogColorMapper if orientation='vertical'", ->
-        @color_bar.color_mapper = new LogColorMapper({low: 0, high: 1000, palette: Viridis.Viridis10})
+        @color_bar.color_mapper = new LogColorMapper({low: 1, high: 1000, palette: Viridis.Viridis10})
         @color_bar.height = 100
         @color_bar.orientation = 'vertical'
 
         tick_coords = @color_bar._tick_coordinates()
 
-        expect(tick_coords.major[0]).to.be.deep.equal([0, 0, 0, 0, 0, 0])
-        expect(tick_coords.major[1]).to.be.deep.equal(new Float64Array([100, 23.299000144533963, 13.264666955734583, 7.394958320545214, 3.2303337669352175, 0]))
-        expect(tick_coords.major_labels).to.be.deep.equal([0, 200, 400, 600, 800, 1000])
+        expect(tick_coords.major[0]).to.be.deep.equal([0, 0, 0, 0, 0])
+        expect(tick_coords.major[1]).to.be.deep.equal(new Float64Array([23.299000144533963, 13.264666955734583, 7.394958320545214, 3.2303337669352175, 0]))
+        expect(tick_coords.major_labels).to.be.deep.equal([200, 400, 600, 800, 1000])
 
-      it "Should correctly determine tick coords and labels for LogColorMapper if orientation='vertical'", ->
-        @color_bar.color_mapper = new LogColorMapper({low: 0, high: 1000, palette: Viridis.Viridis10})
+      it "Should correctly determine tick coords and labels for LogColorMapper if orientation='horizontal'", ->
+        @color_bar.color_mapper = new LogColorMapper({low: 1, high: 1000, palette: Viridis.Viridis10})
         @color_bar.width = 100
         @color_bar.orientation = 'horizontal'
 
         tick_coords = @color_bar._tick_coordinates()
 
-        expect(tick_coords.major[1]).to.be.deep.equal([0, 0, 0, 0, 0, 0])
-        expect(tick_coords.major[0]).to.be.deep.equal(new Float64Array([0, 76.70099985546604, 86.73533304426542, 92.60504167945479, 96.76966623306478, 100]))
-        expect(tick_coords.major_labels).to.be.deep.equal([0, 200, 400, 600, 800, 1000])
+        expect(tick_coords.major[1]).to.be.deep.equal([0, 0, 0, 0, 0])
+        expect(tick_coords.major[0]).to.be.deep.equal(new Float64Array([76.70099985546604, 86.73533304426542, 92.60504167945479, 96.76966623306478, 100]))
+        expect(tick_coords.major_labels).to.be.deep.equal([200, 400, 600, 800, 1000])
+
+      it "Should correctly return empty tick coords and labels for LogColorMapper if log(high)/log(low) are non-numeric", ->
+        @color_bar.color_mapper = new LogColorMapper({low: -1, high: 0, palette: Viridis.Viridis10})
+        @color_bar.ticker = new LogTicker()
+        tick_coords = @color_bar._tick_coordinates()
+
+        expect(tick_coords.major[0]).to.be.deep.equal([])
+        expect(tick_coords.major[1]).to.be.deep.equal(new Float64Array([]))
+        expect(tick_coords.major_labels).to.be.deep.equal([])
 
   describe "ColorBarView", ->
 
@@ -269,6 +278,12 @@ describe "ColorBar module", ->
       # Note: ctx.measureText is stubbed to return {'width': 1, 'ascent': 1} in test/utils
       expect(@color_bar_view._get_label_extent()).to.be.equal(6)
 
+    it "ColorBarView._get_label_extent method (orientation='vertical') and no major_labels", ->
+      # Handle case where mapper start/end causes no ticks to exist (usually for a logticker)
+      stub = sinon.stub(@color_bar_view.model, "_tick_coordinates").returns({"major_labels": []})
+      expect(@color_bar_view._get_label_extent()).to.be.equal(0)
+      stub.restore()
+
     it "ColorBarView._get_label_extent method (orientation='horizontal')", ->
       @color_bar_view.model.orientation = "horizontal"
       expect(@color_bar_view._get_label_extent()).to.be.equal(20)
diff --git a/bokehjs/test/models/glyphs/rect.coffee b/bokehjs/test/models/glyphs/rect.coffee
--- a/bokehjs/test/models/glyphs/rect.coffee
+++ b/bokehjs/test/models/glyphs/rect.coffee
@@ -86,7 +86,7 @@ describe "Rect", ->
           })
         else if type == "log"
           mapper = new LogMapper({
-            source_range: new Range1d({start: 0, end: 100})
+            source_range: new Range1d({start: 1, end: 1000})
             target_range: new Range1d({start: 0, end: 200})
           })
         else
@@ -297,14 +297,14 @@ describe "Rect", ->
           expect(result3['1d'].indices).to.be.deep.equal([1])
 
         it "should work when axis is log", ->
-          data = {x: [60, 100, 140], y: [60, 100, 140]}
+          data = {x: [1, 10, 100, 1000], y: [1, 10, 100, 1000]}
           glyph_view = create_glyph_view(@glyph, data)
 
           @set_mappers(glyph_view, type="log")
           glyph_view.map_data()
 
-          result4 = glyph_view._hit_point(@geometry4)
-          result5 = glyph_view._hit_point(@geometry5)
+          result4 = glyph_view._hit_point({ vx: 66.666, vy: 66.666 })
+          result5 = glyph_view._hit_point({ vx: 133.333, vy: 133.333 })
 
-          expect(result4['1d'].indices).to.be.deep.equal([1])
-          expect(result5['1d'].indices).to.be.deep.equal([1])
+          expect(result4['1d'].indices).to.be.deep.equal([])
+          expect(result5['1d'].indices).to.be.deep.equal([])
diff --git a/bokehjs/test/models/mappers/log_mapper.coffee b/bokehjs/test/models/mappers/log_mapper.coffee
--- a/bokehjs/test/models/mappers/log_mapper.coffee
+++ b/bokehjs/test/models/mappers/log_mapper.coffee
@@ -4,83 +4,92 @@ utils = require "../../utils"
 {LogMapper} = utils.require("models/mappers/log_mapper")
 {Range1d} = utils.require("models/ranges/range1d")
 
-describe "log_mapper module", ->
-  source = {start: 0, end: 10000}
-  target = {start: 10, end: 110}
-
-  generate_mapper = ->
-    new LogMapper({
-      source_range: new Range1d(source)
-      target_range: new Range1d(target)
+describe "LogMapper module", ->
+
+  beforeEach ->
+    @mapper = new LogMapper({
+      source_range: new Range1d({start: 1, end: 10000})
+      target_range: new Range1d({start: 10, end: 110})
     })
 
-  describe "creation with Range1d ranges", ->
-    mapper = generate_mapper()
+  describe "_mapper_state method", ->
+
+    it "should correctly compute the mapper state", ->
+      mapper_state = @mapper._mapper_state()
+      expect(mapper_state).to.be.deep.equal [ 100, 10, 9.210340371976184, 0 ]
 
-    it "should compute mapper state", ->
-      expect(mapper.mapper_state).to.be.deep.equal [ 100, 10, 9.210340371976184, 0 ]
+  describe "map_to_target method", ->
 
-    it "should map values <= start to start", ->
-      expect(mapper.map_to_target(0)).to.be.equal 10
-      expect(mapper.map_to_target(-1)).to.be.equal 10
+    it "should map NaN values to NaN", ->
+      expect(@mapper.map_to_target(NaN)).to.be.NaN
+
+    it "should map infinity values to NaN", ->
+      @mapper.source_range.start = 0
+      expect(@mapper.map_to_target(0)).to.be.NaN
 
     it "should map values > start logly", ->
-      expect(mapper.map_to_target(10)).to.be.equal 35
-      expect(mapper.map_to_target(100)).to.be.equal 60
-      expect(mapper.map_to_target(1000)).to.be.equal 84.99999999999999
-      expect(mapper.map_to_target(10000)).to.be.equal 110
-      expect(mapper.map_to_target(100000)).to.be.equal 135
+      expect(@mapper.map_to_target(1)).to.be.equal 10
+      expect(@mapper.map_to_target(10)).to.be.equal 35
+      expect(@mapper.map_to_target(100)).to.be.equal 60
+      expect(@mapper.map_to_target(10000)).to.be.equal 110
+
+  describe "v_map_to_target method", ->
+
+    it "should vector map NaN values to NaN", ->
+      expect(@mapper.v_map_to_target([NaN])).to.be.deep.equal(new Float64Array([NaN]))
+
+    it "should vector map infinity values to NaN", ->
+      @mapper.source_range.start = 0
+      expect(@mapper.v_map_to_target([0])).to.be.deep.equal(new Float64Array([NaN]))
 
     it "should vector map values logly", ->
-      expect(mapper.v_map_to_target([0,10,100,10000])).to.be.deep.equal new Float64Array [10, 35, 60, 110]
+      expect(@mapper.v_map_to_target([1,10,100,10000])).to.be.deep.equal new Float64Array [10, 35, 60, 110]
 
     it "should map to a Float64Array", ->
-      expect(mapper.v_map_to_target([-1,0,5,10,11])).to.be.instanceof Float64Array
+      expect(@mapper.v_map_to_target([-1,0,5,10,11])).to.be.instanceof Float64Array
+
+  describe "map_from_target method", ->
 
     it "should inverse map values logly", ->
-      expect(mapper.map_from_target(-15)).to.be.equal 0.09999999999999996
-      expect(mapper.map_from_target(10)).to.be.equal 1
-      expect(mapper.map_from_target(35)).to.be.equal 10.000000000000004
-      expect(mapper.map_from_target(60)).to.be.equal 100.00000000000007
-      expect(mapper.map_from_target(85)).to.be.equal 1000.0000000000014
-      expect(mapper.map_from_target(110)).to.be.equal 10000.000000000007
+      expect(@mapper.map_from_target(-15)).to.be.equal 0.09999999999999996
+      expect(@mapper.map_from_target(10)).to.be.equal 1
+      expect(@mapper.map_from_target(35)).to.be.equal 10.000000000000004
+      expect(@mapper.map_from_target(60)).to.be.equal 100.00000000000007
+      expect(@mapper.map_from_target(85)).to.be.equal 1000.0000000000014
+      expect(@mapper.map_from_target(110)).to.be.equal 10000.000000000007
+
+  describe "v_map_from_target method", ->
 
     it "should vector map inverse map values logly", ->
-      expect(mapper.v_map_from_target([-15, 10, 35, 60, 85, 110])).to.be.deep.equal new Float64Array [0.09999999999999996, 1, 10.000000000000004, 100.00000000000007, 1000.0000000000014, 10000.000000000007]
+      expect(@mapper.v_map_from_target([-15, 10, 35, 60, 85, 110])).to.be.deep.equal new Float64Array [0.09999999999999996, 1, 10.000000000000004, 100.00000000000007, 1000.0000000000014, 10000.000000000007]
 
     it "should inverse map to a Float64Array", ->
-      expect(mapper.v_map_from_target([-1,0,5,10,11])).to.be.instanceof Float64Array
+      expect(@mapper.v_map_from_target([-1,0,5,10,11])).to.be.instanceof Float64Array
 
     describe "update source range1d", ->
 
       it "should update on whole range replacement", ->
-        mapper = generate_mapper()
-        mapper.source_range = new Range1d({start: -10, end: 20})
-        expect(mapper.mapper_state).to.be.deep.equal [ 100, 10, 2.995732273553991, 0 ]
+        @mapper.source_range = new Range1d({start: -10, end: 20})
+        expect(@mapper.mapper_state).to.be.deep.equal [ 100, 10, 2.995732273553991, 0 ]
 
       it "should update on range start update", ->
-        mapper = generate_mapper()
-        mapper.source_range.start = -10
-        expect(mapper.mapper_state).to.be.deep.equal [ 100, 10, 9.210340371976184, 0 ]
+        @mapper.source_range.start = -10
+        expect(@mapper.mapper_state).to.be.deep.equal [ 100, 10, 9.210340371976184, 0 ]
 
       it "should update on range end update", ->
-        mapper = generate_mapper()
-        mapper.source_range.end = 20
-        expect(mapper.mapper_state).to.be.deep.equal [ 100, 10, 2.995732273553991, 0 ]
+        @mapper.source_range.end = 20
+        expect(@mapper.mapper_state).to.be.deep.equal [ 100, 10, 2.995732273553991, 0 ]
 
     describe "update target range1d", ->
 
       it "should update on whole range replacement", ->
-        mapper = generate_mapper()
-        mapper.target_range = new Range1d({start: 0, end: 100})
-        expect(mapper.mapper_state).to.be.deep.equal [ 100, 0, 9.210340371976184, 0 ]
+        @mapper.target_range = new Range1d({start: 0, end: 100})
+        expect(@mapper.mapper_state).to.be.deep.equal [ 100, 0, 9.210340371976184, 0 ]
 
       it "should update on range start update", ->
-        mapper = generate_mapper()
-        mapper.target_range.start = 0
-        expect(mapper.mapper_state).to.be.deep.equal [ 110, 0, 9.210340371976184, 0 ]
+        @mapper.target_range.start = 0
+        expect(@mapper.mapper_state).to.be.deep.equal [ 110, 0, 9.210340371976184, 0 ]
 
       it "should update on range end update", ->
-        mapper = generate_mapper()
-        mapper.target_range.end = 100
-        expect(mapper.mapper_state).to.be.deep.equal [ 90, 10, 9.210340371976184, 0 ]
+        @mapper.target_range.end = 100
+        expect(@mapper.mapper_state).to.be.deep.equal [ 90, 10, 9.210340371976184, 0 ]
diff --git a/bokehjs/test/models/tickers/log_ticker.coffee b/bokehjs/test/models/tickers/log_ticker.coffee
--- a/bokehjs/test/models/tickers/log_ticker.coffee
+++ b/bokehjs/test/models/tickers/log_ticker.coffee
@@ -7,21 +7,35 @@ describe "LogTicker Model", ->
 
   describe "LogTicker get_ticks_no_defaults method", ->
 
+    # not finite range
+
+    it "should return empty ticks when start/end is not finite", ->
+      ticker = new LogTicker()
+      ticks = ticker.get_ticks_no_defaults(NaN, NaN, null, 3)
+      expect(ticks.major).to.be.deep.equal([ ])
+      expect(ticks.minor).to.be.deep.equal([ ])
+
+    it "should return empty ticks when log(start) or log(end) not finite", ->
+      ticker = new LogTicker()
+      ticks = ticker.get_ticks_no_defaults(0, 100, null, 3)
+      expect(ticks.major).to.be.deep.equal([ ])
+      expect(ticks.minor).to.be.deep.equal([ ])
+
     # short range (<2 base orders)
 
-    it "should have three major ticks and zero minor ticks for long range", ->
+    it "should have three major ticks and zero minor ticks for short range", ->
       ticker = new LogTicker({desired_num_ticks: 3, num_minor_ticks: 0})
       ticks = ticker.get_ticks_no_defaults(1, 999, null, 3)
       expect(ticks.major).to.be.deep.equal([ 1, 10, 100 ])
       expect(ticks.minor).to.be.deep.equal([ ])
 
-    it "should have three major ticks and five minor ticks for long range", ->
+    it "should have three major ticks and five minor ticks for short range", ->
       ticker = new LogTicker({desired_num_ticks: 3, num_minor_ticks: 1})
       ticks = ticker.get_ticks_no_defaults(1, 999, null, 3)
       expect(ticks.major).to.be.deep.equal([ 1, 10, 100 ])
       expect(ticks.minor).to.be.deep.equal([ 0.1, 1, 10, 100, 1000 ])
 
-    it "should do stuff on short scale", ->
+    it "should have three major ticks and nine minor ticks for short range", ->
       ticker = new LogTicker({desired_num_ticks: 3, num_minor_ticks: 2})
       ticks = ticker.get_ticks_no_defaults(1, 999, null, 3)
       expect(ticks.major).to.be.deep.equal([ 1, 10, 100 ])