Repository: bokeh/bokeh

Issue:
Create JS/TS charts API



Discussion:

Resolving patch:
diff --git a/bokeh/core/enums.py b/bokeh/core/enums.py
--- a/bokeh/core/enums.py
+++ b/bokeh/core/enums.py
@@ -92,9 +92,6 @@ def enumeration(*values, **kwargs):
 #: Specify a stroke direction for circles, wedges, etc.
 Direction = enumeration("clock", "anticlock")
 
-#: Specify a left/right side
-Side = enumeration("left", "right")
-
 #: Specify units for mapping values
 Units = enumeration("screen", "data")
 
diff --git a/bokeh/models/annotations.py b/bokeh/models/annotations.py
--- a/bokeh/models/annotations.py
+++ b/bokeh/models/annotations.py
@@ -5,7 +5,7 @@
 from __future__ import absolute_import
 
 from ..core.enums import (
-    Orientation, LegendLocation, SpatialUnits, Dimension, RenderMode, Side,
+    Orientation, LegendLocation, SpatialUnits, Dimension, RenderMode,
     AngleUnits, TextAlign, FontStyle
 )
 from ..core.property_mixins import LineProps, FillProps, TextProps
@@ -652,9 +652,10 @@ class Tooltip(Annotation):
     """
     level = Override(default="overlay")
 
-    side = Either(Auto, Enum(Side), default="auto", help="""
+    attachment = Enum("horizontal", "vertical", "left", "right", "above", "below", help="""
     Whether the tooltip should display to the left or right off the cursor
-    position, or if it should be automatically placed.
+    position or above or below it, or if it should be automatically placed
+    in the horizontal or vertical dimension.
     """)
 
     inner_only = Bool(default=True, help="""
diff --git a/bokeh/models/tools.py b/bokeh/models/tools.py
--- a/bokeh/models/tools.py
+++ b/bokeh/models/tools.py
@@ -27,7 +27,7 @@
 from ..core.properties import (
     Any, Auto, Bool, String, Enum, Instance, Either, List, Dict, Tuple, Override
 )
-from ..core.enums import Dimension, Location
+from ..core.enums import Dimension, Location, Anchor
 
 from .annotations import BoxAnnotation, PolyAnnotation
 from .callbacks import Callback
@@ -657,8 +657,8 @@ class HoverTool(Inspection):
     """)
 
     point_policy = Enum("snap_to_data", "follow_mouse", "none", help="""
-    Whether the tooltip position should snap to the "center" position of
-    the associated glyph, or always follow the current mouse cursor
+    Whether the tooltip position should snap to the "center" (or other anchor)
+    position of the associated glyph, or always follow the current mouse cursor
     position.
     """)
 
