Repository: bokeh/bokeh

Issue:
Correctly handle data values <= 0 on a log scale
This is a continuation from issue #5389, partially adressed by PR #5477.  There persists an issue where negative data is not handled correctly.  All data <= 0 should be discarded before generating the plot.

As is, if `values = np.linspace(-0.1, 0.9), a JS error complains that it "could not set initial ranges", probably because `log(n)` for `n<=0` is not defined.


Discussion:
Taking a quick look at this, the problem is definitely because the computed start for the range is NaN. I remember there was code (~~that I took out~~ EDIT: this code is actually downstream in the log_ticker and was not taken out.) which was labeled hotfix, where the minimum data value was set to 1 whenever it was < 0. That fix also lead to weird behavior (flipped ranges) because the set minimum of 1 could be greater than all of the other data values. Especially in the log plotting context, it is impossible to guess at a minimum value. 

The best thing to do would be to set the minimum data value to the the minimum value that is still > 0. But the min data value is saved into the `bounds` for each renderer and the ranges only use that and don't access all of the data values. 

I want to try out a few things to see what would be the best solution. @bryevdv @StevenCHowell do you think it is worth it to first add back the hotfix? I could make that small PR tonight and then figure out a better way to fix it. 

Also, the workaround now would be to supply a range start and end. 
@clairetang6, for the hotfix, setting it to 1 will obviously create other problems (typically my data ranges from 0 to 0.3 ish).  As you say, something more like the following would be more appropriate:
```
x_min = min(x[x > 0])
```

I don't think there is a rush to patch this, just to have to return to it later.    Did you see issue #5550?  Perhaps consider this as well while trying things as it is closely related.

Resolving patch:
diff --git a/sphinx/source/docs/user_guide/examples/plotting_log_scale_axis.py b/sphinx/source/docs/user_guide/examples/plotting_log_scale_axis.py
--- a/sphinx/source/docs/user_guide/examples/plotting_log_scale_axis.py
+++ b/sphinx/source/docs/user_guide/examples/plotting_log_scale_axis.py
@@ -6,8 +6,7 @@
 output_file("log.html")
 
 # create a new plot with a log axis type
-p = figure(plot_width=400, plot_height=400,
-           y_axis_type="log", y_range=(10**-1, 10**4))
+p = figure(plot_width=400, plot_height=400, y_axis_type="log")
 
 p.line(x, y, line_width=2)
 p.circle(x, y, fill_color="white", size=8)


Test patch:
diff --git a/bokehjs/test/core/util/bbox.coffee b/bokehjs/test/core/util/bbox.coffee
--- a/bokehjs/test/core/util/bbox.coffee
+++ b/bokehjs/test/core/util/bbox.coffee
@@ -10,6 +10,16 @@ describe "bbox module", ->
     it "should be an unbounded box", ->
       expect(bbox.empty()).to.deep.equal {minX: Infinity, minY: Infinity, maxX: -Infinity, maxY:-Infinity}
 
+  describe "positive_x", ->
+
+    it "should be box covering the area where x is positive", ->
+      expect(bbox.positive_x()).to.deep.equal {minX: Number.MIN_VALUE, minY: -Infinity, maxX: Infinity, maxY: Infinity}
+
+  describe "positive_y", ->
+
+    it "should be box covering the area where y is positive", ->
+      expect(bbox.positive_y()).to.deep.equal {minX: -Infinity, minY: Number.MIN_VALUE, maxX: Infinity, maxY: Infinity}
+
   describe "union", ->
     empty    = bbox.empty()
     outside  = { minX: 0, maxX: 10, minY:  0, maxY: 10 }
diff --git a/bokehjs/test/models/glyphs/glyph_utils.coffee b/bokehjs/test/models/glyphs/glyph_utils.coffee
--- a/bokehjs/test/models/glyphs/glyph_utils.coffee
+++ b/bokehjs/test/models/glyphs/glyph_utils.coffee
@@ -8,7 +8,7 @@ sinon = require 'sinon'
 {ColumnDataSource} = utils.require('models/sources/column_data_source')
 {PlotCanvasView} = utils.require('models/plots/plot_canvas')
 
-create_glyph_view = (glyph) ->
+create_glyph_view = (glyph, data={}) ->
   ###
   Requires stubbing the canvas and solver before calling
   ###
@@ -21,7 +21,7 @@ create_glyph_view = (glyph) ->
   plot_view = new plot.plot_canvas.default_view({model: plot.plot_canvas })
   sinon.stub(plot_view, 'update_constraints')
 
-  @data_source = new ColumnDataSource()
+  @data_source = new ColumnDataSource({data: data})
 
   glyph_renderer = new GlyphRenderer({
     glyph: glyph
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,3 +1,4 @@
 require "./image"
 require "./image_rgba"
 require "./image_url"
+require "./rect"
diff --git a/bokehjs/test/models/glyphs/rect.coffee b/bokehjs/test/models/glyphs/rect.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/glyphs/rect.coffee
@@ -0,0 +1,80 @@
+{expect} = require "chai"
+utils = require "../../utils"
+sinon = require "sinon"
+
+{create_glyph_view} = require("./glyph_utils")
+{Rect, RectView} = utils.require('models/glyphs/rect')
+
+describe "Glyph (using Rect as a concrete Glyph)", ->
+
+  describe "GlyphView", ->
+
+    afterEach ->
+      utils.unstub_canvas()
+      utils.unstub_solver()
+      @stub.restore()
+
+    beforeEach ->
+      utils.stub_canvas()
+      utils.stub_solver()
+
+      @stub = sinon.stub(RectView.prototype, '_bounds', (bounds) -> bounds )
+
+      @glyph = new Rect({
+        x: {field: "x"}
+        y: {field: "y"}
+      })
+
+    it "should calculate bounds based on data", ->
+      data = {x: [1, 2, 3, 4], y: [-20, 10, 0, 30]}
+      glyph_view = create_glyph_view(@glyph, data)
+      bounds = glyph_view.bounds()
+
+      expect(bounds).to.be.deep.equal({ minX: 1, minY: -20, maxX: 4, maxY: 30 })
+
+    it "should calculate log bounds based on data values > 0", ->
+      data = {x: [1, 2, 3, 4], y: [-20, 0, 10, 30]}
+      glyph_view = create_glyph_view(@glyph, data)
+      log_bounds = glyph_view.log_bounds()
+
+      expect(log_bounds).to.be.deep.equal({ minX: 1, minY: 10, maxX: 4, maxY: 30 })
+
+    it "should calculate log bounds when NaNs are present", ->
+      data = {x: [1, 2, 3, 4], y: [-20, 0, 10, NaN]}
+      glyph_view = create_glyph_view(@glyph, data)
+      log_bounds = glyph_view.log_bounds()
+
+      expect(log_bounds).to.be.deep.equal({ minX: 1, minY: 10, maxX: 3, maxY: 10 })
+
+describe "Rect", ->
+
+  describe "RectView", ->
+
+    afterEach ->
+      utils.unstub_canvas()
+      utils.unstub_solver()
+
+    beforeEach ->
+      utils.stub_canvas()
+      utils.stub_solver()
+
+      @glyph = new Rect({
+        x: {field: "x"}
+        y: {field: "y"}
+        width: {value: 10}
+        height: {value: 20}
+      })
+
+    it "should calculate bounds based on data including width and height", ->
+      data = {x: [0, 1, 2, 3], y: [0, 1, 2, 3]}
+      glyph_view = create_glyph_view(@glyph, data)
+      bounds = glyph_view.bounds()
+
+      expect(bounds).to.be.deep.equal({ minX: -5, minY: -10, maxX: 8, maxY: 13 })
+
+    it "should calculate log bounds based on data including width and height", ->
+      data = {x: [0, 1, 2, 3], y: [0, 1, 2, 3]}
+      glyph_view = create_glyph_view(@glyph, data)
+      log_bounds = glyph_view.log_bounds()
+
+      expect(log_bounds).to.be.deep.equal({ minX: -4, minY: -9, maxX: 8, maxY: 13 })
diff --git a/bokehjs/test/models/ranges/data_range1d.coffee b/bokehjs/test/models/ranges/data_range1d.coffee
--- a/bokehjs/test/models/ranges/data_range1d.coffee
+++ b/bokehjs/test/models/ranges/data_range1d.coffee
@@ -155,9 +155,9 @@ describe "datarange1d module", ->
       r.default_span = 4
       expect(r._compute_range(3, 3)).to.be.deep.equal [1, 5]
 
-    it "should use default_span as powers of 10 when _mapper_type='log'", ->
+    it "should use default_span as powers of 10 when mapper_hint='log'", ->
       r = new DataRange1d()
-      r._mapper_type = "log"
+      r.mapper_hint = "log"
       expect(r._compute_range(100, 100)).to.be.deep.equal [9.988493699365053, 1001.1519555381683]
       r.default_span = 4
       expect(r._compute_range(100, 100)).to.be.deep.equal [0.9988493699365047, 10011.519555381703]
@@ -206,10 +206,10 @@ describe "datarange1d module", ->
       r.range_padding = 0.5
       expect(r._compute_range(1, 3)).to.be.deep.equal [0.5, 3.5]
 
-    it "should apply range_padding logly when _mapper_type='log'", ->
+    it "should apply range_padding logly when mapper_hint='log'", ->
       r = new DataRange1d()
       r.range_padding = 0.5
-      r._mapper_type = "log"
+      r.mapper_hint = "log"
       expect(r._compute_range(0.01, 10)).to.be.deep.equal [0.0017782794100389264, 56.23413251903488]
 
   describe "_compute_min_max", ->
@@ -264,31 +264,31 @@ describe "datarange1d module", ->
       r = new DataRange1d()
       p = new TestObject()
       g = new GlyphRenderer()
-      g.id = 1
+      g.id = "id"
       p.renderers = [g]
       r.plots = [p]
 
       bds = {
-        1: {minX: -10, maxX: -6, minY: 5, maxY: 6}
+        "id": {minX: -10, maxX: -6, minY: 5, maxY: 6}
       }
 
-      r.update(bds, 0, 1)
+      r.update(bds, 0, "id")
       expect(r.start).to.be.equal -10.2
 
     it "should not update its start or end values to NaN when log", ->
       r = new DataRange1d()
-      r._mapper_type = "log"
+      r.mapper_hint = "log"
       p = new TestObject()
       g = new GlyphRenderer()
-      g.id = 1
+      g.id = "id"
       p.renderers = [g]
       r.plots = [p]
 
       bds = {
-        1: {minX: -10, maxX: -6, minY: 5, maxY: 6}
+        "id": {minX: Infinity, maxX: -Infinity, minY: 5, maxY: 6}
       }
 
-      r.update(bds, 0, 1)
+      r.update(bds, 0, "id")
       expect(r.start).not.to.be.NaN
       expect(r.end).not.to.be.NaN