Repository: bokeh/bokeh

Issue:
DataRange1d.rangepadding should allow "fixed" values
Currently it's value is interpreted as a percent. Also, `range` prefix is unnecessary and it should allow to set different padding on both sides.



Discussion:
I don't like the word padding in this context either but I am not sure of a better one off the top of my head. Just a reminder, plan for `DataRange1d` is to be the thing that "queries" all the renderers to ask for their preferred bounds and union them all together to obtain the global preferred bound. So we will keep `DataRange1d` but its implementation and execution will change entirely (no recourse to data sources at all, only to renderers)

I know that `DataRange1d` will be replaced, but I'm pretty sure that "padding" will stay in this or the other form.


Resolving patch:
diff --git a/bokeh/core/enums.py b/bokeh/core/enums.py
--- a/bokeh/core/enums.py
+++ b/bokeh/core/enums.py
@@ -258,3 +258,6 @@ def accept_left_right_center(value):
 
 #: Legend's click policies
 LegendClickPolicy = enumeration("none", "hide", "mute")
+
+#: Whether range padding should be interpreted a percentage or and absolute quantity
+PaddingUnits = enumeration("percent", "absolute")
diff --git a/bokeh/models/ranges.py b/bokeh/models/ranges.py
--- a/bokeh/models/ranges.py
+++ b/bokeh/models/ranges.py
@@ -5,7 +5,7 @@
 '''
 from __future__ import absolute_import
 
-from ..core.enums import StartEnd
+from ..core.enums import PaddingUnits, StartEnd
 from ..core.has_props import abstract
 from ..core.properties import (Auto, Bool, Datetime, Either, Enum, Float, Instance, Int,
                                List, MinMaxBounds, String, TimeDelta)
@@ -113,8 +113,18 @@ class DataRange1d(DataRange):
     '''
 
     range_padding = Float(default=0.1, help="""
-    A fraction of the total range size to add as padding to
-    the range start and end.
+    How much padding to add around the computed data bounds.
+
+    When ``range_padding_units`` is set to ``"percent"``, the span of the
+    range span is expanded to make the range ``range_padding`` percent larger.
+
+    When ``range_padding_units`` is set to ``"absolute"``, the start and end
+    of the range span are extended by the amount ``range_padding``.
+    """)
+
+    range_padding_units = Enum(PaddingUnits, default="percent", help="""
+    Whether the ``range_padding`` should be interpreted as a percentage, or
+    as an absolute quantity. (default: ``"percent"``)
     """)
 
     start = Float(help="""
@@ -131,15 +141,17 @@ class DataRange1d(DataRange):
     The bounds that the range is allowed to go to - typically used to prevent
     the user from panning/zooming/etc away from the data.
 
-    By default, the bounds will be None, allowing your plot to pan/zoom as far as you want.
-    If bounds are 'auto' they will be computed to be the same as the start and end of the DataRange1d.
+    By default, the bounds will be None, allowing your plot to pan/zoom as far
+    as you want. If bounds are 'auto' they will be computed to be the same as
+    the start and end of the DataRange1d.
 
-    Bounds are provided as a tuple of ``(min, max)`` so regardless of whether your range is
-    increasing or decreasing, the first item should be the minimum value of the range and the
-    second item should be the maximum. Setting min > max will result in a ``ValueError``.
+    Bounds are provided as a tuple of ``(min, max)`` so regardless of whether
+    your range is increasing or decreasing, the first item should be the
+    minimum value of the range and the second item should be the maximum.
+    Setting ``min > max`` will result in a ``ValueError``.
 
-    If you only want to constrain one end of the plot, you can set min or max to
-    ``None`` e.g. ``DataRange1d(bounds=(None, 12))``
+    If you only want to constrain one end of the plot, you can set ``min`` or
+    ``max`` to ``None`` e.g. ``DataRange1d(bounds=(None, 12))``
     """)
 
     min_interval = Float(default=None, help="""
@@ -172,7 +184,8 @@ class DataRange1d(DataRange):
     If set to ``None`` (default), then auto-ranging does not follow, and
     the range will encompass both the minimum and maximum data values.
 
-    ``follow`` cannot be used with bounds, and if set, bounds will be set to ``None``.
+    ``follow`` cannot be used with bounds, and if set, bounds will be set to
+    ``None``.
     """)
 
     follow_interval = Float(default=None, help="""


Test patch:
diff --git a/bokeh/core/tests/test_enums.py b/bokeh/core/tests/test_enums.py
--- a/bokeh/core/tests/test_enums.py
+++ b/bokeh/core/tests/test_enums.py
@@ -62,6 +62,7 @@ def test_enums_contents():
         'NamedColor',
         'NumeralLanguage',
         'Orientation',
+        'PaddingUnits',
         'Palette',
         'RenderLevel',
         'RenderMode',
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
@@ -78,11 +78,13 @@ describe "datarange1d module", ->
     it "should reset configuration to initial values", ->
       r = new DataRange1d()
       r.range_padding = 0.2
+      r.range_padding_units = 'absolute'
       r.follow = 'end'
       r.follow_interval = 10
       r.default_span = 10
       r.reset()
       expect(r.range_padding).to.be.equal 0.1
+      expect(r.range_padding_units).to.be.equal "percent"
       expect(r.follow).to.be.null
       expect(r.follow_interval).to.be.null
       expect(r.default_span).to.be.equal 2
@@ -201,17 +203,26 @@ describe "datarange1d module", ->
       expect(r._compute_range(1, 3)).to.be.deep.equal [3, 1]
       expect(r._compute_range(1, 7)).to.be.deep.equal [5, 1]
 
-    it "should apply range_padding", ->
+    it "should apply percentage range_padding", ->
       r = new DataRange1d()
       r.range_padding = 0.5
       expect(r._compute_range(1, 3)).to.be.deep.equal [0.5, 3.5]
 
+    it "should apply absolute range_padding", ->
+      r = new DataRange1d()
+      r.range_padding = 0.2
+      r.range_padding_units = "absolute"
+      expect(r._compute_range(1, 3)).to.be.deep.equal [0.8, 3.2]
+
     it "should apply range_padding logly when mapper_hint='log'", ->
       r = new DataRange1d()
       r.range_padding = 0.5
       r.mapper_hint = "log"
       expect(r._compute_range(0.01, 10)).to.be.deep.equal [0.0017782794100389264, 56.23413251903488]
 
+      r.range_padding_units = "absolute"
+      expect(r._compute_range(1, 10)).to.be.deep.equal [0.5000000000000001, 10.5]
+
   describe "_compute_min_max", ->
 
     it "should compute max/min for dimension of a single plot_bounds", ->