Repository: bokeh/bokeh

Issue:
Plot not shown if DatetimeTickFormatter partially defined
A plot may not be shown if formats aren't provided for all time scales.

This is reproduced by the following code:

``` python
from datetime import datetime as dt
from bokeh.models.formatters import DatetimeTickFormatter
from bokeh.plotting import Figure, output_file, show

output_file('bokeh.html')

p = Figure(title='Demo', x_axis_type='datetime')

x = [dt(2016, 1, 1, 0, 0, 0), dt(2016, 1, 1, 0, 0, 10)]
print(x[1].day, x[1].hour, x[1].minute, x[1].second)
y = [1, 2]

p.scatter(x=x, y=y)

p.xaxis.formatter = DatetimeTickFormatter(formats=dict(
    seconds=["%H:%M:%S"],
    # a plot is displayed if the following lines are *not* commented out
    # minsec=["%H:%M:%S"],
    # minutes=["%H:%M:%S"],
    # hourmin=["%H:%M:%S"],
)
)

show(p)
```

This has been tested with:

Python version      :  3.5.2 (default, Sep 25 2016, 19:32:42) 
IPython version     :  Not installed
Bokeh version       :  0.12.2



Discussion:
All datetime scales for `DatetimeTickFormatter` always have to be defined. We could probably add a validation check and error if this condition is not satisfied, but the condition will always be necessary. You can create a new dict with a copy of the defaults (that you can update however you like) by calling the function  [`bokeh.models.formatters.DEFAULT_DATETIME_FORMATS`](https://github.com/bokeh/bokeh/blob/master/bokeh/models/formatters.py#L285-L296). Alternatively you can construct your own dict that has all those keys. 

Thanks! A validation check would be great. Alternatively (or in addition) you could perhaps make this clearer in the documentation - at the moment http://bokeh.pydata.org/en/latest/docs/reference/models/formatters.html has an example which _doesn't_ include all datetime scales. :-)

Alternatively, properly defined `DatetimeTickFormatter` shouldn't have `formats` field, but each `format`'s key should be `DatetimeTickFormatter`'s field. Especially that `formats` is a dict that fakes a struct.


Resolving patch:
diff --git a/bokeh/models/formatters.py b/bokeh/models/formatters.py
--- a/bokeh/models/formatters.py
+++ b/bokeh/models/formatters.py
@@ -12,8 +12,9 @@
 from ..core.properties import abstract
 from ..core.properties import (Bool, Int, String, Enum, Auto, List, Dict,
     Either, Instance)
-from ..core.enums import DatetimeUnits, RoundingFunction, NumeralLanguage
+from ..core.enums import RoundingFunction, NumeralLanguage
 from ..util.dependencies import import_required
+from ..util.deprecation import deprecated
 from ..util.compiler import nodejs_compile, CompilationError
 
 @abstract
@@ -282,59 +283,54 @@ def from_coffeescript(cls, code, args={}):
 
     """)
 
-DEFAULT_DATETIME_FORMATS = lambda : {
-    'microseconds': ['%fus'],
-    'milliseconds': ['%3Nms', '%S.%3Ns'],
-    'seconds':      ['%Ss'],
-    'minsec':       [':%M:%S'],
-    'minutes':      [':%M', '%Mm'],
-    'hourmin':      ['%H:%M'],
-    'hours':        ['%Hh', '%H:%M'],
-    'days':         ['%m/%d', '%a%d'],
-    'months':       ['%m/%Y', '%b%y'],
-    'years':        ['%Y'],
-}
+def DEFAULT_DATETIME_FORMATS():
+    deprecated((0, 12, 4), 'DEFAULT_DATETIME_FORMATS', 'individual DatetimeTickFormatter fields')
+    return {
+        'microseconds': ['%fus'],
+        'milliseconds': ['%3Nms', '%S.%3Ns'],
+        'seconds':      ['%Ss'],
+        'minsec':       [':%M:%S'],
+        'minutes':      [':%M', '%Mm'],
+        'hourmin':      ['%H:%M'],
+        'hours':        ['%Hh', '%H:%M'],
+        'days':         ['%m/%d', '%a%d'],
+        'months':       ['%m/%Y', '%b%y'],
+        'years':        ['%Y'],
+    }
+
+def _DATETIME_TICK_FORMATTER_HELP(field):
+    return """
+    Formats for displaying datetime values in the %s range.
+
+    See the :class:`~bokeh.models.formatters.DatetimeTickFormatter` help for a list of all supported formats.
+    """ % field
 
 class DatetimeTickFormatter(TickFormatter):
-    """ Display tick values from a continuous range as formatted
-    datetimes.
-
-    """
-
-    formats = Dict(Enum(DatetimeUnits), List(String), default=DEFAULT_DATETIME_FORMATS, help="""
-    User defined formats for displaying datetime values.
-
-    The enum values correspond roughly to different "time scales". The
-    corresponding value is a list of `strftime`_ formats to use for
+    """ A ``TickFormatter`` for displaying datetime values nicely across a
+    range of scales.
+
+    ``DatetimeTickFormatter`` has the following properties for setting formats
+    at different scales scales:
+
+    * ``microseconds``
+    * ``milliseconds``
+    * ``seconds``
+    * ``minsec``
+    * ``minutes``
+    * ``hourmin``
+    * ``hours``
+    * ``days``
+    * ``months``
+    * ``years``
+
+    Each scale property can be set to format or list of formats to use for
     formatting datetime tick values that fall in in that "time scale".
-
     By default, only the first format string passed for each time scale
     will be used. By default, all leading zeros are stripped away from
-    the formatted labels. These behaviors cannot be changed as of now.
-
-    An example of specifying the same date format over a range of time scales::
-
-        DatetimeTickFormatter(
-            formats=dict(
-                hours=["%B %Y"],
-                days=["%B %Y"],
-                months=["%B %Y"],
-                years=["%B %Y"],
-            )
-        )
+    the formatted labels.
 
     This list of supported `strftime`_ formats is reproduced below.
 
-
-    .. warning::
-        The client library BokehJS uses the `timezone`_ library to
-        format datetimes. The inclusion of the list below is based on the
-        claim that `timezone`_ makes to support "the full compliment
-        of GNU date format specifiers." However, this claim has not
-        been tested exhaustively against this list. If you find formats
-        that do not function as expected, please submit a `github issue`_,
-        so that the documentation can be updated appropriately.
-
     %a
         The abbreviated name of the day of the week according to the
         current locale.
@@ -505,8 +501,92 @@ class DatetimeTickFormatter(TickFormatter):
     %%
         A literal '%' character.
 
+    .. warning::
+        The client library BokehJS uses the `timezone`_ library to
+        format datetimes. The inclusion of the list below is based on the
+        claim that `timezone`_ makes to support "the full compliment
+        of GNU date format specifiers." However, this claim has not
+        been tested exhaustively against this list. If you find formats
+        that do not function as expected, please submit a `github issue`_,
+        so that the documentation can be updated appropriately.
+
     .. _strftime: http://man7.org/linux/man-pages/man3/strftime.3.html
     .. _timezone: http://bigeasy.github.io/timezone/
     .. _github issue: https://github.com/bokeh/bokeh/issues
 
-    """)
+    """
+    microseconds = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``microseconds``"),
+                        default=['%fus']).accepts(String, lambda fmt: [fmt])
+
+    milliseconds = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``milliseconds``"),
+                        default=['%3Nms', '%S.%3Ns']).accepts(String, lambda fmt: [fmt])
+
+    seconds      = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``seconds``"),
+                        default=['%Ss']).accepts(String, lambda fmt: [fmt])
+
+    minsec       = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``minsec`` (for combined minutes and seconds)"),
+                        default=[':%M:%S']).accepts(String, lambda fmt: [fmt])
+
+    minutes      = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``minutes``"),
+                        default=[':%M', '%Mm']).accepts(String, lambda fmt: [fmt])
+
+    hourmin      = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``hourmin`` (for combined hours and minutes)"),
+                        default=['%H:%M']).accepts(String, lambda fmt: [fmt])
+
+    hours        = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``hours``"),
+                        default=['%Hh', '%H:%M']).accepts(String, lambda fmt: [fmt])
+
+    days         = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``days``"),
+                        default=['%m/%d', '%a%d']).accepts(String, lambda fmt: [fmt])
+
+    months       = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``months``"),
+                        default=['%m/%Y', '%b%y']).accepts(String, lambda fmt: [fmt])
+
+    years        = List(String,
+                        help=_DATETIME_TICK_FORMATTER_HELP("``years``"),
+                        default=['%Y']).accepts(String, lambda fmt: [fmt])
+
+    __deprecated_attributes__ = ('formats',)
+
+    @property
+    def formats(self):
+        ''' A dictionary containing formats for all scales.
+
+        THIS PROPERTY IS DEPRECTATED. Use individual DatetimeTickFormatter fields instead.
+
+        '''
+        deprecated((0, 12, 4), 'DatetimeTickFormatter.formats', 'individual DatetimeTickFormatter fields')
+        return dict(
+            microseconds = self.microseconds,
+            milliseconds = self.milliseconds,
+            seconds      = self.seconds,
+            minsec       = self.minsec,
+            minutes      = self.minutes,
+            hourmin      = self.hourmin,
+            hours        = self.hours,
+            days         = self.days,
+            months       = self.months,
+            years        = self.years)
+
+    @formats.setter
+    def formats(self, value):
+        deprecated((0, 12, 4), 'DatetimeTickFormatter.formats', 'individual DatetimeTickFormatter fields')
+        if 'microseconds' in value: self.microseconds = value['microseconds']
+        if 'milliseconds' in value: self.milliseconds = value['milliseconds']
+        if 'seconds'      in value: self.seconds      = value['seconds']
+        if 'minsec'       in value: self.minsec       = value['minsec']
+        if 'minutes'      in value: self.minutes      = value['minutes']
+        if 'hourmin'      in value: self.hourmin      = value['hourmin']
+        if 'hours'        in value: self.hours        = value['hours']
+        if 'days'         in value: self.days         = value['days']
+        if 'months'       in value: self.months       = value['months']
+        if 'years'        in value: self.years        = value['years']
diff --git a/examples/models/daylight.py b/examples/models/daylight.py
--- a/examples/models/daylight.py
+++ b/examples/models/daylight.py
@@ -75,7 +75,7 @@
 text = Text(x="dates", y="times", text="texts", text_align="center")
 plot.add_glyph(text_source, text)
 
-xformatter = DatetimeTickFormatter(formats=dict(months=["%b %Y"]))
+xformatter = DatetimeTickFormatter(months="%b %Y")
 xaxis = DatetimeAxis(formatter=xformatter)
 plot.add_layout(xaxis, 'below')
 


Test patch:
diff --git a/bokeh/models/tests/test_glyphs.py b/bokeh/models/tests/test_glyphs.py
--- a/bokeh/models/tests/test_glyphs.py
+++ b/bokeh/models/tests/test_glyphs.py
@@ -37,7 +37,7 @@
     FontStyle,
     TextAlign, TextBaseline,
     Direction,
-    Units, AngleUnits, DatetimeUnits,
+    Units, AngleUnits,
     Dimension,
     Anchor, Location, LegendLocation,
     DashPattern,
@@ -46,7 +46,7 @@
 
 # fool flake8
 (LineJoin, LineDash, LineCap, FontStyle, TextAlign, TextBaseline, Direction,
- Units, AngleUnits, DatetimeUnits, Dimension, Anchor, Location, LegendLocation,
+ Units, AngleUnits, Dimension, Anchor, Location, LegendLocation,
  DashPattern, ButtonType, MapType, Color, NamedIcon)