Repository: bokeh/bokeh

Issue:
allow users to specify explicit tick labels
`FixedTicker` allows users to specify fixed tick locations, but also need to allow users to specify explicit tick labels



Discussion:
One idea would be to add mapping (`labels`? `custom`?)  to any `TickFormatter` that maps numeric tick values to specific custom labels for that tick location. Before doing its normal formatting and rendering for a tick value, the `TickFormatter` would first try to look that value up in the mapping. If it exists, that label is used as-is, instead of the normal formatting. 

So use might look something like:

```
plot.xaxis.formatter.custom[50] = "median"
```

Random observations:
- this means the customization is attached to a formatter, so if the formatter is changed the customization is lost (potentially confusing in those situations)
- suggests might want ability to add custom fixed ticks to any ticker (or to be able to combine a fixed ticker with a regular ticker)

@bokeh/dev  thoughts?

I can see multiple angles on this.  There is an argument to be made that it should be on the Ticker, as well as the Formatter.  However, the most important thing IMO is that the split between ticker vs. formatter is an architectural one in our object model, and, under normal circumstances, it's not a distinction that we should burden the user with understanding.

We need to consider the use cases when an end user is most likely to want to specify these things:
1. They have a few, specific locations that they want labelled.
2. They have a custom format that _extends_ an existing built-in formatter
3. They want to replace a built-in ticker+formatter altogether and _only_ show labels at certain locations, regardless of zoom level

What would the ideal spelling look like for each of these?  Are there other common use cases I'm missing here?

@bryevdv Is there a way to do this yet?

Currently you could do this with a custom `TickFormatter`. Given that there is _some_ path to doing this, even if it is not ideal, adding support into the core library will probably not rise high in the ~750 open issue list. A new interested contributor submitting a PR is probably the quickest route. 

@bryevdv Thanks. That is currently the way I'm doing it in my code as well.

Here's an example of doing exactly that (using a FixedTicker and FuncTickFormatter together to set explicit tick labels at specific locations). I think it's the best method for doing this:

``` python
from bokeh.plotting import figure, show
from bokeh.models import ColorBar, LinearColorMapper, Plot, Range1d, LinearAxis, FixedTicker, FuncTickFormatter

color_mapper = LinearColorMapper(palette="Spectral6", low=0, high=60)
ticker = FixedTicker(ticks=[5,15,25,35,45,55])
formatter = FuncTickFormatter(code="""
    data = {5: '0-10', 15: '10-20', 25: '20-30', 35: '30-40', 45: '40-50', 55: '50+'}
    return data[tick]
""")

cbar = ColorBar(color_mapper=color_mapper, ticker=ticker, formatter=formatter,
                major_tick_out=0, major_tick_in=0, major_label_text_align='left',
                major_label_text_font_size='10pt', label_standoff=2)

p = Plot(x_range=Range1d(0,1), y_range=Range1d(0,1), width=500, height=500, toolbar_location=None)
p.add_layout(LinearAxis(), 'left')
p.add_layout(LinearAxis(), 'below')

p.add_layout(cbar)

show(p)
```

@canavandl Thank you for the idea. Currently I am using

``` python
class FixedTickFormatter(TickFormatter):
    """
    Class used to allow custom axis tick labels on a bokeh chart
    Extends bokeh.model.formatters.TickFormatte
    """

    JS_CODE =  """
        _ = require "underscore"
        Model = require "model"
        p = require "core/properties"
        class FixedTickFormatter extends Model
          type: 'FixedTickFormatter'
          doFormat: (ticks) ->
            labels = @get("labels")
            return (labels[tick] ? "" for tick in ticks)
          @define {
            labels: [ p.Any ]
          }
        module.exports =
          Model: FixedTickFormatter
    """

    labels = Dict(Int, String, help="""
    A mapping of integer ticks values to their labels.
    """)

    __implementation__ = JS_CODE
```

