Repository: bokeh/bokeh

Issue:
Option to rotate additional categorical ticks
Currently only the categorical ticks immediately next the axis can be rotated. Add options to rotate higher level categories as well. 

This has come up several times now on SO and the ML list, I can't find that anyone has made an issue yet, though I have asked. 


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
@@ -256,5 +256,8 @@ def enumeration(*values, **kwargs):
 #: Whether range padding should be interpreted a percentage or and absolute quantity
 PaddingUnits = enumeration("percent", "absolute")
 
+#: Specify how axis tick labels are oriented with respect to the axis
+TickLabelOrientation = enumeration("horizontal", "vertical", "parallel", "normal")
+
 #: Specify how a format string for a tooltip field should be interpreted
 TooltipFieldFormatter = enumeration("numeral", "datetime", "printf")
diff --git a/bokeh/models/axes.py b/bokeh/models/axes.py
--- a/bokeh/models/axes.py
+++ b/bokeh/models/axes.py
@@ -4,6 +4,7 @@
 '''
 from __future__ import absolute_import
 
+from ..core.enums import TickLabelOrientation
 from ..core.has_props import abstract
 from ..core.properties import Auto, Datetime, Dict, Either, Enum, Float, Include, Instance, Int, Override, Seq, String, Tuple
 from ..core.property_mixins import LineProps, TextProps
@@ -171,8 +172,11 @@ class LogAxis(ContinuousAxis):
     formatter = Override(default=lambda: LogTickFormatter())
 
 class CategoricalAxis(Axis):
-    ''' An axis that picks evenly spaced tick locations for a
-    collection of categories/factors.
+    ''' An axis that displays ticks and labels for categorical ranges.
+
+    The ``CategoricalAxis`` can handle factor ranges with up to two levels of
+    nesting, including drawing a seperator line between top-level groups of
+    factors.
 
     '''
     ticker = Override(default=lambda: CategoricalTicker())
@@ -181,14 +185,29 @@ class CategoricalAxis(Axis):
 
     separator_props = Include(LineProps, help="""
     The %s of the separator line between top-level categorical groups.
+
+    This property always applies to factors in the outermost level of nesting.
     """)
 
     separator_line_color = Override(default="lightgrey")
-
     separator_line_width = Override(default=2)
 
     group_props = Include(TextProps, help="""
-    The %s of the group top-level categorical groups.
+    The %s of the group categorical labels.
+
+    This property always applies to factors in the outermost level of nesting.
+    If the list of categorical factors is flat (i.e. no nesting) then this
+    property has no effect.
+    """)
+
+    group_label_orientation = Either(Enum(TickLabelOrientation), Float, default="parallel", help="""
+    What direction the group label text should be oriented.
+
+    If a number is supplied, the angle of the text is measured from horizontal.
+
+    This property always applies to factors in the outermost level of nesting.
+    If the list of categorical factors is flat (i.e. no nesting) then this
+    property has no effect.
     """)
 
     group_text_font_size = Override(default={'value': "8pt"})
@@ -196,13 +215,26 @@ class CategoricalAxis(Axis):
     group_text_color = Override(default="grey")
 
     subgroup_props = Include(TextProps, help="""
-    The %s of the group top-level categorical groups.
+    The %s of the subgroup categorical labels.
+
+    This property always applies to factors in the middle level of nesting.
+    If the list of categorical factors is has only zero or one levels of nesting,
+    then this property has no effect.
+    """)
+
+    subgroup_label_orientation = Either(Enum(TickLabelOrientation), Float, default="parallel", help="""
+    What direction the subgroup label text should be oriented.
+
+    If a number is supplied, the angle of the text is measured from horizontal.
+
+    This property always applies to factors in the middle level of nesting.
+    If the list of categorical factors is has only zero or one levels of nesting,
+    then this property has no effect.
     """)
 
     subgroup_text_font_size = Override(default={'value': "8pt"})
     subgroup_text_font_style = Override(default="bold")
 
-
 class DatetimeAxis(LinearAxis):
     ''' An LinearAxis that picks nice numbers for tick locations on
     a datetime scale. Configured with a ``DatetimeTickFormatter`` by
diff --git a/examples/integration/axes/cat_labels_rotate.py b/examples/integration/axes/cat_labels_rotate.py
new file mode 100644
--- /dev/null
+++ b/examples/integration/axes/cat_labels_rotate.py
@@ -0,0 +1,19 @@
+from bokeh.io import show
+from bokeh.layouts import column
+from bokeh.models import FactorRange
+from bokeh.plotting import figure
+
+xr = [('foo', 'bar'), ('foo', 'baz'), ('quux', 'bar')]
+p1 = figure(plot_height=300, x_range=FactorRange(*xr), toolbar_location=None)
+p1.xaxis.major_label_orientation = 1.2
+p1.xaxis.group_label_orientation = "normal"
+p1.vbar(x=xr, top=1, width=0.5)
+
+xr = [('A', 'foo', 'bar'), ('B', 'foo', 'baz'), ('B', 'quux', 'bar')]
+p2 = figure(plot_height=300, x_range=FactorRange(*xr), toolbar_location=None)
+p2.xaxis.major_label_orientation = 1.2
+p2.xaxis.subgroup_label_orientation = "vertical"
+p2.xaxis.group_label_orientation = 0.8
+p2.vbar(x=xr, top=1, width=0.5)
+
+show(column(p1, p2))


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
@@ -69,6 +69,7 @@ def test_enums_contents():
         'StepMode',
         'TextAlign',
         'TextBaseline',
+        'TickLabelOrientation',
         'TooltipAttachment',
         'TooltipFieldFormatter',
         'VerticalAlign',