From 76e058837285203a1b30f66ad4615f581bc1aa7c Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 17 May 2016 21:58:02 -0500 Subject: [PATCH] External templates --- pandas/formats/style.py | 469 ++++++++++++++----------------- pandas/formats/templates/html.j2 | 42 +++ setup.py | 3 +- 3 files changed, 259 insertions(+), 255 deletions(-) create mode 100644 pandas/formats/templates/html.j2 diff --git a/pandas/formats/style.py b/pandas/formats/style.py index cf3f4bd6d6b14..3faa729a778a2 100644 --- a/pandas/formats/style.py +++ b/pandas/formats/style.py @@ -10,7 +10,7 @@ from collections import defaultdict, MutableMapping try: - from jinja2 import Template + from jinja2 import Environment, PackageLoader except ImportError: msg = "pandas.HTMLStyler requires jinja2. "\ "Please install with `conda install Jinja2`\n"\ @@ -39,93 +39,7 @@ def _mpl(func): raise ImportError(no_mpl_message.format(func.__name__)) -class HTMLStyler(object): - """ - Helps style a DataFrame or Series according to the - data with HTML and CSS. - - .. versionadded:: 0.17.1 - - .. warning:: - This is a new feature and is under active development. - We'll be adding features and possibly making breaking changes in future - releases. - - 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 - ---------- - template: Jinja Template - - Notes - ----- - Most styling will be done by passing style functions into - ``HTMLStyler.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, HTMLStyler has defined a ``_repr_html_`` - to automatically render itself. Otherwise call HTMLStyler.render to get - the genterated HTML. - - See Also - -------- - pandas.DataFrame.style - """ - template = Template(""" - - - - {% if caption %} - - {% endif %} - - - {% for r in head %} - - {% for c in r %} - <{{c.type}} class="{{c.class}}">{{c.value}} - {% endfor %} - - {% endfor %} - - - {% for r in body %} - - {% for c in r %} - <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}"> - {{ c.display_value }} - {% endfor %} - - {% endfor %} - -
{{caption}}
- """) +class BaseStyler(object): def __init__(self, data, precision=None, table_styles=None, uuid=None, caption=None, table_attributes=None): @@ -160,117 +74,6 @@ def default_display_func(x): self._display_funcs = defaultdict(lambda: default_display_func) - 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, [])) - value = clabels[r][c] - row_es.append({"type": "th", - "value": value, - "display_value": value, - "class": " ".join(cs)}) - head.append(row_es) - - if self.data.index.names and self.data.index.names != [None]: - index_header_row = [] - - for c, name in enumerate(self.data.index.names): - cs = [COL_HEADING_CLASS, - "level%s" % (n_clvls + 1), - "col%s" % c] - index_header_row.append({"type": "th", "value": name, - "class": " ".join(cs)}) - - index_header_row.extend( - [{"type": "th", - "value": BLANK_VALUE, - "class": " ".join([BLANK_CLASS]) - }] * len(clabels[0])) - - head.append(index_header_row) - - 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), - "display_value": rlabels[r][c]} - 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, [])) - formatter = self._display_funcs[(r, c)] - value = self.data.iloc[r, c] - row_es.append({ - "type": "td", - "value": value, - "class": " ".join(cs), - "id": "_".join(cs[1:]), - "display_value": formatter(value) - }) - 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, table_attributes=self.table_attributes) - def format(self, formatter, subset=None): """ Format the text display value of cells. @@ -520,25 +323,6 @@ def set_precision(self, precision): self.precision = precision return self - def set_table_attributes(self, attributes): - """ - Set the table attributes. These are the items - that show up in the opening ```` tag in addition - to to automatic (by default) id. - - .. versionadded:: 0.17.1 - - Parameters - ---------- - precision: int - - Returns - ------- - self : HTMLStyler - """ - self.table_attributes = attributes - return self - def export(self): """ Export the styles to applied to the current HTMLStyler. @@ -579,38 +363,243 @@ def use(self, styles): self._todo.extend(styles) return self - def set_uuid(self, uuid): + def set_caption(self, caption): """ - Set the uuid for a HTMLStyler. + Se the caption on a HTMLStyler .. versionadded:: 0.17.1 Parameters ---------- - uuid: str + caption: str Returns ------- self : HTMLStyler """ - self.uuid = uuid + self.caption = caption return self - def set_caption(self, caption): + def set_properties(self, subset=None, **kwargs): """ - Se the caption on a HTMLStyler + Convience method for setting one or more non-data dependent + properties or each cell. .. versionadded:: 0.17.1 Parameters ---------- - caption: str + 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 : HTMLStyler + + Examples + -------- + >>> df = pd.DataFrame(np.random.randn(10, 4)) + >>> df.style.set_properties(color="white", align="right") """ - self.caption = caption + 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) + +class HTMLStyler(BaseStyler): + """ + Helps style a DataFrame or Series according to the + data with HTML and CSS. + + .. versionadded:: 0.17.1 + + .. warning:: + This is a new feature and is under active development. + We'll be adding features and possibly making breaking changes in future + releases. + + 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 + ---------- + template: Jinja Template + + Notes + ----- + Most styling will be done by passing style functions into + ``HTMLStyler.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, HTMLStyler has defined a ``_repr_html_`` + to automatically render itself. Otherwise call HTMLStyler.render to get + the genterated HTML. + + See Also + -------- + pandas.DataFrame.style + """ + environment = Environment(loader=PackageLoader('pandas', + 'formats/templates')) + template = environment.get_template('html.j2') + + 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, [])) + value = clabels[r][c] + row_es.append({"type": "th", + "value": value, + "display_value": value, + "class": " ".join(cs)}) + head.append(row_es) + + if self.data.index.names and self.data.index.names != [None]: + index_header_row = [] + + for c, name in enumerate(self.data.index.names): + cs = [COL_HEADING_CLASS, + "level%s" % (n_clvls + 1), + "col%s" % c] + index_header_row.append({"type": "th", "value": name, + "class": " ".join(cs)}) + + index_header_row.extend( + [{"type": "th", + "value": BLANK_VALUE, + "class": " ".join([BLANK_CLASS]) + }] * len(clabels[0])) + + head.append(index_header_row) + + 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), + "display_value": rlabels[r][c]} + 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, [])) + formatter = self._display_funcs[(r, c)] + value = self.data.iloc[r, c] + row_es.append({ + "type": "td", + "value": value, + "class": " ".join(cs), + "id": "_".join(cs[1:]), + "display_value": formatter(value) + }) + 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, table_attributes=self.table_attributes) + + def set_table_attributes(self, attributes): + """ + Set the table attributes. These are the items + that show up in the opening ``
`` tag in addition + to to automatic (by default) id. + + .. versionadded:: 0.17.1 + + Parameters + ---------- + precision: int + + Returns + ------- + self : HTMLStyler + """ + self.table_attributes = attributes + return self + + def set_uuid(self, uuid): + """ + Set the uuid for a HTMLStyler. + + .. versionadded:: 0.17.1 + + Parameters + ---------- + uuid: str + + Returns + ------- + self : HTMLStyler + """ + self.uuid = uuid return self def set_table_styles(self, table_styles): @@ -720,34 +709,6 @@ def _background_gradient(s, cmap='PuBu', low=0, high=0): 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 : HTMLStyler - - 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()) diff --git a/pandas/formats/templates/html.j2 b/pandas/formats/templates/html.j2 new file mode 100644 index 0000000000000..f285e3dad0b41 --- /dev/null +++ b/pandas/formats/templates/html.j2 @@ -0,0 +1,42 @@ + + +
+{% if caption %} + +{% endif %} + + + {% for r in head %} + + {% for c in r %} + <{{c.type}} class="{{c.class}}">{{c.value}} + {% endfor %} + + {% endfor %} + + + {% for r in body %} + + {% for c in r %} + <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}"> + {{ c.display_value }} + {% endfor %} + + {% endfor %} + +
{{caption}}
diff --git a/setup.py b/setup.py index 596fe62ff0781..edce42a46de5e 100755 --- a/setup.py +++ b/setup.py @@ -616,7 +616,8 @@ def pxd(name): 'pandas.tests.formats': ['data/*.csv'], 'pandas.tests.indexes': ['data/*.pickle'], 'pandas.tseries.tests': ['data/*.pickle', - 'data/*.csv'] + 'data/*.csv'], + 'pandas.formats.templates': ['*.j2'], }, ext_modules=extensions, maintainer_email=EMAIL,