@@ -669,6 +669,15 @@ class HoverTool(Inspection):
     mouse position.
     """)
 
+    anchor = Enum(Anchor, default="center", help="""
+    If point policy is set to `"snap_to_data"`, `anchor` defines the attachment
+    point of a tooltip. The default is to attach to the center of a glyph.
+    """)
+
+    attachment = Enum("horizontal", "vertical", help="""
+    Whether tooltip's arrow should appear in the horizontal or vertical dimension.
+    """)
+
 DEFAULT_HELP_TIP = "Click the question mark to learn more about Bokeh plot tools."
 DEFAULT_HELP_URL = "http://bokeh.pydata.org/en/latest/docs/user_guide/tools.html"
 


Test patch:
diff --git a/bokehjs/test/common/defaults.coffee b/bokehjs/test/common/defaults.coffee
--- a/bokehjs/test/common/defaults.coffee
+++ b/bokehjs/test/common/defaults.coffee
@@ -35,18 +35,12 @@ deep_value_to_json = (key, value, optional_parent_object) ->
   else if _.isArray(value)
     ref_array = []
     for v, i in value
-      if v instanceof HasProps and not v.serializable_in_document()
-        console.log("May need to add #{key} to nonserializable_attribute_names of #{optional_parent_object?.constructor.name} because array contains a nonserializable type #{v.constructor.name} under index #{i}")
-      else
-        ref_array.push(deep_value_to_json(i, v, value))
+      ref_array.push(deep_value_to_json(i, v, value))
     ref_array
   else if _.isObject(value)
     ref_obj = {}
     for own subkey of value
-      if value[subkey] instanceof HasProps and not value[subkey].serializable_in_document()
-        console.log("May need to add #{key} to nonserializable_attribute_names of #{optional_parent_object?.constructor.name} because value of type #{value.constructor.name} contains a nonserializable type #{value[subkey].constructor.name} under #{subkey}")
-      else
-        ref_obj[subkey] = deep_value_to_json(subkey, value[subkey], value)
+      ref_obj[subkey] = deep_value_to_json(subkey, value[subkey], value)
     ref_obj
   else
     value
diff --git a/bokehjs/test/core/layout/side_panel.coffee b/bokehjs/test/core/layout/side_panel.coffee
--- a/bokehjs/test/core/layout/side_panel.coffee
+++ b/bokehjs/test/core/layout/side_panel.coffee
@@ -13,6 +13,7 @@ Axis = utils.require("models/axes/axis").Model
 BasicTicker = utils.require("models/tickers/basic_ticker").Model
 BasicTickFormatter = utils.require("models/formatters/basic_tick_formatter").Model
 Plot = utils.require("models/plots/plot").Model
+PlotCanvas = utils.require("models/plots/plot_canvas").Model
 PlotView = utils.require("models/plots/plot").View
 Range1d = utils.require("models/ranges/range1d").Model
 Toolbar = utils.require("models/tools/toolbar").Model
@@ -26,7 +27,7 @@ describe "SidePanel.Model", ->
     # constraints, but this is hard (impossible?) at the moment, so will have to do some
     # visual testing to make sure things line up.
 
-describe "SidePanel apply_location_heuristics", ->
+  describe "apply_location_heuristics", ->
 
     it "should calculate appropriate axis_label text properties based on location", ->
       p = new SidePanel({side: 'left'})
@@ -41,7 +42,7 @@ describe "SidePanel apply_location_heuristics", ->
       expect(ctx.textBaseline).to.be.equal "middle"
       expect(ctx.textAlign).to.be.equal "right"
 
-describe "SidePanel get_label_angle_heuristic", ->
+  describe "get_label_angle_heuristic", ->
 
     it "should calculate appropriate axis_label angle rotation based on location", ->
       p = new SidePanel({side: 'left'})
@@ -52,86 +53,93 @@ describe "SidePanel get_label_angle_heuristic", ->
       angle = p.get_label_angle_heuristic('horizontal')
       expect(angle).to.be.equal 0
 
-describe "SidePanel update_constraints", ->
-  # Using axis_view as the view to pass into update_constraints
-
-  afterEach ->
-    utils.unstub_canvas()
-    utils.unstub_solver()
-
-  beforeEach ->
-    utils.stub_canvas()
-    solver_stubs = utils.stub_solver()
-    @solver_add_constraint = solver_stubs['add']
-    @solver_remove_constraint = solver_stubs['remove']
-
-    test_plot = new Plot({ x_range: new Range1d({start: 0, end: 1}), y_range: new Range1d({start: 0, end: 1}), toolbar: new Toolbar()})
-    test_plot.attach_document(new Document())
-    @axis = new Axis({ ticker: new BasicTicker(), formatter: new BasicTickFormatter() })
-    @test_plot = test_plot.plot_canvas()
-    @test_plot.add_layout(@axis, 'below')
-    @test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
-    @axis_view = new @axis.default_view({ model: @axis, plot_model: @test_plot, plot_view: @test_plot_view })
-
-  it "should not fail if visible is not on model", ->
-    an = new Annotation()
-    an_view = new an.default_view({model: an, plot_model: @test_plot, plot_view: @test_plot_view})
-    expect(an_view._size_constraint).to.be.undefined
-    update_constraints(an_view)
-    # Should still be undefined because visible is false
-    expect(an_view._size_constraint).to.be.undefined
-
-  it "should not set _size_constraint if visible is false", ->
-    @axis.set('visible', false)
-    expect(@axis_view._size_constraint).to.be.undefined
-    update_constraints(@axis_view)
-    # Should still be undefined because visible is false
-    expect(@axis_view._size_constraint).to.be.undefined
-
-  it "should set last_size", ->
-    sinon.stub(@axis_view, '_tick_extent', () -> 0.11)
-    sinon.stub(@axis_view, '_axis_label_extent', () -> 0.11)
-    sinon.stub(@axis_view, '_tick_label_extent', () -> 0.11)
-    expect(@axis_view._size_constraint).to.be.undefined
-    update_constraints(@axis_view)
-    expect(@axis_view._last_size).to.be.equal 0.33
-
-  it "should add two constraints on first call (one for size, one for full)", ->
-    add_constraint_call_count = @solver_add_constraint.callCount
-    update_constraints(@axis_view)
-    expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 2
-
-  it "should add one constraints on first call (for size) if manually set view._full_set to true", ->
-    add_constraint_call_count = @solver_add_constraint.callCount
-    @axis_view._full_set = true
-    update_constraints(@axis_view)
-    expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 1
-
-  it "should add and remove a constraint if the size changes", ->
-    @axis_view._tick_extent = sinon.stub()
-    @axis_view._tick_extent.onCall(0).returns(0.11)
-    @axis_view._tick_extent.onCall(1).returns(0.22)
-
-    add_constraint_call_count = @solver_add_constraint.callCount
-    remove_constraint_call_count = @solver_remove_constraint.callCount
-
-    update_constraints(@axis_view)
-
-    expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 2
-    expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count + 0
-
-    update_constraints(@axis_view)
-
-    expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 3
-    expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count + 1
-
-  it "should not add and remove a constraint if the size is the same", ->
-    @axis_view._tick_extent = sinon.stub()
-    @axis_view._tick_extent.returns(0.11)
-
-    update_constraints(@axis_view)
-    add_constraint_call_count = @solver_add_constraint.callCount
-    remove_constraint_call_count = @solver_remove_constraint.callCount
-    update_constraints(@axis_view)
-    expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count
-    expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count
+  describe "update_constraints", ->
+    # Using axis_view as the view to pass into update_constraints
+
+    afterEach ->
+      utils.unstub_canvas()
+      utils.unstub_solver()
+
+    beforeEach ->
+      utils.stub_canvas()
+      solver_stubs = utils.stub_solver()
+      @solver_add_constraint = solver_stubs['add']
+      @solver_remove_constraint = solver_stubs['remove']
+
+      doc = new Document()
+      plot = new Plot({
+        x_range: new Range1d({start: 0, end: 1})
+        y_range: new Range1d({start: 0, end: 1})
+        toolbar: new Toolbar()
+      })
+      @axis = new Axis({ ticker: new BasicTicker(), formatter: new BasicTickFormatter() })
+      plot.add_layout(@axis, 'below')
+      plot.attach_document(doc)
+      plot_view = new plot.default_view({ 'model': plot })
+      @plot_canvas = new PlotCanvas({ 'plot': plot })
+      @plot_canvas.attach_document(doc)
+      @plot_canvas_view = new @plot_canvas.default_view({ 'model': @plot_canvas })
+      @axis_view = new @axis.default_view({ model: @axis, plot_model: @plot_canvas, plot_view: @plot_canvas_view })
+
+    it "should not fail if visible is not on model", ->
+      an = new Annotation()
+      an_view = new an.default_view({model: an, plot_model: @plot_canvas, plot_view: @plot_canvas_view})
+      expect(an_view._size_constraint).to.be.undefined
+      update_constraints(an_view)
+      # Should still be undefined because visible is false
+      expect(an_view._size_constraint).to.be.undefined
+
+    it "should not set _size_constraint if visible is false", ->
+      @axis.set('visible', false)
+      expect(@axis_view._size_constraint).to.be.undefined
+      update_constraints(@axis_view)
+      # Should still be undefined because visible is false
+      expect(@axis_view._size_constraint).to.be.undefined
+
+    it "should set last_size", ->
+      sinon.stub(@axis_view, '_tick_extent', () -> 0.11)
+      sinon.stub(@axis_view, '_axis_label_extent', () -> 0.11)
+      sinon.stub(@axis_view, '_tick_label_extent', () -> 0.11)
+      expect(@axis_view._size_constraint).to.be.undefined
+      update_constraints(@axis_view)
+      expect(@axis_view._last_size).to.be.equal 0.33
+
+    it "should add two constraints on first call (one for size, one for full)", ->
+      add_constraint_call_count = @solver_add_constraint.callCount
+      update_constraints(@axis_view)
+      expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 2
+
+    it "should add one constraints on first call (for size) if manually set view._full_set to true", ->
+      add_constraint_call_count = @solver_add_constraint.callCount
+      @axis_view._full_set = true
+      update_constraints(@axis_view)
+      expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 1
+
+    it "should add and remove a constraint if the size changes", ->
+      @axis_view._tick_extent = sinon.stub()
+      @axis_view._tick_extent.onCall(0).returns(0.11)
+      @axis_view._tick_extent.onCall(1).returns(0.22)
+
+      add_constraint_call_count = @solver_add_constraint.callCount
+      remove_constraint_call_count = @solver_remove_constraint.callCount
+
+      update_constraints(@axis_view)
+
+      expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 2
+      expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count + 0
+
+      update_constraints(@axis_view)
+
+      expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count + 3
+      expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count + 1
+
+    it "should not add and remove a constraint if the size is the same", ->
+      @axis_view._tick_extent = sinon.stub()
+      @axis_view._tick_extent.returns(0.11)
+
+      update_constraints(@axis_view)
+      add_constraint_call_count = @solver_add_constraint.callCount
+      remove_constraint_call_count = @solver_remove_constraint.callCount
+      update_constraints(@axis_view)
+      expect(@solver_add_constraint.callCount).to.be.equal add_constraint_call_count
+      expect(@solver_remove_constraint.callCount).to.be.equal remove_constraint_call_count
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
@@ -16,7 +16,7 @@ Toolbar = utils.require("models/tools/toolbar").Model
 describe "Axis.Model", ->
 
   it "should have a SidePanel after add_panel is called", ->
-    p = new PlotCanvas({
+    p = new Plot({
       x_range: new Range1d({start: 0, end: 1})
       y_range: new Range1d({start: 0, end: 1})
     })
@@ -34,21 +34,19 @@ describe "Axis.Model", ->
     expect(axis.panel).to.be.an.instanceOf(SidePanel)
 
   it "should have a SidePanel after plot.add_layout is called", ->
-    p = new PlotCanvas({
-      x_range: new Range1d({start: 0, end: 1})
-      y_range: new Range1d({start: 0, end: 1})
-    })
-    p.attach_document(new Document())
     ticker = new BasicTicker()
     formatter = new BasicTickFormatter()
     axis = new Axis({
       ticker: ticker
       formatter: formatter
-      plot: p
     })
-    axis.attach_document(p.document)
     expect(axis.panel).to.be.undefined
+    p = new Plot({
+      x_range: new Range1d({start: 0, end: 1})
+      y_range: new Range1d({start: 0, end: 1})
+    })
     p.add_layout(axis, 'left')
+    p.attach_document(new Document())
     expect(axis.panel).to.be.an.instanceOf(SidePanel)
 
 describe "Axis.View", ->
@@ -61,13 +59,7 @@ describe "Axis.View", ->
     utils.stub_canvas()
     solver_stubs = utils.stub_solver()
 
-    test_plot = new Plot({
-      x_range: new Range1d({start: 0, end: 1})
-      y_range: new Range1d({start: 0, end: 1})
-      toolbar: new Toolbar()
-    })
-    test_plot.attach_document(new Document())
-    test_plot_canvas = test_plot.plot_canvas()
+    doc = new Document()
     ticker = new BasicTicker()
     formatter = new BasicTickFormatter()
     @axis = new Axis({
@@ -76,13 +68,20 @@ describe "Axis.View", ->
       ticker: ticker
       formatter: formatter
     })
-    test_plot_canvas.add_layout(@axis, 'below')
-    # Must create the test_plot_view after adding the axis to the plot
-    test_plot_canvas_view = new test_plot_canvas.default_view({ 'model': test_plot_canvas })
+    plot = new Plot({
+      x_range: new Range1d({start: 0, end: 1})
+      y_range: new Range1d({start: 0, end: 1})
+      toolbar: new Toolbar()
+    })
+    plot.add_layout(@axis, 'below')
+    plot.attach_document(doc)
+    plot_canvas = new PlotCanvas({ 'plot': plot })
+    plot_canvas.attach_document(doc)
+    plot_canvas_view = new plot_canvas.default_view({ 'model': plot_canvas })
     @axis_view = new @axis.default_view({
       model: @axis
-      plot_model: test_plot_canvas
-      plot_view: test_plot_canvas_view
+      plot_model: plot_canvas
+      plot_view: plot_canvas_view
     })
 
   it "_tick_extent should return the major_tick_out property", ->
diff --git a/bokehjs/test/models/plots/plot.coffee b/bokehjs/test/models/plots/plot.coffee
--- a/bokehjs/test/models/plots/plot.coffee
+++ b/bokehjs/test/models/plots/plot.coffee
@@ -18,7 +18,7 @@ describe "Plot", ->
     @x_range = new Range1d({start: 0, end:10})
     @y_range = new Range1d({start: 0, end: 10})
     toolbar = new Toolbar()
-    @p = new Plot({x_range: @x_range, y_range: @y_range, toolbar: toolbar})
+    @p = new Plot({x_range: @x_range, y_range: @y_range, toolbar: toolbar, title: null})
 
   describe "Plot.View", ->
     afterEach ->
@@ -96,26 +96,51 @@ describe "Plot", ->
       expect(h).to.be.equal 49
       expect(w).to.be.equal 49 * (3/5)
 
+    it "should set min_border_x to value of min_border if min_border_x is not specified", ->
+      p = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border: 33.33})
+      expect(p.min_border_top).to.be.equal 33.33
+      expect(p.min_border_bottom).to.be.equal 33.33
+      expect(p.min_border_left).to.be.equal 33.33
+      expect(p.min_border_right).to.be.equal 33.33
+
+    it "should set min_border_x to value of specified, and others to value of min_border", ->
+      p = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border: 33.33, min_border_left: 66.66})
+      expect(p.min_border_top).to.be.equal 33.33
+      expect(p.min_border_bottom).to.be.equal 33.33
+      expect(p.min_border_left).to.be.equal 66.66
+      expect(p.min_border_right).to.be.equal 33.33
+
+    it "should set min_border_x to value of specified, and others to default min_border", ->
+      p = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border_left: 4})
+      # MIN_BORDER is 5
+      expect(p.min_border_top).to.be.equal 5
+      expect(p.min_border_bottom).to.be.equal 5
+      expect(p.min_border_left).to.be.equal 4
+      expect(p.min_border_right).to.be.equal 5
+
+    it.skip "should add the title to the list of renderers", ->
+      # TODO(bird) Write this test.
+      null
 
   describe "Plot.Model", ->
 
     it "should have _horizontal set to true by default", ->
       expect(@p._horizontal).to.true
 
-    it "should have a PlotCanvas set on initialization with all the options passed to Plot", ->
-      expect(@p.plot_canvas()).to.exist
-      expect(@p.plot_canvas().x_range).to.be.deep.equal @x_range
-      expect(@p.plot_canvas().y_range).to.be.deep.equal @y_range
+    it "should have a PlotCanvas set on initialization with plot on it", ->
+      expect(@p.plot_canvas).to.exist
+      expect(@p.plot_canvas.plot).to.be.deep.equal @p
 
     it "should attach document to plot canvas when document is attached to it", ->
-      expect(@p.plot_canvas().document).to.be.null
+      expect(@p.plot_canvas.document).to.be.null
       doc = new Document()
       @p.attach_document(doc)
-      expect(@p.plot_canvas().document).to.be.equal doc
+      expect(@p.plot_canvas.document).to.be.equal doc
 
     describe "get_constrained_variables", ->
+
       beforeEach ->
-        plot_canvas = @p.plot_canvas()
+        plot_canvas = @p.plot_canvas
         # Visual alignment is dominated by the plot_canvas so a number of the
         # constraints come from there - whilst others come from the plot container.
         @expected_constrained_variables = {
diff --git a/bokehjs/test/models/plots/plot_canvas.coffee b/bokehjs/test/models/plots/plot_canvas.coffee
--- a/bokehjs/test/models/plots/plot_canvas.coffee
+++ b/bokehjs/test/models/plots/plot_canvas.coffee
@@ -24,46 +24,52 @@ Toolbar = utils.require("models/tools/toolbar").Model
 
 describe "PlotCanvas.Model", ->
 
+  beforeEach ->
+    @doc = new Document()
+    @plot = new Plot({
+      x_range: new Range1d({start: 0, end: 1})
+      y_range: new Range1d({start: 0, end: 1})
+      toolbar: new Toolbar()
+      title: null
+    })
+    @plot.attach_document(@doc)
+    @plot_canvas = new PlotCanvas({ 'plot': @plot })
+    @plot_canvas.attach_document(@doc)
+
   it "should set the sizing_mode to box by default", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d()})
-    expect(p.sizing_mode).to.be.equal 'stretch_both'
-
-  it "should have a four LayoutCanvases after document is attached is called", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d()})
-    expect(p.above_panel).to.be.undefined
-    expect(p.below_panel).to.be.undefined
-    expect(p.left_panel).to.be.undefined
-    expect(p.right_panel).to.be.undefined
-    p.attach_document(new Document())
-    expect(p.above_panel).to.be.an.instanceOf(LayoutCanvas)
-    expect(p.below_panel).to.be.an.instanceOf(LayoutCanvas)
-    expect(p.left_panel).to.be.an.instanceOf(LayoutCanvas)
-    expect(p.right_panel).to.be.an.instanceOf(LayoutCanvas)
+    expect(@plot_canvas.sizing_mode).to.be.equal 'stretch_both'
+
+  it "should have a four LayoutCanvases", ->
+    expect(@plot_canvas.above_panel).to.be.an.instanceOf(LayoutCanvas)
+    expect(@plot_canvas.below_panel).to.be.an.instanceOf(LayoutCanvas)
+    expect(@plot_canvas.left_panel).to.be.an.instanceOf(LayoutCanvas)
+    expect(@plot_canvas.right_panel).to.be.an.instanceOf(LayoutCanvas)
 
   it "should have panels, frame, and canvas returned in get_layoutable_children", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d()})
-    p.attach_document(new Document())
-    layoutable_children = p.get_layoutable_children()
+    layoutable_children = @plot_canvas.get_layoutable_children()
     expect(layoutable_children.length).to.be.equal 6
-    expect(_.contains(layoutable_children, p.above_panel)).to.be.true
-    expect(_.contains(layoutable_children, p.below_panel)).to.be.true
-    expect(_.contains(layoutable_children, p.left_panel)).to.be.true
-    expect(_.contains(layoutable_children, p.right_panel)).to.be.true
-    expect(_.contains(layoutable_children, p.frame)).to.be.true
-    expect(_.contains(layoutable_children, p.canvas)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.above_panel)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.below_panel)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.left_panel)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.right_panel)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.frame)).to.be.true
+    expect(_.contains(layoutable_children, @plot_canvas.canvas)).to.be.true
 
   it "should have axis panels in get_layoutable_children if axes added", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d()})
-    p.attach_document(new Document())
+    plot = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d(), title: null})
+    doc = new Document()
     above_axis = new LinearAxis()
     below_axis = new LinearAxis()
     left_axis = new LinearAxis()
     right_axis = new LinearAxis()
-    p.add_layout(above_axis, 'above')
-    p.add_layout(below_axis, 'below')
-    p.add_layout(left_axis, 'left')
-    p.add_layout(right_axis, 'right')
-    layoutable_children = p.get_layoutable_children()
+    plot.add_layout(above_axis, 'above')
+    plot.add_layout(below_axis, 'below')
+    plot.add_layout(left_axis, 'left')
+    plot.add_layout(right_axis, 'right')
+    plot.attach_document(doc)
+    plot_canvas = new PlotCanvas({ 'plot': plot })
+    plot_canvas.attach_document(doc)
+    layoutable_children = plot_canvas.get_layoutable_children()
     expect(layoutable_children.length).to.be.equal 10
     expect(_.contains(layoutable_children, above_axis.panel)).to.be.true
     expect(_.contains(layoutable_children, below_axis.panel)).to.be.true
@@ -71,7 +77,8 @@ describe "PlotCanvas.Model", ->
     expect(_.contains(layoutable_children, right_axis.panel)).to.be.true
 
   it "should call get_edit_variables on layoutable children", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d()})
+    pp = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d()})
+    p = pp.plot_canvas
     p.attach_document(new Document())
     children = p.get_layoutable_children()
     expect(children.length).to.be.equal 6
@@ -82,99 +89,95 @@ describe "PlotCanvas.Model", ->
     for child in children
       expect(child.get_edit_variables.callCount).to.be.equal 1
 
-  it "should set min_border_x to value of min_border if min_border_x is not specified", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border: 33.33})
-    p.attach_document(new Document())
-    expect(p.min_border_top).to.be.equal 33.33
-    expect(p.min_border_bottom).to.be.equal 33.33
-    expect(p.min_border_left).to.be.equal 33.33
-    expect(p.min_border_right).to.be.equal 33.33
-
-  it "should set min_border_x to value of specified, and others to value of min_border", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border: 33.33, min_border_left: 66.66})
-    p.attach_document(new Document())
-    expect(p.min_border_top).to.be.equal 33.33
-    expect(p.min_border_bottom).to.be.equal 33.33
-    expect(p.min_border_left).to.be.equal 66.66
-    expect(p.min_border_right).to.be.equal 33.33
-
-  it "should set min_border_x to value of specified, and others to default min_border", ->
-    p = new PlotCanvas({x_range: new DataRange1d(), y_range: new DataRange1d(), min_border_left: 4})
-    p.attach_document(new Document())
-    # MIN_BORDER is 5
-    expect(p.min_border_top).to.be.equal 5
-    expect(p.min_border_bottom).to.be.equal 5
-    expect(p.min_border_left).to.be.equal 4
-    expect(p.min_border_right).to.be.equal 5
-
-  it.skip "should add the title to the list of renderers", ->
-    # TODO(bird) Write this test.
-    null
 
 describe "PlotCanvas.Model constraints", ->
 
   beforeEach ->
-    @test_doc = new Document()
-    test_plot = new Plot({x_range: new DataRange1d(), y_range: new DataRange1d(), toolbar: new Toolbar()})
-    test_plot.attach_document(@test_doc)
-    @test_plot_canvas = test_plot.plot_canvas()
+    test_doc = new Document()
+    plot = new Plot({
+      x_range: new Range1d({start: 0, end: 1})
+      y_range: new Range1d({start: 0, end: 1})
+      toolbar: new Toolbar()
+      title: null
+    })
+    plot.attach_document(test_doc)
+    @plot_canvas = new PlotCanvas({ 'plot': plot })
+    @plot_canvas.attach_document(test_doc)
 
   it "should return 20 constraints from _get_constant_constraints", ->
-    expect(@test_plot_canvas._get_constant_constraints().length).to.be.equal 20
+    expect(@plot_canvas._get_constant_constraints().length).to.be.equal 20
 
   it "should return 0 constraints from _get_side_constraints if there are no side renderers", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
+    expect(@plot_canvas._get_side_constraints().length).to.be.equal 0
+
+  it "should call _get_side_constraints, _get_constant_constraints", ->
+    @plot_canvas._get_side_constraints = sinon.spy()
+    @plot_canvas._get_constant_constraints = sinon.spy()
+    expect(@plot_canvas._get_side_constraints.callCount).to.be.equal 0
+    expect(@plot_canvas._get_constant_constraints.callCount).to.be.equal 0
+    @plot_canvas.get_constraints()
+    expect(@plot_canvas._get_side_constraints.callCount).to.be.equal 1
+    expect(@plot_canvas._get_constant_constraints.callCount).to.be.equal 1
+
+  it "should call _get_constraints on children", ->
+    children = @plot_canvas.get_layoutable_children()
+    expect(children.length).to.be.equal 6
+    for child in children
+      child.get_constraints = sinon.spy()
+      expect(child.get_constraints.callCount).to.be.equal 0
+    @plot_canvas.get_constraints()
+    for child in children
+      expect(child.get_constraints.callCount).to.be.equal 1
+
+describe "PlotCanvas.Model constraints with different layouts", ->
+
+  beforeEach ->
+    @doc = new Document()
+    @plot = new Plot({
+      x_range: new Range1d({start: 0, end: 1})
+      y_range: new Range1d({start: 0, end: 1})
+      toolbar: new Toolbar()
+      title: null
+    })
 
   it "should return 2 constraints from _get_side_constraints if there is one side renderer on above", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'above')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 2
+    @plot.add_layout(new LinearAxis(), 'above')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 2
+
+  it "should return 3 constraints from _get_side_constraints if there are two side renderers on one side", ->
+    @plot.add_layout(new LinearAxis(), 'left')
+    @plot.add_layout(new LinearAxis(), 'left')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 3
 
   it "should return 2 constraints from _get_side_constraints if there is one side renderer on below", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'below')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 2
+    @plot.add_layout(new LinearAxis(), 'below')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 2
 
   it "should return 2 constraints from _get_side_constraints if there is one side renderer on left", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'left')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 2
+    @plot.add_layout(new LinearAxis(), 'left')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 2
 
   it "should return 2 constraints from _get_side_constraints if there is one side renderer on right", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'right')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 2
+    @plot.add_layout(new LinearAxis(), 'right')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 2
 
   it "should return 4 constraints from _get_side_constraints if there are two side renderers", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'left')
-    @test_plot_canvas.add_layout(new LinearAxis(), 'right')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 4
-
-  it "should return 3 constraints from _get_side_constraints if there are two side renderers on one side", ->
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 0
-    @test_plot_canvas.add_layout(new LinearAxis(), 'left')
-    @test_plot_canvas.add_layout(new LinearAxis(), 'left')
-    expect(@test_plot_canvas._get_side_constraints().length).to.be.equal 3
+    @plot.add_layout(new LinearAxis(), 'left')
+    @plot.add_layout(new LinearAxis(), 'right')
+    @plot.attach_document(@doc)
+    plot_canvas = new PlotCanvas({ 'plot': @plot })
+    expect(plot_canvas._get_side_constraints().length).to.be.equal 4
 
-  it "should call _get_side_constraints, _get_constant_constraints", ->
-    @test_plot_canvas._get_side_constraints = sinon.spy()
-    @test_plot_canvas._get_constant_constraints = sinon.spy()
-    expect(@test_plot_canvas._get_side_constraints.callCount).to.be.equal 0
-    expect(@test_plot_canvas._get_constant_constraints.callCount).to.be.equal 0
-    @test_plot_canvas.get_constraints()
-    expect(@test_plot_canvas._get_side_constraints.callCount).to.be.equal 1
-    expect(@test_plot_canvas._get_constant_constraints.callCount).to.be.equal 1
-
-  it "should call _get_constraints on children", ->
-    children = @test_plot_canvas.get_layoutable_children()
-    expect(children.length).to.be.equal 6
-    for child in children
-      child.get_constraints = sinon.spy()
-      expect(child.get_constraints.callCount).to.be.equal 0
-    @test_plot_canvas.get_constraints()
-    for child in children
-      expect(child.get_constraints.callCount).to.be.equal 1
 
 describe "PlotCanvas.View render", ->
 
@@ -186,19 +189,20 @@ describe "PlotCanvas.View render", ->
     utils.stub_canvas()
     utils.stub_solver()
 
-    @test_doc = new Document()
-    @test_plot = new PlotCanvas({
+    doc = new Document()
+    plot = new Plot({
       x_range: new Range1d({start: 0, end: 1})
       y_range: new Range1d({start: 0, end: 1})
       toolbar: new Toolbar()
     })
-    @test_plot.document = @test_doc
-    @test_plot._doc_attached()
-    @test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
+    plot.attach_document(doc)
+    plot_canvas = new PlotCanvas({ 'plot': plot })
+    plot_canvas.attach_document(doc)
+    @plot_canvas_view = new plot_canvas.default_view({ 'model': plot_canvas })
 
   it "should call own update_constraints method", ->
-    spy = sinon.spy(@test_plot_view, 'update_constraints')
-    @test_plot_view.render()
+    spy = sinon.spy(@plot_canvas_view, 'update_constraints')
+    @plot_canvas_view.render()
     expect(spy.calledOnce).to.be.true
 
 describe "PlotCanvas.View resize", ->
@@ -220,56 +224,57 @@ describe "PlotCanvas.View resize", ->
     solver_stubs = utils.stub_solver()
     @solver_suggest = solver_stubs['suggest']
 
-    @test_doc = new Document()
-    @test_plot = new PlotCanvas({
+    doc = new Document()
+    plot = new Plot({
       x_range: new Range1d({start: 0, end: 1})
       y_range: new Range1d({start: 0, end: 1})
       toolbar: new Toolbar()
     })
-    @test_plot.document = @test_doc
-    @test_plot._doc_attached()
-    @test_plot._dom_left = {_value: dom_left}
-    @test_plot._dom_top = {_value: dom_top}
-    @test_plot._width = {_value: width}
-    @test_plot._height = {_value: height}
-    @test_plot._whitespace_left = {_value: wl}
-    @test_plot._whitespace_right = {_value: wr}
-    @test_plot._whitespace_top = {_value: wt}
-    @test_plot._whitespace_bottom = {_value: wb}
-    @test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
+    plot.attach_document(doc)
+    @plot_canvas = new PlotCanvas({ 'plot': plot })
+    @plot_canvas.attach_document(doc)
+    @plot_canvas._dom_left = {_value: dom_left}
+    @plot_canvas._dom_top = {_value: dom_top}
+    @plot_canvas._width = {_value: width}
+    @plot_canvas._height = {_value: height}
+    @plot_canvas._whitespace_left = {_value: wl}
+    @plot_canvas._whitespace_right = {_value: wr}
+    @plot_canvas._whitespace_top = {_value: wt}
+    @plot_canvas._whitespace_bottom = {_value: wb}
+    @plot_canvas_view = new @plot_canvas.default_view({ 'model': @plot_canvas })
 
   it "should set the appropriate positions and paddings on the element", ->
-    @test_plot.sizing_mode = 'stretch_both'
-    @test_plot_view.resize()
+    @plot_canvas.sizing_mode = 'stretch_both'
+    @plot_canvas_view.resize()
     expected_style = "position: absolute; left: #{dom_left}px; top: #{dom_top}px; width: #{width}px; height: #{height}px;"
-    expect(@test_plot_view.$el.attr('style')).to.be.equal expected_style
+    expect(@plot_canvas_view.$el.attr('style')).to.be.equal expected_style
 
   it "should call canvas.set_dims with width & height if sizing_mode is box, and trigger true", ->
-    spy = sinon.spy(@test_plot_view.canvas_view, 'set_dims')
-    @test_plot.sizing_mode = 'stretch_both'
-    @test_plot_view.resize()
+    spy = sinon.spy(@plot_canvas_view.canvas_view, 'set_dims')
+    @plot_canvas.sizing_mode = 'stretch_both'
+    @plot_canvas_view.resize()
     expect(spy.calledOnce).to.be.true
     expect(spy.calledWith([width, height], true)).to.be.true
 
   it "should call canvas.set_dims and trigger if plot is_root", ->
-    spy = sinon.spy(@test_plot_view.canvas_view, 'set_dims')
-    @test_plot._is_root = true
-    @test_plot.sizing_mode = 'stretch_both'
-    @test_plot_view.resize()
+    spy = sinon.spy(@plot_canvas_view.canvas_view, 'set_dims')
+    @plot_canvas._is_root = true
+    @plot_canvas.sizing_mode = 'stretch_both'
+    @plot_canvas_view.resize()
     expect(spy.calledOnce).to.be.true
     expect(spy.calledWith([width, height], true)).to.be.true
 
   it "should call solver.suggest_value for width and height if sizing_mode is fixed", ->
-    spy = sinon.spy(@test_plot_view.canvas_view, 'set_dims')
-    @test_plot.sizing_mode = 'fixed'
-    @test_plot_view.resize()
+    spy = sinon.spy(@plot_canvas_view.canvas_view, 'set_dims')
+    @plot_canvas.sizing_mode = 'fixed'
+    @plot_canvas_view.resize()
     expect(spy.calledOnce, 'set_dims was not called').to.be.true
     expect(spy.calledWith([width, height], true)).to.be.true
 
   it "should throw an error if height is 0", ->
-    @test_plot._height = {_value: 0}
-    @test_plot.sizing_mode = 'stretch_both'
-    expect(@test_plot_view.resize).to.throw Error
+    @plot_canvas._height = {_value: 0}
+    @plot_canvas.sizing_mode = 'stretch_both'
+    expect(@plot_canvas_view.resize).to.throw Error
 
 
 describe "PlotCanvas.View update_constraints", ->
@@ -284,14 +289,15 @@ describe "PlotCanvas.View update_constraints", ->
     @solver_suggest_stub = solver_stubs['suggest']
     @solver_update_stub = solver_stubs['update']
 
-    @test_doc = new Document()
-    @test_plot = new PlotCanvas({
+    doc = new Document()
+    plot = new Plot({
       x_range: new Range1d({start: 0, end: 1})
       y_range: new Range1d({start: 0, end: 1})
       toolbar: new Toolbar()
     })
-    @test_plot.document = @test_doc
-    @test_plot._doc_attached()
+    plot.attach_document(doc)
+    @plot_canvas = new PlotCanvas({ 'plot': plot })
+    @plot_canvas.attach_document(doc)
 
   #it "should call SidePanel update_constraints with axis view as argument", ->
   #  ticker = new BasicTicker()
@@ -305,23 +311,23 @@ describe "PlotCanvas.View update_constraints", ->
   #  test_plot_view.update_constraints()
   #  expect(spy.calledOnce).to.be.true
 
-  it "should call solver suggest twice for frame size", ->
-    test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
+  it "should call solver suggest twice for frame sizing", ->
+    test_plot_canvas_view = new @plot_canvas.default_view({ 'model': @plot_canvas })
 
     initial_count = @solver_suggest_stub.callCount
-    test_plot_view.update_constraints()
+    test_plot_canvas_view.update_constraints()
     expect(@solver_suggest_stub.callCount).to.be.equal initial_count + 2
 
   it "should call solver update_variables with false for trigger", ->
-    test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
+    test_plot_canvas_view = new @plot_canvas.default_view({ 'model': @plot_canvas })
 
     initial_count = @solver_update_stub.callCount
-    test_plot_view.update_constraints()
+    test_plot_canvas_view.update_constraints()
     expect(@solver_update_stub.calledWith(false)).to.be.true
     expect(@solver_update_stub.callCount).to.be.equal initial_count + 1
 
 
-describe "Plot.View get_canvas_element", ->
+describe "PlotCanvas.View get_canvas_element", ->
 
   afterEach ->
     utils.unstub_canvas()
@@ -331,18 +337,19 @@ describe "Plot.View get_canvas_element", ->
     utils.stub_canvas()
     utils.stub_solver()
 
-    @test_doc = new Document()
-    @test_plot = new PlotCanvas({
+    doc = new Document()
+    plot = new Plot({
       x_range: new Range1d({start: 0, end: 1})
       y_range: new Range1d({start: 0, end: 1})
       toolbar: new Toolbar()
     })
-    @test_plot.document = @test_doc
-    @test_plot._doc_attached()
-    @test_plot_view = new @test_plot.default_view({ 'model': @test_plot })
+    plot.attach_document(doc)
+    plot_canvas = new PlotCanvas({ 'plot': plot })
+    plot_canvas.attach_document(doc)
+    @plot_canvas_view = new plot_canvas.default_view({ 'model': plot_canvas })
 
   it "should exist because get_canvas_element depends on it", ->
-    expect(@test_plot_view.canvas_view.ctx).to.exist
+    expect(@plot_canvas_view.canvas_view.ctx).to.exist
 
   it "should exist to grab the canvas DOM element using canvas_view.ctx", ->
-    expect(@test_plot_view.get_canvas_element).to.exist
+    expect(@plot_canvas_view.canvas_view.get_canvas_element).to.exist
diff --git a/bokehjs/test/models/tools/inspectors/crosshair_tool.coffee b/bokehjs/test/models/tools/inspectors/crosshair_tool.coffee
--- a/bokehjs/test/models/tools/inspectors/crosshair_tool.coffee
+++ b/bokehjs/test/models/tools/inspectors/crosshair_tool.coffee
@@ -22,10 +22,6 @@ describe "Crosshair Tool", ->
       @p = new Plot({x_range: @x_range, y_range: @y_range, toolbar: toolbar})
 
     it "should add two new spans to the plot_canvas synthetic_renderers", ->
-      plot_canvas = @p.plot_canvas()
-      # Plot canvas has no renderers
-      expect(plot_canvas.renderers.length).to.be.equal 0
-
       crosshair = new CrosshairTool({plot: @p})
       spans = [crosshair.spans.width, crosshair.spans.height]
       # Plot canvas should now have the two cross hair span renderers