Repository: bokeh/bokeh

Issue:
Axis should have option to have a fixed orientation
Right now axes determine the orientation of the "In" vector based on which side of the plot they're on.  For axis locations of "min" or "max", that's OK, but when a plot has a fixed position in dataspace, then it should be able to have a fixed orientation, i.e. tick labels are always above/below or always left/right of the axis.



Discussion:
@bryevdv I don't know the guts of BokehJS well enough, but I'm pretty sure this is no longer applicable. Am I correct in thinking so?

Yes and no... It's no longer applicable because the ability to position axes at fixed data space coordinates is currently removed. But, it should be added back at some point when we can figure out how to make it work.

> When we can figure out how to make it work

What problem exactly needs to be figured out?

There's a tension with the current layout system capabilities and what can be spelled. 

I actually don't think this will be terrible to implement at this point but there are too many bugs left to fix for `0.12.6`, moving to next milestone
Just hard coding `loc` to return 0 demonstrates that things already "sort of work":

<img width="616" alt="screen shot 2018-04-08 at 15 41 03" src="https://user-images.githubusercontent.com/1078448/38472287-5d989962-3b43-11e8-8895-e598605ebba6.png">

As it stands, there is extra padding for the vacant-but-still-present side panels, and also the axes are not clipped at the inner plot boundary. So I would propose to add an optional `fixed_location` property to `Axis` that is what `loc` returns (if it is set). Then fix up other code paths so that: 

* if there is a fixed location, the axis is added to a render level that gets clipped
* if there is a fixed location, the side panel width/height is collapsed to zero

To be clear, this is a slight abuse of side panels, but anything else would seem to be much more work. In particular, keeping the side panel, and adding the axis to a layout on a side, provides an immediate, already working way to specify which "side" of the axis the tick labels should be drawn on. Otherewise we would need new dedicated properties for those, and then to also figure out interactions with all the existing properties. 

We could also consider an `add_axis` method to plot that would be a little more comprehensible to users than `add_layout`.

Resolving patch:
diff --git a/bokeh/models/axes.py b/bokeh/models/axes.py
--- a/bokeh/models/axes.py
+++ b/bokeh/models/axes.py
@@ -146,6 +146,16 @@ class Axis(GuideRenderer):
     main plot area.
     """)
 
+    fixed_location = Either(Float, String, Tuple(String, String), Tuple(String, String, String), default=None, help="""
+    Set to specify a fixed coordinate location to draw the axis. The direction
+    of ticks and major labels is determined by the side panel that the axis
+    belongs to.
+
+    .. note::
+        Axes labels are suppressed when axes are positioned at fixed locations
+        inside the central plot area.
+    """)
+
 @abstract
 class ContinuousAxis(Axis):
     ''' A base class for all numeric, non-categorical axes types.
diff --git a/examples/plotting/file/fixed_axis.py b/examples/plotting/file/fixed_axis.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/file/fixed_axis.py
@@ -0,0 +1,18 @@
+import numpy as np
+
+from bokeh.plotting import figure, show, output_file
+
+x = np.linspace(-6, 6, 500)
+y = 8*np.sin(x)*np.sinc(x)
+
+p = figure(plot_width=800, plot_height=300, title="", tools="",
+           toolbar_location=None, match_aspect=True)
+
+p.line(x, y, color="navy", alpha=0.4, line_width=4)
+p.background_fill_color = "#efefef"
+p.xaxis.fixed_location = 0
+p.yaxis.fixed_location = 0
+
+output_file("fixed_axis.html", title="fixed_axis.py example")
+
+show(p)


Test patch:
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
@@ -8,8 +8,10 @@ sinon = require 'sinon'
 {Plot} = utils.require("models/plots/plot")
 {PlotCanvas} = utils.require("models/plots/plot_canvas")
 {PlotView} = utils.require("models/plots/plot")
+{FactorRange} = utils.require("models/ranges/factor_range")
 {Range1d} = utils.require("models/ranges/range1d")
 {SidePanel} = utils.require("core/layout/side_panel")
+{CategoricalScale} = utils.require("models/scales/categorical_scale")
 {Toolbar} = utils.require("models/tools/toolbar")
 {Document} = utils.require "document"
 
@@ -32,6 +34,43 @@ describe "Axis", ->
     })
     expect(axis.compute_labels([0,2,4.0,6,8,10])).to.be.deep.equal ["zero", "2", "four", "6", "8", "ten"]
 
+  it "loc should return numeric fixed_location", ->
+    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
+      fixed_location: 10
+    })
+    expect(axis.loc).to.equal 10
+
+  it "loc should return synthetic for categorical fixed_location", ->
+    doc = new Document()
+    p = new Plot({
+      x_range: new FactorRange({factors: ["foo", "bar"]})
+      x_scale: new CategoricalScale()
+      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
+      fixed_location: "foo"
+    })
+    axis.attach_document(p.document)
+    axis.add_panel('left')
+    expect(axis.loc).to.equal 0.5
+
   it "should have a SidePanel after add_panel is called", ->
     doc = new Document()
     p = new Plot({
@@ -103,6 +142,13 @@ describe "AxisView", ->
       parent: plot_canvas_view
     })
 
+  it "needs_clip should return the false when fixed_location null", ->
+    expect(@axis_view.needs_clip).to.be.equal false
+
+  it "needs_clip should return the false when fixed_location null", ->
+    @axis.fixed_location = 10
+    expect(@axis_view.needs_clip).to.be.equal true
+
   it "_tick_extent should return the major_tick_out property", ->
     expect(@axis_view._tick_extent()).to.be.equal @axis.major_tick_out
 
diff --git a/bokehjs/test/models/renderers/index.coffee b/bokehjs/test/models/renderers/index.coffee
--- a/bokehjs/test/models/renderers/index.coffee
+++ b/bokehjs/test/models/renderers/index.coffee
@@ -1 +1,2 @@
 require "./glyph_renderer"
+require "./renderer"
diff --git a/bokehjs/test/models/renderers/renderer.coffee b/bokehjs/test/models/renderers/renderer.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/renderers/renderer.coffee
@@ -0,0 +1,13 @@
+{expect} = require "chai"
+utils = require "../../utils"
+
+{Renderer, RendererView} = utils.require("models/renderers/renderer")
+
+describe "RendererView", ->
+
+  describe "needs_clip", ->
+
+    it "should return false", ->
+      r = new Renderer()
+      rv = new RendererView({model: r, parent: null})
+      expect(rv.needs_clip).to.be.equal false