Repository: bokeh/bokeh

Issue:
Fixed tick locations do not affect grid line locations
Using current master it is possible to specify fixed tick locations using the FixedTicker, however I would have expected that these fixed tick locations also determine the locations of the gridlines. Instead the tick locations and the grid lines are completely independent in this configuration. I do not think this behavior is desirable so it would be nice that at least by default the grid lines respect the fixed tick locations.

To illustrate the issue, in the plot below I would have expected the x-axis gridlines to align with the four ticks at 0.25, 0.75, 1.25 and 1.75:

![bokeh_plot](https://user-images.githubusercontent.com/1550771/51051602-32204c80-15cc-11e9-989f-7085c8e36294.png)

This problem was noted in the original issue that led to the introduction of the FixedTicker: https://github.com/bokeh/bokeh/issues/2548



Discussion:
Well this is because `Grid` has its own individually configured `ticker`. Arguably it might be preferable for `Grid` to be configured with an `Axis` so that it could delegate to the Axis's ticker. But then how do you allow for the case where someone *doesn't* want them synchronized? And how could this be changed in a compatible way? It seems like it might need to be a 2.0 kind of thing.

I think the only option in the short term might some convenience method like `set_ticker_for_grid_and_axis(...)` to explicitly coordinate things, or else maybe something like  `grid.sync_ticker_to_axis()`
Personally I'd be fine deferring this to 2.0 and then defaulting to mirroring the Axis ticker, but allowing a separate ticker. For our usage I'm happy to syncing the tickers manually for the time being (I didn't actually know about the separate Grid ticker).
I'm running into this same issue - how can I align the Gridlines with the Tickers manually?
@carlosrobelli This worked for me:

```python
plot.xgrid[0].ticker = plot.xaxis[0].ticker
plot.ygrid[0].ticker = plot.yaxis[0].ticker
```
As of Bokeh 1.4 this slightly simpler version should also work:

```
plot.xgrid.ticker = plot.xaxis.ticker
plot.ygrid.ticker = plot.yaxis.ticker
```
@philippjfr do you have a proposal for how this could be improved for 2.0? 

I supposed `Grid` could be configured with either a `.axis` property, in which case it delegates to the axis' ticker, or also with an explicit `.ticker` that would override the axis. We would change `bokeh.plotting` to configure `.axis` but if anyone is already setting `.ticker` then their code would continue working as expected. That would also satisfy the use case of wanting a different ticker from the axis.

Resolving patch:
diff --git a/bokeh/models/axes.py b/bokeh/models/axes.py
--- a/bokeh/models/axes.py
+++ b/bokeh/models/axes.py
@@ -308,7 +308,7 @@ def __init__(self, dimension='lat', **kw):
         super().__init__(**kw)
 
         # Just being careful. It would be defeat the purpose for anyone to actually
-        # configure this axis with differnet kinds of tickers or formatters.
+        # configure this axis with different kinds of tickers or formatters.
         if isinstance(self.ticker, MercatorTicker):
             self.ticker.dimension = dimension
         if isinstance(self.formatter, MercatorTickFormatter):
diff --git a/bokeh/models/grids.py b/bokeh/models/grids.py
--- a/bokeh/models/grids.py
+++ b/bokeh/models/grids.py
@@ -26,6 +26,7 @@
 from ..core.properties import Auto, Either, Float, Include, Instance, Int, Override, Seq, String, Tuple
 from ..core.property_mixins import ScalarFillProps, ScalarHatchProps, ScalarLineProps
 
+from .axes import Axis
 from .renderers import GuideRenderer
 from .tickers import FixedTicker, Ticker
 
@@ -63,19 +64,24 @@ class Grid(GuideRenderer):
     # a path, ranges in both dimensions will matter.
 
     x_range_name = String('default', help="""
-    A particular (named) x-range to use for computing screen
-    locations when rendering a grid on the plot. If unset, use the
-    default x-range.
+    A particular (named) x-range to use for computing screen locations when
+    rendering a grid on the plot. If unset, use the default x-range.
     """)
 
     y_range_name = String('default', help="""
-    A particular (named) y-range to use for computing screen
-    locations when rendering a grid on the plot. If unset, use the
-    default y-range.
+    A particular (named) y-range to use for computing screen locations when
+    rendering a grid on the plot. If unset, use the default y-range.
+    """)
+
+    axis = Instance(Axis, help="""
+    An Axis to delegate ticking to. If the ticker property is None, then the
+    Grid will use the ticker on the specified axis for computing where to draw
+    grid lines. Otherwise, it ticker is not None, it will take precedence over
+    any Axis.
     """)
 
     ticker = Instance(Ticker, help="""
-    The Ticker to use for computing locations for the Grid lines.
+    A Ticker to use for computing locations for the Grid lines.
     """).accepts(Seq(Float), lambda ticks: FixedTicker(ticks=ticks))
 
     grid_props = Include(ScalarLineProps, help="""
diff --git a/bokeh/plotting/helpers.py b/bokeh/plotting/helpers.py
--- a/bokeh/plotting/helpers.py
+++ b/bokeh/plotting/helpers.py
@@ -630,7 +630,7 @@ def _process_axis_and_grid(plot, axis_type, axis_location, minor_ticks, axis_lab
         if axis_label:
             axis.axis_label = axis_label
 
-        grid = Grid(dimension=dim, ticker=axis.ticker)
+        grid = Grid(dimension=dim, axis=axis)
         plot.add_layout(grid, "center")
 
         if axis_location is not None:


Test patch:
diff --git a/bokehjs/test/models/grids/grid.ts b/bokehjs/test/models/grids/grid.ts
--- a/bokehjs/test/models/grids/grid.ts
+++ b/bokehjs/test/models/grids/grid.ts
@@ -3,6 +3,7 @@ import {expect} from "chai"
 import {Axis} from "@bokehjs/models/axes/axis"
 import {BasicTicker} from "@bokehjs/models/tickers/basic_ticker"
 import {BasicTickFormatter} from "@bokehjs/models/formatters/basic_tick_formatter"
+import {FixedTicker} from "@bokehjs/models/tickers/fixed_ticker"
 import {Grid, GridView} from "@bokehjs/models/grids/grid"
 import {Plot} from "@bokehjs/models/plots/plot"
 import {Range1d} from "@bokehjs/models/ranges/range1d"
@@ -99,4 +100,48 @@ describe("Grid", () => {
       [[0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9]],
     ])
   })
+
+  it("should delegate to an Axis ticker", () => {
+    const plot = new Plot({
+      x_range: new Range1d({start: 0.1, end: 9.9}),
+      y_range: new Range1d({start: 0.1, end: 9.9}),
+    })
+    const ticker = new BasicTicker()
+    const formatter = new BasicTickFormatter()
+    const axis = new Axis({ticker, formatter})
+    plot.add_layout(axis, 'below')
+    const grid = new Grid({axis})
+    plot.add_layout(grid, 'center')
+    const plot_view = new plot.default_view({model: plot, parent: null}).build()
+    const grid_view = plot_view.renderer_views[grid.id] as GridView
+
+    expect(grid_view.grid_coords('major', false)).to.be.deep.equal([
+      [[0.1, 0.1], [2, 2],     [4, 4],     [6, 6],     [8, 8],     [9.9, 9.9]],
+      [[0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9]],
+    ])
+  })
+
+  it("should prefer an explicit ticker to an Axis ticker", () => {
+    const plot = new Plot({
+      x_range: new Range1d({start: 0.1, end: 9.9}),
+      y_range: new Range1d({start: 0.1, end: 9.9}),
+    })
+
+    const axis_ticker = new FixedTicker({ticks: [1, 2, 3, 4]})
+    const formatter = new BasicTickFormatter()
+    const axis = new Axis({ticker: axis_ticker, formatter})
+
+    plot.add_layout(axis, 'below')
+    const ticker = new BasicTicker()
+    const grid = new Grid({axis, ticker})
+    plot.add_layout(grid, 'center')
+    const plot_view = new plot.default_view({model: plot, parent: null}).build()
+    const grid_view = plot_view.renderer_views[grid.id] as GridView
+
+    expect(grid_view.grid_coords('major', false)).to.be.deep.equal([
+      [[0.1, 0.1], [2, 2],     [4, 4],     [6, 6],     [8, 8],     [9.9, 9.9]],
+      [[0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9], [0.1, 9.9]],
+    ])
+  })
+
 })
diff --git a/tests/unit/bokeh/models/test_grids.py b/tests/unit/bokeh/models/test_grids.py
--- a/tests/unit/bokeh/models/test_grids.py
+++ b/tests/unit/bokeh/models/test_grids.py
@@ -19,7 +19,7 @@
 # External imports
 
 # Bokeh imports
-from bokeh.models import FixedTicker
+from bokeh.models import FixedTicker, LinearAxis
 
 # Module under test
 import bokeh.models.grids as bmg
@@ -42,6 +42,14 @@ def test_ticker_accepts_number_sequences():
     assert isinstance(g.ticker, FixedTicker)
     assert g.ticker.ticks == [-10, 0, 10, 20.7]
 
+def test_ticker_accepts_axis():
+    g = bmg.Grid(axis=LinearAxis())
+    assert isinstance(g.axis, LinearAxis)
+
+    g = bmg.Grid()
+    g.axis = LinearAxis()
+    assert isinstance(g.axis, LinearAxis)
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------
diff --git a/tests/unit/bokeh/plotting/test_figure.py b/tests/unit/bokeh/plotting/test_figure.py
--- a/tests/unit/bokeh/plotting/test_figure.py
+++ b/tests/unit/bokeh/plotting/test_figure.py
@@ -145,6 +145,14 @@ def test_log_axis(self):
         p.circle([1, 2, 3], [1, 2, 3])
         assert isinstance(p.y_scale, LogScale)
 
+    def test_grid_tickers(self):
+        p = bpf.figure()
+        assert p.xgrid[0].axis == p.xaxis[0]
+        assert p.xgrid[0].ticker is None
+
+        assert p.ygrid[0].axis == p.yaxis[0]
+        assert p.ygrid[0].ticker is None
+
     def test_xgrid(self):
         p = bpf.figure()
         p.circle([1, 2, 3], [1, 2, 3])