Repository: pandas-dev/pandas

Issue:
API: Would it be useful for everyone if DataFrame.to_html (and to_string) had an argument to specify justify settings for each column?
related #3190

Currently it is only possible to specify justify settings for column headers.  This would extremely useful when used in conjunction with IPython Notebook.



Discussion:
i like this idea. want to submit a pr?

is there a standard way of doing this, e.g.

`justify = [('foo','left'),('bar','right')]`

or

`justify_left = ['foo'], justify_right = ['bar']`

or

`style = dict(justify_left = ['foo'], justify_right = ['bar'])`

?

@jreback i don't know. u don't think this is output-formatting tag material?

yes...but how to you specify to the function that you want to do this? there are other options in html as well (color e.g., that we may want to support in the future). Looking for a general API to pass `output-formatting` options. Which might be as simple as allowing `column_label_formatter, row_label_formatter, cell_formatter` as passed functions?

related https://github.com/pydata/pandas/issues/3190

but #3190 is long term and would require a significant time investment

It think pretty easy to pass a `style=Formatter()` where formatter is class that we define as a base class and advanced could sub-class. the engines then call out to format rows,columns,cells....

but agreed prob a bit of work to do correctly

someone could write a package and call it `framer` or `framed` to do this....

`Formatter` would work tho, while we don't have `framed`

or maybe `Style`

and `HTMLStyle`, `TextStyle`.....


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -577,6 +577,19 @@ def _repr_html_(self):
         else:
             return None
 