@Rubyj I was curious about this, so I put together a complete example.

Please note that you're custom FixedTickFormatter needed to be changed quite a lot for it to work on master (and hence the next release)

``` python
from bokeh.plotting import show
from bokeh.properties import Dict, Int, String
from bokeh.models import (
    ColorBar,
    LinearColorMapper,
    Plot,
    Range1d,
    LinearAxis,
    FixedTicker,
    TickFormatter,
)
from bokeh.util.compiler import CoffeeScript

class FixedTickFormatter(TickFormatter):
    """
    Class used to allow custom axis tick labels on a bokeh chart
    Extends bokeh.model.formatters.TickFormatter
    """

    COFFEESCRIPT =  """
        import {Model} from "model"
        import * as p from "core/properties"
        export class FixedTickFormatter extends Model
          type: 'FixedTickFormatter'
          doFormat: (ticks) ->
            labels = @get("labels")
            return (labels[tick] ? "" for tick in ticks)
          @define {
            labels: [ p.Any ]
          }
    """

    labels = Dict(Int, String, help="""
    A mapping of integer ticks values to their labels.
    """)

    __implementation__ = CoffeeScript(COFFEESCRIPT)


color_mapper = LinearColorMapper(palette="Spectral6", low=0, high=60)
ticker = FixedTicker(ticks=[5,15,25,35,45,55])
formatter = FixedTickFormatter(labels={5: '0-10', 15: '10-20', 25: '20-30', 35: '30-40', 45: '40-50', 55: '50+'})

cbar = ColorBar(color_mapper=color_mapper, ticker=ticker, formatter=formatter,
                major_tick_out=0, major_tick_in=0, major_label_text_align='left',
                major_label_text_font_size='10pt', label_standoff=2)

p = Plot(x_range=Range1d(0,1), y_range=Range1d(0,1), width=500, height=500, toolbar_location=None)
p.add_layout(LinearAxis(), 'left')
p.add_layout(LinearAxis(), 'below')
p.add_layout(cbar)
show(p)
```

@bryevdv - we're going to need some major migration notes about the new export stuff, then new import stuff, and the compilation stuff for folks

@birdsarah awesome! Thank you for this :)

```
doFormat: (ticks) ->
    labels = @get("labels")
    return (labels[tick] ? "" for tick in ticks)
```

Actually `labels = @labels`, otherwise deprecation warning will appear.

Hi, I arrived here via Stack Overflow.  
The example above breaks somewhere between v 0.12.2 and 0.12.4.  I get the following message:

> Model `FixedTickFormatter' does not exist. This could be due to a widget or a custom model not being registered before first usage.

The clue seems to be in the comment above :)

>  we're going to need some major migration notes about the new export stuff, then new import stuff, and the compilation stuff for folks

Has anyone got any suggestion what the best way to go about this is now?  I guess there's some different way to register the JS code?
@hodgson-neil 

 I am using the below code which works fine in my application running the latest version of bokeh

```class FixedTickFormatter(TickFormatter):
    """
    Class used to allow custom axis tick labels on a bokeh chart
    Extends bokeh.model.formatters.TickFormatter
    """

    COFFEESCRIPT =  """
        import {Model} from "model"
        import * as p from "core/properties"
        export class FixedTickFormatter extends Model
          type: 'FixedTickFormatter'
          doFormat: (ticks) ->
            labels = @labels
            return (labels[tick] ? "" for tick in ticks)
          @define {
            labels: [ p.Any ]
          }
    """

    labels = Dict(Int, String, help="""
    A mapping of integer ticks values to their labels.
    """)

    __implementation__ = CoffeeScript(COFFEESCRIPT)```
@Rubyj Thanks, my mistake.  It is indeed working fine on my use case!  Thanks.
@canavandl How can you do the same for the x-axis in a scatter plot? Thanks.
@pzwang I guess my use case is either the first or the third.

I'm generating a Manhattan plot, which plots millions of points with the probability of association between phenotypes and genotypes at millions of base pair locations in the genome:
https://en.wikipedia.org/wiki/Manhattan_plot

Preferably I would also like for the position on a given chromosome to be shown, when I zoom in, but that would probably require a secondary x-axis.

I'm super excited about Bokeh! Right now I have to semi naively down-sample my data to less than 100k data points, because the hover tool doesn't work with datashader. Tomorrow I'll look into doing what I think you refer to as call backs with Bokeh serve, so I can do Manhattan plots for various phenotypic traits. Awesome! Excited!
@tommycarstensen , I haven't tested it, but the below should work:

```python
from bokeh.plotting import show
from bokeh.properties import Dict, Int, String
from bokeh.models import (
    Plot,
    Range1d,
    LinearAxis,
    FixedTicker,
    TickFormatter,
)
from bokeh.util.compiler import CoffeeScript

class FixedTickFormatter(TickFormatter):
    """
    Class used to allow custom axis tick labels on a bokeh chart
    Extends bokeh.model.formatters.TickFormatter
    """

    COFFEESCRIPT =  """
        import {Model} from "model"
        import * as p from "core/properties"
        export class FixedTickFormatter extends Model
          type: 'FixedTickFormatter'
          doFormat: (ticks) ->
            labels = @get("labels")
            return (labels[tick] ? "" for tick in ticks)
          @define {
            labels: [ p.Any ]
          }
    """

    labels = Dict(Int, String, help="""
    A mapping of integer ticks values to their labels.
    """)

    __implementation__ = CoffeeScript(COFFEESCRIPT)

ticker = FixedTicker(ticks=[5,15,25,35,45,55])
formatter = FixedTickFormatter(labels={5: '0-10', 15: '10-20', 25: '20-30', 35: '30-40', 45: '40-50', 55: '50+'})
x_axis = LinearAxis(ticker=ticker, formatter=formatter)

p = Plot(x_range=Range1d(0,60))
p.add_layout(x_axis, 'below')

show(p)
```
> Right now I have to semi naively down-sample my data to less than 100k data points, because the hover tool doesn't work with datashader.

With the latest GitHub master version of HoloViews, you can now easily overlay a hover layer on a datashader plot, as shown here: https://github.com/ioam/holoviews/pull/1223

Not sure if that helps your use case, but if it doesn't, please file an issue about that on the datashader site.

Resolving patch:
diff --git a/bokeh/models/axes.py b/bokeh/models/axes.py
--- a/bokeh/models/axes.py
+++ b/bokeh/models/axes.py
@@ -5,12 +5,12 @@
 from __future__ import absolute_import
 
 from ..core.has_props import abstract
-from ..core.properties import Auto, Datetime, Either, Enum, Float, Include, Instance, Int, Override, String, Tuple
+from ..core.properties import Auto, Datetime, Dict, Either, Enum, Float, Include, Instance, Int, Override, Seq, String, Tuple
 from ..core.property_mixins import LineProps, TextProps
 
 from .formatters import BasicTickFormatter, CategoricalTickFormatter, DatetimeTickFormatter, LogTickFormatter, TickFormatter
 from .renderers import GuideRenderer
-from .tickers import Ticker, BasicTicker, LogTicker, CategoricalTicker, DatetimeTicker
+from .tickers import Ticker, BasicTicker, LogTicker, CategoricalTicker, DatetimeTicker, FixedTicker
 
 @abstract
 class Axis(GuideRenderer):
@@ -37,7 +37,29 @@ class Axis(GuideRenderer):
 
     ticker = Instance(Ticker, help="""
     A Ticker to use for computing locations of axis components.
-    """)
+
+    The property may also be passed a sequence of floating point numbers as
+    a shorthand for creating and configuring a ``FixedTicker``, e.g. the
+    following code
+
+    .. code-block:: python
+
+        from bokeh.plotting import figure
+
+        p = figure()
+        p.xaxis.ticker = [10, 20, 37.4]
+
+    is equivalent to:
+
+    .. code-block:: python
+
+        from bokeh.plotting import figure
+        from bokeh.models.tickers import FixedTicker
+
+        p = figure()
+        p.xaxis.ticker = FixedTicker(ticks=[10, 20, 37.4])
+
+    """).accepts(Seq(Float), lambda ticks: FixedTicker(ticks=ticks))
 
     formatter = Instance(TickFormatter, help="""
     A TickFormatter to use for formatting the visual appearance
@@ -76,6 +98,11 @@ class Axis(GuideRenderer):
     number is supplied, the angle of the text is measured from horizontal.
     """)
 
+    major_label_overrides = Dict(Either(Float, String), String, default={}, help="""
+    Provide explicit tick label values for specific tick locations that
+    override normal formatting.
+    """)
+
     major_label_props = Include(TextProps, help="""
     The %s of the major tick labels.
     """)
diff --git a/sphinx/source/docs/user_guide/examples/styling_fixed_ticker.py b/sphinx/source/docs/user_guide/examples/styling_fixed_ticker.py
--- a/sphinx/source/docs/user_guide/examples/styling_fixed_ticker.py
+++ b/sphinx/source/docs/user_guide/examples/styling_fixed_ticker.py
@@ -1,11 +1,10 @@
 from bokeh.plotting import figure, output_file, show
-from bokeh.models import FixedTicker
 
-output_file("axes.html")
+output_file("fixed_ticks.html")
 
 p = figure(plot_width=400, plot_height=400)
 p.circle([1,2,3,4,5], [2,5,8,2,7], size=10)
 
-p.xaxis[0].ticker=FixedTicker(ticks=[2, 3.5, 4])
+p.xaxis.ticker = [2, 3.5, 4]
 
 show(p)


Test patch:
diff --git a/bokeh/models/tests/test_axes.py b/bokeh/models/tests/test_axes.py
new file mode 100644
--- /dev/null
+++ b/bokeh/models/tests/test_axes.py
@@ -0,0 +1,15 @@
+from __future__ import absolute_import
+
+import bokeh.models.axes as bma
+
+from bokeh.models import FixedTicker
+
+def test_ticker_accepts_number_sequences():
+    a = bma.Axis(ticker=[-10, 0, 10, 20.7])
+    assert isinstance(a.ticker, FixedTicker)
+    assert a.ticker.ticks == [-10, 0, 10, 20.7]
+
+    a = bma.Axis()
+    a.ticker = [-10, 0, 10, 20.7]
+    assert isinstance(a.ticker, FixedTicker)
+    assert a.ticker.ticks == [-10, 0, 10, 20.7]
diff --git a/bokehjs/test/models/axes/axis.coffee b/bokehjs/test/models/axes/axis.coffee
--- a/bokehjs/test/models/axes/axis.coffee
+++ b/bokehjs/test/models/axes/axis.coffee
@@ -15,6 +15,23 @@ sinon = require 'sinon'
 
 describe "Axis", ->
 
+  it "should compute labels with overrides", ->
+    doc = new Document()
+    p = new Plot({
+      x_range: new Range1d({start: 0, end: 10})
+      y_range: new Range1d({start: 0, end: 10})
+    })
+    doc.add_root(p)
+    ticker = new BasicTicker()
+    formatter = new BasicTickFormatter()
+    axis = new Axis({
+      ticker: ticker
+      formatter: formatter
+      plot: p
+      major_label_overrides: {0: "zero", 4: "four", 10: "ten"}
+    })
+    expect(axis.compute_labels([0,2,4.0,6,8,10])).to.be.deep.equal ["zero", "2", "four", "6", "8", "ten"]
+
   it "should have a SidePanel after add_panel is called", ->
     doc = new Document()
     p = new Plot({