+    @property
+    def style(self):
+        """
+        Property returning a Styler object containing methods for
+        building a styled HTML representation fo the DataFrame.
+
+        See Also
+        --------
+        pandas.core.Styler
+        """
+        from pandas.core.style import Styler
+        return Styler(self)
+
     def iteritems(self):
         """
         Iterator over (column name, Series) pairs.
@@ -1518,6 +1531,7 @@ def to_html(self, buf=None, columns=None, col_space=None, colSpace=None,
                                            max_rows=max_rows,
                                            max_cols=max_cols,
                                            show_dimensions=show_dimensions)
+        # TODO: a generic formatter wld b in DataFrameFormatter
         formatter.to_html(classes=classes, notebook=notebook)
 
         if buf is None:
diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py
--- a/pandas/core/indexing.py
+++ b/pandas/core/indexing.py
@@ -524,7 +524,7 @@ def _align_series(self, indexer, ser, multiindex_indexer=False):
         Parameters
         ----------
         indexer : tuple, slice, scalar
-            The indexer used to get the locations that will be set to 
+            The indexer used to get the locations that will be set to
             `ser`
 
         ser : pd.Series
@@ -532,7 +532,7 @@ def _align_series(self, indexer, ser, multiindex_indexer=False):
 
         multiindex_indexer : boolean, optional
             Defaults to False. Should be set to True if `indexer` was from
-            a `pd.MultiIndex`, to avoid unnecessary broadcasting. 
+            a `pd.MultiIndex`, to avoid unnecessary broadcasting.
 
 
         Returns:
@@ -1806,3 +1806,46 @@ def maybe_droplevels(index, key):
             pass
 
     return index
+
+
+def _non_reducing_slice(slice_):
+    """
+    Ensurse that a slice doesn't reduce to a Series or Scalar.
+
+    Any user-paseed `subset` should have this called on it
+    to make sure we're always working with DataFrames.
+    """
+    # default to column slice, like DataFrame
+    # ['A', 'B'] -> IndexSlices[:, ['A', 'B']]
+    kinds = tuple(list(compat.string_types) +
+                  [ABCSeries, np.ndarray, Index, list])
+    if isinstance(slice_, kinds):
+        slice_ = IndexSlice[:, slice_]
+
+    def pred(part):
+        # true when slice does *not* reduce
+        return isinstance(part, slice) or com.is_list_like(part)
+
+    if not com.is_list_like(slice_):
+        if not isinstance(slice_, slice):
+            # a 1-d slice, like df.loc[1]
+            slice_ = [[slice_]]
+        else:
+            # slice(a, b, c)
+            slice_ = [slice_]  # to tuplize later
+    else:
+        slice_ = [part if pred(part) else [part] for part in slice_]
+    return tuple(slice_)
+
+
+def _maybe_numeric_slice(df, slice_, include_bool=False):
+    """
+    want nice defaults for background_gradient that don't break
+    with non-numeric data. But if slice_ is passed go with that.
+    """
+    if slice_ is None:
+        dtypes = [np.number]
+        if include_bool:
+            dtypes.append(bool)
+        slice_ = IndexSlice[:, df.select_dtypes(include=dtypes).columns]
+    return slice_
diff --git a/pandas/core/style.py b/pandas/core/style.py
new file mode 100644
--- /dev/null
+++ b/pandas/core/style.py
@@ -0,0 +1,720 @@
+"""
+Module for applying conditional formatting to
+DataFrames and Series.
+"""
+from functools import partial
+from contextlib import contextmanager
+from uuid import uuid1
+import copy
+from collections import defaultdict
+
+try:
+    from jinja2 import Template
+except ImportError:
+    msg = "pandas.Styler requires jinja2. "\
+          "Please install with `conda install Jinja2`\n"\
+          "or `pip install Jinja2`"
+    raise ImportError(msg)
+
+import numpy as np
+import pandas as pd
+from pandas.compat import lzip
+from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
+try:
+    import matplotlib.pyplot as plt
+    from matplotlib import colors
+    has_mpl = True
+except ImportError:
+    has_mpl = False
+    no_mpl_message = "{0} requires matplotlib."
+
+
+@contextmanager
+def _mpl(func):
+    if has_mpl:
+        yield plt, colors
+    else:
+        raise ImportError(no_mpl_message.format(func.__name__))
+
+
+class Styler(object):
+    """
+    Helps style a DataFrame or Series according to the
+    data with HTML and CSS.
+
+    .. versionadded:: 0.17.1
+
+    Parameters
+    ----------
+    data: Series or DataFrame
+    precision: int
+        precision to round floats to, defaults to pd.options.display.precision
+    table_styles: list-like, default None
+        list of {selector: (attr, value)} dicts; see Notes
+    uuid: str, default None
+        a unique identifier to avoid CSS collisons; generated automatically
+    caption: str, default None
+        caption to attach to the table
+
+    Attributes
+    ----------
+    tempate: Jinja Template
+
+    Notes
+    -----
+    Most styling will be done by passing style functions into
+    Styler.apply or Styler.applymap. Style functions should
+    return values with strings containing CSS 'attr: value' that will
+    be applied to the indicated cells.
+
+    If using in the Jupyter notebook, Styler has defined a _repr_html_
+    to automatically render itself. Otherwise call Styler.render to get
+    the genterated HTML.
+
+    See Also
+    --------
+    pandas.DataFrame.style
+    """
+    template = Template("""
+        <style  type="text/css" >
+        {% for s in table_styles %}
+            #T_{{uuid}} {{s.selector}} {
+            {% for p,val in s.props %}
+                {{p}}: {{val}};
+            {% endfor %}
+            }
+        {% endfor %}
+        {% for s in cellstyle %}
+            #T_{{uuid}}{{s.selector}} {
+            {% for p,val in s.props %}
+                {{p}}: {{val}};
+            {% endfor %}
+            }
+        {% endfor %}
+        </style>
+
+        <table id="T_{{uuid}}">
+        {% if caption %}
+            <caption>{{caption}}</caption>
+        {% endif %}
+
+        <thead>
+            {% for r in head %}
+            <tr>
+                {% for c in r %}
+                <{{c.type}} class="{{c.class}}">{{c.value}}
+                {% endfor %}
+            </tr>
+            {% endfor %}
+        </thead>
+        <tbody>
+            {% for r in body %}
+            <tr>
+                {% for c in r %}
+                <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}">
+                    {% if c.value is number %}
+                        {{c.value|round(precision)}}
+                    {% else %}
+                        {{c.value}}
+                    {% endif %}
+                {% endfor %}
+            </tr>
+            {% endfor %}
+        </tbody>
+        </table>
+        """)
+
+    def __init__(self, data, precision=None, table_styles=None, uuid=None,
+                 caption=None):
+        self.ctx = defaultdict(list)
+        self._todo = []
+
+        if not isinstance(data, (pd.Series, pd.DataFrame)):
+            raise TypeError
+        if data.ndim == 1:
+            data = data.to_frame()
+        if not data.index.is_unique or not data.columns.is_unique:
+            raise ValueError("style is not supported for non-unique indicies.")
+
+        self.data = data
+        self.index = data.index
+        self.columns = data.columns
+
+        self.uuid = uuid
+        self.table_styles = table_styles
+        self.caption = caption
+        if precision is None:
+            precision = pd.options.display.precision
+        self.precision = precision
+
+    def _repr_html_(self):
+        '''
+        Hooks into Jupyter notebook rich display system.
+        '''
+        return self.render()
+
+    def _translate(self):
+        """
+        Convert the DataFrame in `self.data` and the attrs from `_build_styles`
+        into a dictionary of {head, body, uuid, cellstyle}
+        """
+        table_styles = self.table_styles or []
+        caption = self.caption
+        ctx = self.ctx
+        precision = self.precision
+        uuid = self.uuid or str(uuid1()).replace("-", "_")
+        ROW_HEADING_CLASS = "row_heading"
+        COL_HEADING_CLASS = "col_heading"
+        DATA_CLASS = "data"
+        BLANK_CLASS = "blank"
+        BLANK_VALUE = ""
+
+        cell_context = dict()
+
+        n_rlvls = self.data.index.nlevels
+        n_clvls = self.data.columns.nlevels
+        rlabels = self.data.index.tolist()
+        clabels = self.data.columns.tolist()
+
+        idx_values = self.data.index.format(sparsify=False, adjoin=False,
+                                            names=False)
+        idx_values = lzip(*idx_values)
+
+        if n_rlvls == 1:
+            rlabels = [[x] for x in rlabels]
+        if n_clvls == 1:
+            clabels = [[x] for x in clabels]
+        clabels = list(zip(*clabels))
+
+        cellstyle = []
+        head = []
+
+        for r in range(n_clvls):
+            row_es = [{"type": "th", "value": BLANK_VALUE,
+                       "class": " ".join([BLANK_CLASS])}] * n_rlvls
+            for c in range(len(clabels[0])):
+                cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c]
+                cs.extend(cell_context.get(
+                    "col_headings", {}).get(r, {}).get(c, []))
+                row_es.append({"type": "th", "value": clabels[r][c],
+                               "class": " ".join(cs)})
+            head.append(row_es)
+
+        body = []
+        for r, idx in enumerate(self.data.index):
+            cs = [ROW_HEADING_CLASS, "level%s" % c, "row%s" % r]
+            cs.extend(cell_context.get(
+                "row_headings", {}).get(r, {}).get(c, []))
+            row_es = [{"type": "th",
+                       "value": rlabels[r][c],
+                       "class": " ".join(cs)}
+                      for c in range(len(rlabels[r]))]
+
+            for c, col in enumerate(self.data.columns):
+                cs = [DATA_CLASS, "row%s" % r, "col%s" % c]
+                cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
+                row_es.append({"type": "td", "value": self.data.iloc[r][c],
+                               "class": " ".join(cs), "id": "_".join(cs[1:])})
+                props = []
+                for x in ctx[r, c]:
+                    # have to handle empty styles like ['']
+                    if x.count(":"):
+                        props.append(x.split(":"))
+                    else:
+                        props.append(['', ''])
+                cellstyle.append(
+                    {'props': props,
+                     'selector': "row%s_col%s" % (r, c)}
+                )
+            body.append(row_es)
+
+        return dict(head=head, cellstyle=cellstyle, body=body, uuid=uuid,
+                    precision=precision, table_styles=table_styles,
+                    caption=caption)
+
+    def render(self):
+        """
+        Render the built up styles to HTML
+
+        .. versionadded:: 0.17.1
+
+        Returns
+        -------
+        rendered: str
+            the rendered HTML
+
+        Notes
+        -----
+        ``Styler`` objects have defined the ``_repr_html_`` method
+        which automatically calls ``self.render()`` when it's the
+        last item in a Notebook cell. When calling ``Styler.render()``
+        directly, wrap the resul in ``IPython.display.HTML`` to view
+        the rendered HTML in the notebook.
+        """
+        self._compute()
+        d = self._translate()
+        # filter out empty styles, every cell will have a class
+        # but the list of props may just be [['', '']].
+        # so we have the neested anys below
+        trimmed = [x for x in d['cellstyle'] if
+                    any(any(y) for y in x['props'])]
+        d['cellstyle'] = trimmed
+        return self.template.render(**d)
+
+    def _update_ctx(self, attrs):
+        """
+        update the state of the Styler. Collects a mapping
+        of {index_label: ['<property>: <value>']}
+
+        attrs: Series or DataFrame
+        should contain strings of '<property>: <value>;<prop2>: <val2>'
+        Whitespace shouldn't matter and the final trailing ';' shouldn't
+        matter.
+        """
+        for row_label, v in attrs.iterrows():
+            for col_label, col in v.iteritems():
+                i = self.index.get_indexer([row_label])[0]
+                j = self.columns.get_indexer([col_label])[0]
+                for pair in col.rstrip(";").split(";"):
+                    self.ctx[(i, j)].append(pair)
+
+    def _copy(self, deepcopy=False):
+        styler = Styler(self.data, precision=self.precision,
+                        caption=self.caption, uuid=self.uuid,
+                        table_styles=self.table_styles)
+        if deepcopy:
+            styler.ctx = copy.deepcopy(self.ctx)
+            styler._todo = copy.deepcopy(self._todo)
+        else:
+            styler.ctx = self.ctx
+            styler._todo = self._todo
+        return styler
+
+    def __copy__(self):
+        """
+        Deep copy by default.
+        """
+        return self._copy(deepcopy=False)
+
+    def __deepcopy__(self, memo):
+        return self._copy(deepcopy=True)
+
+    def clear(self):
+        self.ctx.clear()
+        self._todo = []
+
+    def _compute(self):
+        '''
+        Execute the style functions built up in `self._todo`.
+
+        Relies on the conventions that all style functions go through
+        .apply or .applymap. The append styles to apply as tuples of
+
+        (application method, *args, **kwargs)
+        '''
+        r = self
+        for func, args, kwargs in self._todo:
+            r = func(self)(*args, **kwargs)
+        return r
+
+    def _apply(self, func, axis=0, subset=None, **kwargs):
+        subset = slice(None) if subset is None else subset
+        subset = _non_reducing_slice(subset)
+        if axis is not None:
+            result = self.data.loc[subset].apply(func, axis=axis, **kwargs)
+        else:
+            # like tee
+            result = func(self.data.loc[subset], **kwargs)
+        self._update_ctx(result)
+        return self
+
+    def apply(self, func, axis=0, subset=None, **kwargs):
+        """
+        Apply a function column-wise, row-wise, or table-wase,
+        updating the HTML representation with the result.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        func: function
+        axis: int, str or None
+            apply to each column (``axis=0`` or ``'index'``)
+            or to each row (``axis=1`` or ``'columns'``) or
+            to the entire DataFrame at once with ``axis=None``.
+        subset: IndexSlice
+            a valid indexer to limit ``data`` to *before* applying the
+            function. Consider using a pandas.IndexSlice
+        kwargs: dict
+            pass along to ``func``
+
+        Returns
+        -------
+        self
+
+        Notes
+        -----
+        This is similar to DataFrame.apply, except that axis=None applies
+        the function to the entire DataFrame at once, rather tha column
+        or rowwise.
+        """
+        self._todo.append((lambda instance: getattr(instance, '_apply'),
+                          (func, axis, subset),
+                           kwargs))
+        return self
+
+    def _applymap(self, func, subset=None, **kwargs):
+        func = partial(func, **kwargs)  # applymap doesn't take kwargs?
+        if subset is None:
+            subset = pd.IndexSlice[:]
+        subset = _non_reducing_slice(subset)
+        result = self.data.loc[subset].applymap(func)
+        self._update_ctx(result)
+        return self
+
+    def applymap(self, func, subset=None, **kwargs):
+        """
+        Apply a function elementwise, updating the HTML
+        representation with the result.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        func : function
+        subset : IndexSlice
+            a valid indexer to limit ``data`` to *before* applying the
+            function. Consider using a pandas.IndexSlice
+        kwargs : dict
+            pass along to ``func``
+
+        Returns
+        -------
+        self
+
+        """
+        self._todo.append((lambda instance: getattr(instance, '_applymap'),
+                          (func, subset),
+                          kwargs))
+        return self
+
+    def set_precision(self, precision):
+        """
+        Set the precision used to render.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        precision: int
+
+        Returns
+        -------
+        self
+        """
+        self.precision = precision
+        return self
+
+    def export(self):
+        """
+        Export the styles to applied to the current Styler.
+        Can be applied to a second style with `.use`.
+
+        .. versionadded:: 0.17.1
+
+        Returns
+        -------
+        styles: list
+
+        See Also
+        --------
+        Styler.use
+        """
+        return self._todo
+
+    def use(self, styles):
+        """
+        Set the styles on the current Styler, possibly using styles
+        from `Styler.export`
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        styles: list
+            list of style functions
+
+        Returns
+        -------
+        self
+
+        See Also
+        --------
+        Styler.export
+        """
+        self._todo.extend(styles)
+        return self
+
+    def set_uuid(self, uuid):
+        """
+        Set the uuid for a Styler.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        uuid: str
+
+        Returns
+        -------
+        self
+        """
+        self.uuid = uuid
+        return self
+
+    def set_caption(self, caption):
+        """
+        Se the caption on a Styler
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        caption: str
+
+        Returns
+        -------
+        self
+        """
+        self.caption = caption
+        return self
+
+    def set_table_styles(self, table_styles):
+        """
+        Set the table styles on a Styler
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        table_styles: list
+
+        Returns
+        -------
+        self
+        """
+        self.table_styles = table_styles
+        return self
+
+    # -----------------------------------------------------------------------
+    # A collection of "builtin" styles
+    # -----------------------------------------------------------------------
+
+    @staticmethod
+    def _highlight_null(v, null_color):
+        return 'background-color: %s' % null_color if pd.isnull(v) else ''
+
+    def highlight_null(self, null_color='red'):
+        """
+        Shade the background ``null_color`` for missing values.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        null_color: str
+
+        Returns
+        -------
+        self
+        """
+        self.applymap(self._highlight_null, null_color=null_color)
+        return self
+
+    def background_gradient(self, cmap='PuBu', low=0, high=0,
+                            axis=0, subset=None):
+        """
+        Color the background in a gradient according to
+        the data in each column (optionally row).
+        Requires matplotlib.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        cmap: str or colormap
+            matplotlib colormap
+        low, high: float
+            compress the range by these values.
+        axis: int or str
+            1 or 'columns' for colunwise, 0 or 'index' for rowwise
+        subset: IndexSlice
+            a valid slice for ``data`` to limit the style application to
+
+        Returns
+        -------
+        self
+
+        Notes
+        -----
+        Tune ``low`` and ``high`` to keep the text legible by
+        not using the entire range of the color map. These extend
+        the range of the data by ``low * (x.max() - x.min())``
+        and ``high * (x.max() - x.min())`` before normalizing.
+        """
+        subset = _maybe_numeric_slice(self.data, subset)
+        subset = _non_reducing_slice(subset)
+        self.apply(self._background_gradient, cmap=cmap, subset=subset,
+                   axis=axis, low=low, high=high)
+        return self
+
+    @staticmethod
+    def _background_gradient(s, cmap='PuBu', low=0, high=0):
+        """Color background in a range according to the data."""
+        with _mpl(Styler.background_gradient) as (plt, colors):
+            rng = s.max() - s.min()
+            # extend lower / upper bounds, compresses color range
+            norm = colors.Normalize(s.min() - (rng * low),
+                                    s.max() + (rng * high))
+            # matplotlib modifies inplace?
+            # https://github.com/matplotlib/matplotlib/issues/5427
+            normed = norm(s.values)
+            c = [colors.rgb2hex(x) for x in plt.cm.get_cmap(cmap)(normed)]
+            return ['background-color: %s' % color for color in c]
+
+    def set_properties(self, subset=None, **kwargs):
+        """
+        Convience method for setting one or more non-data dependent
+        properties or each cell.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        subset: IndexSlice
+            a valid slice for ``data`` to limit the style application to
+        kwargs: dict
+            property: value pairs to be set for each cell
+
+        Returns
+        -------
+        self : Styler
+
+        Examples
+        --------
+        >>> df = pd.DataFrame(np.random.randn(10, 4))
+        >>> df.style.set_properties(color="white", align="right")
+        """
+        values = ';'.join('{p}: {v}'.format(p=p, v=v) for p, v in
+                          kwargs.items())
+        f = lambda x: values
+        return self.applymap(f, subset=subset)
+
+    @staticmethod
+    def _bar(s, color, width):
+        normed = width * (s - s.min()) / (s.max() - s.min())
+        attrs = 'width: 10em; height: 80%;'\
+            'background: linear-gradient(90deg,'\
+            '{c} {w}%, transparent 0%)'
+        return [attrs.format(c=color, w=x) for x in normed]
+
+    def bar(self, subset=None, axis=0, color='#d65f5f', width=100):
+        """
+        Color the background ``color`` proptional to the values in each column.
+        Excludes non-numeric data by default.
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        subset: IndexSlice, default None
+            a valid slice for ``data`` to limit the style application to
+        axis: int
+        color: str
+        width: float
+            A number between 0 or 100. The largest value will cover ``width``
+            percent of the cell's width
+
+        Returns
+        -------
+        self
+        """
+        subset = _maybe_numeric_slice(self.data, subset)
+        subset = _non_reducing_slice(subset)
+        self.apply(self._bar, subset=subset, axis=axis, color=color,
+                   width=width)
+        return self
+
+    def highlight_max(self, subset=None, color='yellow', axis=0):
+        """
+        Highlight the maximum by shading the background
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        subset: IndexSlice, default None
+            a valid slice for ``data`` to limit the style application to
+        color: str, default 'yellow'
+        axis: int, str, or None; default None
+            0 or 'index' for columnwise, 1 or 'columns' for rowwise
+            or ``None`` for tablewise (the default)
+
+        Returns
+        -------
+        self
+        """
+        return self._highlight_handler(subset=subset, color=color, axis=axis,
+                                       max_=True)
+
+    def highlight_min(self, subset=None, color='yellow', axis=0):
+        """
+        Highlight the minimum by shading the background
+
+        .. versionadded:: 0.17.1
+
+        Parameters
+        ----------
+        subset: IndexSlice, default None
+            a valid slice for ``data`` to limit the style application to
+        color: str, default 'yellow'
+        axis: int, str, or None; default None
+            0 or 'index' for columnwise, 1 or 'columns' for rowwise
+            or ``None`` for tablewise (the default)
+
+        Returns
+        -------
+        self
+        """
+        return self._highlight_handler(subset=subset, color=color, axis=axis,
+                                       max_=False)
+
+    def _highlight_handler(self, subset=None, color='yellow', axis=None,
+                           max_=True):
+        subset = _non_reducing_slice(_maybe_numeric_slice(self.data, subset))
+        self.apply(self._highlight_extrema, color=color, axis=axis,
+                   subset=subset, max_=max_)
+        return self
+
+    @staticmethod
+    def _highlight_extrema(data, color='yellow', max_=True):
+        '''
+        highlight the min or max in a Series or DataFrame
+        '''
+        attr = 'background-color: {0}'.format(color)
+        if data.ndim == 1:  # Series from .apply
+            if max_:
+                extrema = data == data.max()
+            else:
+                extrema = data == data.min()
+            return [attr if v else '' for v in extrema]
+        else:  # DataFrame from .tee
+            if max_:
+                extrema = data == data.max().max()
+            else:
+                extrema = data == data.min().min()
+            return pd.DataFrame(np.where(extrema, attr, ''),
+                                index=data.index, columns=data.columns)
+
+
+


Test patch:
diff --git a/pandas/tests/test_indexing.py b/pandas/tests/test_indexing.py
--- a/pandas/tests/test_indexing.py
+++ b/pandas/tests/test_indexing.py
@@ -15,6 +15,7 @@
 import pandas as pd
 import pandas.core.common as com
 from pandas import option_context
+from pandas.core.indexing import _non_reducing_slice, _maybe_numeric_slice
 from pandas.core.api import (DataFrame, Index, Series, Panel, isnull,
                              MultiIndex, Float64Index, Timestamp, Timedelta)
 from pandas.util.testing import (assert_almost_equal, assert_series_equal,
@@ -4728,6 +4729,49 @@ def test_large_mi_dataframe_indexing(self):
         result = MultiIndex.from_arrays([range(10**6), range(10**6)])
         assert(not (10**6, 0) in result)
 
+    def test_non_reducing_slice(self):
+        df = pd.DataFrame([[0, 1], [2, 3]])
+
+        slices = [
+            # pd.IndexSlice[:, :],
+            pd.IndexSlice[:, 1],
+            pd.IndexSlice[1, :],
+            pd.IndexSlice[[1], [1]],
+            pd.IndexSlice[1, [1]],
+            pd.IndexSlice[[1], 1],
+            pd.IndexSlice[1],
+            pd.IndexSlice[1, 1],
+            slice(None, None, None),
+            [0, 1],
+            np.array([0, 1]),
+            pd.Series([0, 1])
+        ]
+        for slice_ in slices:
+            tslice_ = _non_reducing_slice(slice_)
+            self.assertTrue(isinstance(df.loc[tslice_], DataFrame))
+
+    def test_list_slice(self):
+        # like dataframe getitem
+        slices = [['A'], pd.Series(['A']), np.array(['A'])]
+        df = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}, index=['A', 'B'])
+        expected = pd.IndexSlice[:, ['A']]
+        for subset in slices:
+            result = _non_reducing_slice(subset)
+            tm.assert_frame_equal(df.loc[result], df.loc[expected])
+
+    def test_maybe_numeric_slice(self):
+        df = pd.DataFrame({'A': [1, 2], 'B': ['c', 'd'], 'C': [True, False]})
+        result = _maybe_numeric_slice(df, slice_=None)
+        expected = pd.IndexSlice[:, ['A']]
+        self.assertEqual(result, expected)
+
+        result = _maybe_numeric_slice(df, None, include_bool=True)
+        expected = pd.IndexSlice[:, ['A', 'C']]
+        result = _maybe_numeric_slice(df, [1])
+        expected = [1]
+        self.assertEqual(result, expected)
+
+
 class TestCategoricalIndex(tm.TestCase):
 
     def setUp(self):
diff --git a/pandas/tests/test_style.py b/pandas/tests/test_style.py
new file mode 100644
--- /dev/null
+++ b/pandas/tests/test_style.py
@@ -0,0 +1,372 @@
+import os
+from nose import SkipTest
+
+# this is a mess. Getting failures on a python 2.7 build with
+# whenever we try to import jinja, whether it's installed or not.
+# so we're explicitly skipping that one *before* we try to import
+# jinja. We still need to export the imports as globals,
+# since importing Styler tries to import jinja2.
+job_name = os.environ.get('JOB_NAME', None)
+if job_name == '27_slow_nnet_LOCALE':
+    raise SkipTest("No jinja")
+try:
+    from pandas.core.style import Styler
+except ImportError:
+    raise SkipTest("No Jinja2")
+
+import copy
+
+import numpy as np
+import pandas as pd
+from pandas import DataFrame
+from pandas.util.testing import TestCase
+import pandas.util.testing as tm
+
+
+class TestStyler(TestCase):
+
+    def setUp(self):
+        np.random.seed(24)
+        self.s = DataFrame({'A': np.random.permutation(range(6))})
+        self.df = DataFrame({'A': [0, 1], 'B': np.random.randn(2)})
+        self.f = lambda x: x
+        self.g = lambda x: x
+
+        def h(x, foo='bar'):
+            return pd.Series(['color: %s' % foo], index=x.index, name=x.name)
+
+        self.h = h
+        self.styler = Styler(self.df)
+        self.attrs = pd.DataFrame({'A': ['color: red', 'color: blue']})
+        self.dataframes = [
+            self.df,
+            pd.DataFrame({'f': [1., 2.], 'o': ['a', 'b'],
+                          'c': pd.Categorical(['a', 'b'])})
+        ]
+
+    def test_update_ctx(self):
+        self.styler._update_ctx(self.attrs)
+        expected = {(0, 0): ['color: red'],
+                    (1, 0): ['color: blue']}
+        self.assertEqual(self.styler.ctx, expected)
+
+    def test_update_ctx_flatten_multi(self):
+        attrs = DataFrame({"A": ['color: red; foo: bar',
+                                 'color: blue; foo: baz']})
+        self.styler._update_ctx(attrs)
+        expected = {(0, 0): ['color: red', ' foo: bar'],
+                    (1, 0): ['color: blue', ' foo: baz']}
+        self.assertEqual(self.styler.ctx, expected)
+
+    def test_update_ctx_flatten_multi_traliing_semi(self):
+        attrs = DataFrame({"A": ['color: red; foo: bar;',
+                                 'color: blue; foo: baz;']})
+        self.styler._update_ctx(attrs)
+        expected = {(0, 0): ['color: red', ' foo: bar'],
+                    (1, 0): ['color: blue', ' foo: baz']}
+        self.assertEqual(self.styler.ctx, expected)
+
+    def test_copy(self):
+        s2 = copy.copy(self.styler)
+        self.assertTrue(self.styler is not s2)
+        self.assertTrue(self.styler.ctx is s2.ctx)  # shallow
+        self.assertTrue(self.styler._todo is s2._todo)
+
+        self.styler._update_ctx(self.attrs)
+        self.styler.highlight_max()
+        self.assertEqual(self.styler.ctx, s2.ctx)
+        self.assertEqual(self.styler._todo, s2._todo)
+
+    def test_deepcopy(self):
+        s2 = copy.deepcopy(self.styler)
+        self.assertTrue(self.styler is not s2)
+        self.assertTrue(self.styler.ctx is not s2.ctx)
+        self.assertTrue(self.styler._todo is not s2._todo)
+
+        self.styler._update_ctx(self.attrs)
+        self.styler.highlight_max()
+        self.assertNotEqual(self.styler.ctx, s2.ctx)
+        self.assertEqual(s2._todo, [])
+        self.assertNotEqual(self.styler._todo, s2._todo)
+
+    def test_clear(self):
+        s = self.df.style.highlight_max()._compute()
+        self.assertTrue(len(s.ctx) > 0)
+        self.assertTrue(len(s._todo) > 0)
+        s.clear()
+        self.assertTrue(len(s.ctx) == 0)
+        self.assertTrue(len(s._todo) == 0)
+
+    def test_render(self):
+        df = pd.DataFrame({"A": [0, 1]})
+        style = lambda x: pd.Series(["color: red", "color: blue"], name=x.name)
+        s = Styler(df, uuid='AB').apply(style).apply(style, axis=1)
+        s.render()
+        # it worked?
+
+    def test_render_double(self):
+        df = pd.DataFrame({"A": [0, 1]})
+        style = lambda x: pd.Series(["color: red; border: 1px",
+                                     "color: blue; border: 2px"], name=x.name)
+        s = Styler(df, uuid='AB').apply(style)
+        s.render()
+        # it worked?
+
+    def test_set_properties(self):
+        df = pd.DataFrame({"A": [0, 1]})
+        result = df.style.set_properties(color='white',
+                                         size='10px')._compute().ctx
+        # order is deterministic
+        v = ["color: white", "size: 10px"]
+        expected = {(0, 0): v, (1, 0): v}
+        self.assertEqual(result.keys(), expected.keys())
+        for v1, v2 in zip(result.values(), expected.values()):
+            self.assertEqual(sorted(v1), sorted(v2))
+
+    def test_set_properties_subset(self):
+        df = pd.DataFrame({'A': [0, 1]})
+        result = df.style.set_properties(subset=pd.IndexSlice[0, 'A'],
+                                         color='white')._compute().ctx
+        expected = {(0, 0): ['color: white']}
+        self.assertEqual(result, expected)
+
+    def test_apply_axis(self):
+        df = pd.DataFrame({'A': [0, 0], 'B': [1, 1]})
+        f = lambda x: ['val: %s' % x.max() for v in x]
+        result = df.style.apply(f, axis=1)
+        self.assertEqual(len(result._todo), 1)
+        self.assertEqual(len(result.ctx), 0)
+        result._compute()
+        expected = {(0, 0): ['val: 1'], (0, 1): ['val: 1'],
+                    (1, 0): ['val: 1'], (1, 1): ['val: 1']}
+        self.assertEqual(result.ctx, expected)
+
+        result = df.style.apply(f, axis=0)
+        expected = {(0, 0): ['val: 0'], (0, 1): ['val: 1'],
+                    (1, 0): ['val: 0'], (1, 1): ['val: 1']}
+        result._compute()
+        self.assertEqual(result.ctx, expected)
+        result = df.style.apply(f)  # default
+        result._compute()
+        self.assertEqual(result.ctx, expected)
+
+    def test_apply_subset(self):
+        axes = [0, 1]
+        slices = [pd.IndexSlice[:], pd.IndexSlice[:, ['A']],
+                  pd.IndexSlice[[1], :], pd.IndexSlice[[1], ['A']],
+                  pd.IndexSlice[:2, ['A', 'B']]]
+        for ax in axes:
+            for slice_ in slices:
+                result = self.df.style.apply(self.h, axis=ax, subset=slice_,
+                                             foo='baz')._compute().ctx
+                expected = dict(((r, c), ['color: baz'])
+                                for r, row in enumerate(self.df.index)
+                                for c, col in enumerate(self.df.columns)
+                                if row in self.df.loc[slice_].index
+                                and col in self.df.loc[slice_].columns)
+                self.assertEqual(result, expected)
+
+    def test_applymap_subset(self):
+        def f(x):
+            return 'foo: bar'
+
+        slices = [pd.IndexSlice[:], pd.IndexSlice[:, ['A']],
+                  pd.IndexSlice[[1], :], pd.IndexSlice[[1], ['A']],
+                  pd.IndexSlice[:2, ['A', 'B']]]
+
+        for slice_ in slices:
+            result = self.df.style.applymap(f, subset=slice_)._compute().ctx
+            expected = dict(((r, c), ['foo: bar'])
+                            for r, row in enumerate(self.df.index)
+                            for c, col in enumerate(self.df.columns)
+                            if row in self.df.loc[slice_].index
+                            and col in self.df.loc[slice_].columns)
+            self.assertEqual(result, expected)
+
+    def test_empty(self):
+        df = pd.DataFrame({'A': [1, 0]})
+        s = df.style
+        s.ctx = {(0, 0): ['color: red'],
+                 (1, 0): ['']}
+
+        result = s._translate()['cellstyle']
+        expected = [{'props': [['color', ' red']], 'selector': 'row0_col0'},
+                    {'props': [['', '']], 'selector': 'row1_col0'}]
+        self.assertEqual(result, expected)
+
+    def test_bar(self):
+        df = pd.DataFrame({'A': [0, 1, 2]})
+        result = df.style.bar()._compute().ctx
+        expected = {
+            (0, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,#d65f5f 0.0%, transparent 0%)'],
+            (1, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,#d65f5f 50.0%, transparent 0%)'],
+            (2, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,#d65f5f 100.0%, transparent 0%)']
+        }
+        self.assertEqual(result, expected)
+
+        result = df.style.bar(color='red', width=50)._compute().ctx
+        expected = {
+            (0, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,red 0.0%, transparent 0%)'],
+            (1, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,red 25.0%, transparent 0%)'],
+            (2, 0): ['width: 10em', ' height: 80%',
+                     'background: linear-gradient('
+                     '90deg,red 50.0%, transparent 0%)']
+        }
+        self.assertEqual(result, expected)
+
+        df['C'] = ['a'] * len(df)
+        result = df.style.bar(color='red', width=50)._compute().ctx
+        self.assertEqual(result, expected)
+        df['C'] = df['C'].astype('category')
+        result = df.style.bar(color='red', width=50)._compute().ctx
+        self.assertEqual(result, expected)
+
+    def test_highlight_null(self, null_color='red'):
+        df = pd.DataFrame({'A': [0, np.nan]})
+        result = df.style.highlight_null()._compute().ctx
+        expected = {(0, 0): [''],
+                    (1, 0): ['background-color: red']}
+        self.assertEqual(result, expected)
+
+    def test_nonunique_raises(self):
+        df = pd.DataFrame([[1, 2]], columns=['A', 'A'])
+        with tm.assertRaises(ValueError):
+            df.style
+
+        with tm.assertRaises(ValueError):
+            Styler(df)
+
+    def test_caption(self):
+        styler = Styler(self.df, caption='foo')
+        result = styler.render()
+        self.assertTrue(all(['caption' in result, 'foo' in result]))
+
+        styler = self.df.style
+        result = styler.set_caption('baz')
+        self.assertTrue(styler is result)
+        self.assertEqual(styler.caption, 'baz')
+
+    def test_uuid(self):
+        styler = Styler(self.df, uuid='abc123')
+        result = styler.render()
+        self.assertTrue('abc123' in result)
+
+        styler = self.df.style
+        result = styler.set_uuid('aaa')
+        self.assertTrue(result is styler)
+        self.assertEqual(result.uuid, 'aaa')
+
+    def test_table_styles(self):
+        style = [{'selector': 'th', 'props': [('foo', 'bar')]}]
+        styler = Styler(self.df, table_styles=style)
+        result = ' '.join(styler.render().split())
+        self.assertTrue('th { foo: bar; }' in result)
+
+        styler = self.df.style
+        result = styler.set_table_styles(style)
+        self.assertTrue(styler is result)
+        self.assertEqual(styler.table_styles, style)
+
+    def test_precision(self):
+        with pd.option_context('display.precision', 10):
+            s = Styler(self.df)
+        self.assertEqual(s.precision, 10)
+        s = Styler(self.df, precision=2)
+        self.assertEqual(s.precision, 2)
+
+        s2 = s.set_precision(4)
+        self.assertTrue(s is s2)
+        self.assertEqual(s.precision, 4)
+
+    def test_apply_none(self):
+        def f(x):
+            return pd.DataFrame(np.where(x == x.max(), 'color: red', ''),
+                                index=x.index, columns=x.columns)
+        result = (pd.DataFrame([[1, 2], [3, 4]])
+                  .style.apply(f, axis=None)._compute().ctx)
+        self.assertEqual(result[(1, 1)], ['color: red'])
+
+    def test_trim(self):
+        result = self.df.style.render()  # trim=True
+        self.assertEqual(result.count('#'), 0)
+
+        result = self.df.style.highlight_max().render()
+        self.assertEqual(result.count('#'), len(self.df.columns))
+
+    def test_highlight_max(self):
+        df = pd.DataFrame([[1, 2], [3, 4]], columns=['A', 'B'])
+        # max(df) = min(-df)
+        for max_ in [True, False]:
+            if max_:
+                attr = 'highlight_max'
+            else:
+                df = -df
+                attr = 'highlight_min'
+            result = getattr(df.style, attr)()._compute().ctx
+            self.assertEqual(result[(1, 1)], ['background-color: yellow'])
+
+            result = getattr(df.style, attr)(color='green')._compute().ctx
+            self.assertEqual(result[(1, 1)], ['background-color: green'])
+
+            result = getattr(df.style, attr)(subset='A')._compute().ctx
+            self.assertEqual(result[(1, 0)], ['background-color: yellow'])
+
+            result = getattr(df.style, attr)(axis=0)._compute().ctx
+            expected = {(1, 0): ['background-color: yellow'],
+                        (1, 1): ['background-color: yellow'],
+                        (0, 1): [''], (0, 0): ['']}
+            self.assertEqual(result, expected)
+
+            result = getattr(df.style, attr)(axis=1)._compute().ctx
+            expected = {(0, 1): ['background-color: yellow'],
+                        (1, 1): ['background-color: yellow'],
+                        (0, 0): [''], (1, 0): ['']}
+            self.assertEqual(result, expected)
+
+        # separate since we cant negate the strs
+        df['C'] = ['a', 'b']
+        result = df.style.highlight_max()._compute().ctx
+        expected = {(1, 1): ['background-color: yellow']}
+
+        result = df.style.highlight_min()._compute().ctx
+        expected = {(0, 0): ['background-color: yellow']}
+
+    def test_export(self):
+        f = lambda x: 'color: red' if x > 0 else 'color: blue'
+        g = lambda x, y, z: 'color: %s' if x > 0 else 'color: %s' % z
+        style1 = self.styler
+        style1.applymap(f)\
+            .applymap(g, y='a', z='b')\
+            .highlight_max()
+        result = style1.export()
+        style2 = self.df.style
+        style2.use(result)
+        self.assertEqual(style1._todo, style2._todo)
+        style2.render()
+
+
+@tm.mplskip
+class TestStylerMatplotlibDep(TestCase):
+
+    def test_background_gradient(self):
+        df = pd.DataFrame([[1, 2], [2, 4]], columns=['A', 'B'])
+        for axis in [0, 1, 'index', 'columns']:
+            for cmap in [None, 'YlOrRd']:
+                result = df.style.background_gradient(cmap=cmap)._compute().ctx
+                self.assertTrue(all("#" in x[0] for x in result.values()))
+                self.assertEqual(result[(0, 0)], result[(0, 1)])
+                self.assertEqual(result[(1, 0)], result[(1, 1)])
+
+        result = (df.style.background_gradient(subset=pd.IndexSlice[1, 'A'])
+                    ._compute().ctx)
+        self.assertEqual(result[(1, 0)], ['background-color: #fff7fb'])
diff --git a/pandas/tests/test_testing.py b/pandas/tests/test_testing.py
--- a/pandas/tests/test_testing.py
+++ b/pandas/tests/test_testing.py
@@ -11,7 +11,8 @@
 from pandas.util.testing import (
     assert_almost_equal, assertRaisesRegexp, raise_with_traceback,
     assert_index_equal, assert_series_equal, assert_frame_equal,
-    assert_numpy_array_equal, assert_isinstance, RNGContext
+    assert_numpy_array_equal, assert_isinstance, RNGContext,
+    assertRaises, skip_if_no_package_deco
 )
 from pandas.compat import is_platform_windows
 
@@ -627,6 +628,23 @@ def test_locale(self):
         self.assertTrue(len(locales) >= 1)
 
 
+def test_skiptest_deco():
+    from nose import SkipTest
+    @skip_if_no_package_deco("fakepackagename")
+    def f():
+        pass
+    with assertRaises(SkipTest):
+        f()
+
+    @skip_if_no_package_deco("numpy")
+    def f():
+        pass
+    # hack to ensure that SkipTest is *not* raised
+    with assertRaises(ValueError):
+        f()
+        raise ValueError
+
+
 if __name__ == '__main__':
     nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
                    exit=False)
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -1578,7 +1578,18 @@ def skip_if_no_package(*args, **kwargs):
                   exc_failed_check=SkipTest,
                   *args, **kwargs)
 
-#
+def skip_if_no_package_deco(pkg_name, version=None, app='pandas'):
+    from nose import SkipTest
+
+    def deco(func):
+        @wraps(func)
+        def wrapper(*args, **kwargs):
+            package_check(pkg_name, version=version, app=app,
+                        exc_failed_import=SkipTest, exc_failed_check=SkipTest)
+            return func(*args, **kwargs)
+        return wrapper
+    return deco
+ #
 # Additional tags decorators for nose
 #