From ecba615c2ffe329d0313bead9c9b607c03c72ef7 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 6 Jul 2016 21:14:12 -0500 Subject: [PATCH 1/2] ENH: MultiIndex Structure for DataFrame.style BUG: Fix index class level row MVP Columns too tests --- doc/source/whatsnew/v0.19.0.txt | 1 + pandas/formats/style.py | 75 +++++++++++++++++++++++++----- pandas/tests/formats/test_style.py | 58 ++++++++++++++++++----- 3 files changed, 110 insertions(+), 24 deletions(-) diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index 04a749dfbc5bc..f617d746d5bdc 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -314,6 +314,7 @@ Other enhancements - ``Series.append`` now supports the ``ignore_index`` option (:issue:`13677`) - ``.to_stata()`` and ``StataWriter`` can now write variable labels to Stata dta files using a dictionary to make column names to labels (:issue:`13535`, :issue:`13536`) - ``.to_stata()`` and ``StataWriter`` will automatically convert ``datetime64[ns]`` columns to Stata format ``%tc``, rather than raising a ``ValueError`` (:issue:`12259`) +- ``DataFrame.style`` will now render sparsified MultiIndexes (:issue:`11655`) - ``DataFrame`` has gained support to re-order the columns based on the values in a row using ``df.sort_values(by='...', axis=1)`` (:issue:`10806`) diff --git a/pandas/formats/style.py b/pandas/formats/style.py index 472fd958d35eb..cac6bb40bf809 100644 --- a/pandas/formats/style.py +++ b/pandas/formats/style.py @@ -21,7 +21,8 @@ import numpy as np import pandas as pd -from pandas.compat import lzip, range +from pandas.compat import range +import pandas.core.common as com from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice try: import matplotlib.pyplot as plt @@ -110,7 +111,9 @@ class Styler(object): {% for r in head %} {% for c in r %} - <{{c.type}} class="{{c.class}}">{{c.value}} + {% if c.is_visible != False %} + <{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>{{c.value}} + {% endif %} {% endfor %} {% endfor %} @@ -119,8 +122,10 @@ class Styler(object): {% for r in body %} {% for c in r %} - <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}"> + {% if c.is_visible != False %} + <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}" {{ c.attributes|join(" ") }}> {{ c.display_value }} + {% endif %} {% endfor %} {% endfor %} @@ -181,6 +186,13 @@ def _translate(self): BLANK_CLASS = "blank" BLANK_VALUE = "" + def format_attr(pair): + return "{key}={value}".format(**pair) + + # for sparsifying a MultiIndex + idx_lengths = _get_level_lengths(self.index) + col_lengths = _get_level_lengths(self.columns) + cell_context = dict() n_rlvls = self.data.index.nlevels @@ -188,10 +200,6 @@ def _translate(self): 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: @@ -213,7 +221,13 @@ def _translate(self): row_es.append({"type": "th", "value": value, "display_value": value, - "class": " ".join(cs)}) + "class": " ".join(cs), + "is_visible": _is_visible(c, r, col_lengths), + "attributes": [ + format_attr({"key": "colspan", + "value": col_lengths.get( + (r, c), 1)}) + ]}) head.append(row_es) if self.data.index.names and self.data.index.names != [None]: @@ -236,12 +250,17 @@ def _translate(self): 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, [])) + # cs.extend( + # cell_context.get("row_headings", {}).get(r, {}).get(c, [])) row_es = [{"type": "th", + "is_visible": _is_visible(r, c, idx_lengths), + "attributes": [ + format_attr({"key": "rowspan", + "value": idx_lengths.get((c, r), 1)}) + ], "value": rlabels[r][c], - "class": " ".join(cs), + "class": " ".join([ROW_HEADING_CLASS, "level%s" % c, + "row%s" % r]), "display_value": rlabels[r][c]} for c in range(len(rlabels[r]))] @@ -893,6 +912,38 @@ def _highlight_extrema(data, color='yellow', max_=True): index=data.index, columns=data.columns) +def _is_visible(idx_row, idx_col, lengths): + """ + Index -> {(idx_row, idx_col): bool}) + """ + return (idx_col, idx_row) in lengths + + +def _get_level_lengths(index): + ''' + Given an index, find the level lenght for each element. + + Result is a dictionary of (level, inital_position): span + ''' + sentinel = com.sentinel_factory() + levels = index.format(sparsify=sentinel, adjoin=False, names=False) + + if index.nlevels == 1: + return {(0, i): 1 for i, value in enumerate(levels)} + + lengths = {} + + for i, lvl in enumerate(levels): + for j, row in enumerate(lvl): + if row != sentinel: + last_label = j + lengths[(i, last_label)] = 1 + else: + lengths[(i, last_label)] += 1 + + return lengths + + def _maybe_wrap_formatter(formatter): if is_string_like(formatter): return lambda x: formatter.format(x) diff --git a/pandas/tests/formats/test_style.py b/pandas/tests/formats/test_style.py index 9a34f545bd119..69aea0d9d1e41 100644 --- a/pandas/tests/formats/test_style.py +++ b/pandas/tests/formats/test_style.py @@ -8,11 +8,6 @@ from pandas.util.testing import TestCase import pandas.util.testing as tm -# 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") @@ -22,7 +17,7 @@ import jinja2 # noqa except ImportError: raise SkipTest("No Jinja2") -from pandas.formats.style import Styler # noqa +from pandas.formats.style import Styler, _get_level_lengths # noqa class TestStyler(TestCase): @@ -152,15 +147,24 @@ def test_empty_index_name_doesnt_display(self): {'class': 'col_heading level0 col0', 'display_value': 'A', 'type': 'th', - 'value': 'A'}, + 'value': 'A', + 'is_visible': True, + 'attributes': ["colspan=1"], + }, {'class': 'col_heading level0 col1', 'display_value': 'B', 'type': 'th', - 'value': 'B'}, + 'value': 'B', + 'is_visible': True, + 'attributes': ["colspan=1"], + }, {'class': 'col_heading level0 col2', 'display_value': 'C', 'type': 'th', - 'value': 'C'}]] + 'value': 'C', + 'is_visible': True, + 'attributes': ["colspan=1"], + }]] self.assertEqual(result['head'], expected) @@ -171,9 +175,11 @@ def test_index_name(self): expected = [[{'class': 'blank', 'type': 'th', 'value': ''}, {'class': 'col_heading level0 col0', 'type': 'th', - 'value': 'B', 'display_value': 'B'}, + 'value': 'B', 'display_value': 'B', + 'is_visible': True, 'attributes': ['colspan=1']}, {'class': 'col_heading level0 col1', 'type': 'th', - 'value': 'C', 'display_value': 'C'}], + 'value': 'C', 'display_value': 'C', + 'is_visible': True, 'attributes': ['colspan=1']}], [{'class': 'col_heading level2 col0', 'type': 'th', 'value': 'A'}, {'class': 'blank', 'type': 'th', 'value': ''}, @@ -189,7 +195,9 @@ def test_multiindex_name(self): expected = [[{'class': 'blank', 'type': 'th', 'value': ''}, {'class': 'blank', 'type': 'th', 'value': ''}, {'class': 'col_heading level0 col0', 'type': 'th', - 'value': 'C', 'display_value': 'C'}], + 'value': 'C', 'display_value': 'C', + 'is_visible': True, 'attributes': ['colspan=1'], + }], [{'class': 'col_heading level2 col0', 'type': 'th', 'value': 'A'}, {'class': 'col_heading level2 col1', 'type': 'th', @@ -581,6 +589,32 @@ def f(x): with tm.assertRaises(ValueError): df.style._apply(f, axis=None) + def test_get_level_lengths(self): + index = pd.MultiIndex.from_product([['a', 'b'], [0, 1, 2]]) + expected = {(0, 0): 3, (0, 3): 3, (1, 0): 1, (1, 1): 1, (1, 2): 1, + (1, 3): 1, (1, 4): 1, (1, 5): 1} + result = _get_level_lengths(index) + self.assertDictEqual(result, expected) + + def test_get_level_lengths_un_sorted(self): + index = pd.MultiIndex.from_arrays([ + [1, 1, 2, 1], + ['a', 'b', 'b', 'd'] + ]) + expected = {(0, 0): 2, (0, 2): 1, (0, 3): 1, + (1, 0): 1, (1, 1): 1, (1, 2): 1, (1, 3): 1} + result = _get_level_lengths(index) + self.assertDictEqual(result, expected) + + def test_mi_sparse(self): + df = pd.DataFrame({'A': [1, 2, 3, 4]}, + index=pd.MultiIndex.from_product([['a', 'b'], + [0, 1]])) + result = df.style.render() + assert 'rowspan' in result + result = df.T.style.render() + assert 'colspan' in result + @tm.mplskip class TestStylerMatplotlibDep(TestCase): From 7c03a72212c2356ca9c00a8ad49ac12935d0ef82 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 21 Jul 2016 06:58:11 -0500 Subject: [PATCH 2/2] ENH: DataFrame.style column names BUG: Fix CSS classes for index names Updates doc2 option honor options --- doc/source/html-styling.ipynb | 21 ++++ doc/source/whatsnew/v0.19.0.txt | 3 +- pandas/formats/style.py | 63 ++++++++++-- pandas/tests/formats/test_style.py | 158 +++++++++++++++++++++++++---- 4 files changed, 211 insertions(+), 34 deletions(-) diff --git a/doc/source/html-styling.ipynb b/doc/source/html-styling.ipynb index 8668ee3de7470..e55712b2bb4f6 100644 --- a/doc/source/html-styling.ipynb +++ b/doc/source/html-styling.ipynb @@ -788,6 +788,27 @@ "We hope to collect some useful ones either in pandas, or preferable in a new package that [builds on top](#Extensibility) the tools here." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CSS Classes\n", + "\n", + "Certain CSS classes are attached to cells.\n", + "\n", + "- Index and Column names include `index_name` and `level` where `k` is its level in a MultiIndex\n", + "- Index label cells include\n", + " + `row_heading`\n", + " + `row` where `n` is the numeric position of the row\n", + " + `level` where `k` is the level in a MultiIndex\n", + "- Column label cells include\n", + " + `col_heading`\n", + " + `col` where `n` is the numeric position of the column\n", + " + `level` where `k` is the level in a MultiIndex\n", + "- Blank cells include `blank`\n", + "- Data cells include `data`" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index f617d746d5bdc..d4c7b4af2e367 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -315,6 +315,7 @@ Other enhancements - ``.to_stata()`` and ``StataWriter`` can now write variable labels to Stata dta files using a dictionary to make column names to labels (:issue:`13535`, :issue:`13536`) - ``.to_stata()`` and ``StataWriter`` will automatically convert ``datetime64[ns]`` columns to Stata format ``%tc``, rather than raising a ``ValueError`` (:issue:`12259`) - ``DataFrame.style`` will now render sparsified MultiIndexes (:issue:`11655`) +- ``DataFrame.style`` will now show column level names (e.g. ``DataFrame.columns.names``) (:issue:`13775`) - ``DataFrame`` has gained support to re-order the columns based on the values in a row using ``df.sort_values(by='...', axis=1)`` (:issue:`10806`) @@ -770,8 +771,8 @@ Bug Fixes - Bug in ``groupby`` with ``as_index=False`` returns all NaN's when grouping on multiple columns including a categorical one (:issue:`13204`) - Bug in ``df.groupby(...)[...]`` where getitem with ``Int64Index`` raised an error (:issue:`13731`) +- Bug in the CSS classes assigned to ``DataFrame.style`` for index names. Previously they were assigned ``"col_heading level col"`` where ``n`` was the number of levels + 1. Now they are assigned ``"index_name level"``, where ``n`` is the correct level for that MultiIndex. - Bug where ``pd.read_gbq()`` could throw ``ImportError: No module named discovery`` as a result of a naming conflict with another python package called apiclient (:issue:`13454`) - Bug in ``Index.union`` returns an incorrect result with a named empty index (:issue:`13432`) - Bugs in ``Index.difference`` and ``DataFrame.join`` raise in Python3 when using mixed-integer indexes (:issue:`13432`, :issue:`12814`) - - Bug in ``.to_excel()`` when DataFrame contains a MultiIndex which contains a label with a NaN value (:issue:`13511`) diff --git a/pandas/formats/style.py b/pandas/formats/style.py index cac6bb40bf809..4d5e72a38bb98 100644 --- a/pandas/formats/style.py +++ b/pandas/formats/style.py @@ -22,6 +22,7 @@ import numpy as np import pandas as pd from pandas.compat import range +from pandas.core.config import get_option import pandas.core.common as com from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice try: @@ -80,6 +81,24 @@ class Styler(object): to automatically render itself. Otherwise call Styler.render to get the genterated HTML. + CSS classes are attached to the generated HTML + + * Index and Column names include ``index_name`` and ``level`` + where `k` is its level in a MultiIndex + * Index label cells include + + * ``row_heading`` + * ``row`` where `n` is the numeric position of the row + * ``level`` where `k` is the level in a MultiIndex + + * Column label cells include + * ``col_heading`` + * ``col`` where `n` is the numeric position of the column + * ``evel`` where `k` is the level in a MultiIndex + + * Blank cells include ``blank`` + * Data cells include ``data`` + See Also -------- pandas.DataFrame.style @@ -112,7 +131,8 @@ class Styler(object): {% for c in r %} {% if c.is_visible != False %} - <{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>{{c.value}} + <{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}> + {{c.value}} {% endif %} {% endfor %} @@ -123,7 +143,8 @@ class Styler(object): {% for c in r %} {% if c.is_visible != False %} - <{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}" {{ c.attributes|join(" ") }}> + <{{c.type}} id="T_{{uuid}}{{c.id}}" + class="{{c.class}}" {{ c.attributes|join(" ") }}> {{ c.display_value }} {% endif %} {% endfor %} @@ -153,7 +174,7 @@ def __init__(self, data, precision=None, table_styles=None, uuid=None, self.table_styles = table_styles self.caption = caption if precision is None: - precision = pd.options.display.precision + precision = get_option('display.precision') self.precision = precision self.table_attributes = table_attributes # display_funcs maps (row, col) -> formatting function @@ -182,6 +203,8 @@ def _translate(self): uuid = self.uuid or str(uuid1()).replace("-", "_") ROW_HEADING_CLASS = "row_heading" COL_HEADING_CLASS = "col_heading" + INDEX_NAME_CLASS = "index_name" + DATA_CLASS = "data" BLANK_CLASS = "blank" BLANK_VALUE = "" @@ -210,9 +233,24 @@ def format_attr(pair): head = [] for r in range(n_clvls): + # Blank for Index columns... row_es = [{"type": "th", "value": BLANK_VALUE, - "class": " ".join([BLANK_CLASS])}] * n_rlvls + "display_value": BLANK_VALUE, + "is_visible": True, + "class": " ".join([BLANK_CLASS])}] * (n_rlvls - 1) + + # ... except maybe the last for columns.names + name = self.data.columns.names[r] + cs = [BLANK_CLASS if name is None else INDEX_NAME_CLASS, + "level%s" % r] + name = BLANK_VALUE if name is None else name + row_es.append({"type": "th", + "value": name, + "display_value": name, + "class": " ".join(cs), + "is_visible": True}) + for c in range(len(clabels[0])): cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c] cs.extend(cell_context.get( @@ -230,13 +268,14 @@ def format_attr(pair): ]}) head.append(row_es) - if self.data.index.names and self.data.index.names != [None]: + if self.data.index.names and not all(x is None + for x in self.data.index.names): index_header_row = [] for c, name in enumerate(self.data.index.names): - cs = [COL_HEADING_CLASS, - "level%s" % (n_clvls + 1), - "col%s" % c] + cs = [INDEX_NAME_CLASS, + "level%s" % c] + name = '' if name is None else name index_header_row.append({"type": "th", "value": name, "class": " ".join(cs)}) @@ -920,11 +959,11 @@ def _is_visible(idx_row, idx_col, lengths): def _get_level_lengths(index): - ''' + """ Given an index, find the level lenght for each element. Result is a dictionary of (level, inital_position): span - ''' + """ sentinel = com.sentinel_factory() levels = index.format(sparsify=sentinel, adjoin=False, names=False) @@ -935,7 +974,9 @@ def _get_level_lengths(index): for i, lvl in enumerate(levels): for j, row in enumerate(lvl): - if row != sentinel: + if not get_option('display.multi_sparse'): + lengths[(i, j)] = 1 + elif row != sentinel: last_label = j lengths[(i, last_label)] = 1 else: diff --git a/pandas/tests/formats/test_style.py b/pandas/tests/formats/test_style.py index 69aea0d9d1e41..3083750e582fc 100644 --- a/pandas/tests/formats/test_style.py +++ b/pandas/tests/formats/test_style.py @@ -8,6 +8,11 @@ from pandas.util.testing import TestCase import pandas.util.testing as tm +# 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") @@ -143,7 +148,8 @@ def test_empty_index_name_doesnt_display(self): df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]}) result = df.style._translate() - expected = [[{'class': 'blank', 'type': 'th', 'value': ''}, + expected = [[{'class': 'blank level0', 'type': 'th', 'value': '', + 'is_visible': True, 'display_value': ''}, {'class': 'col_heading level0 col0', 'display_value': 'A', 'type': 'th', @@ -173,14 +179,15 @@ def test_index_name(self): df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]}) result = df.set_index('A').style._translate() - expected = [[{'class': 'blank', 'type': 'th', 'value': ''}, + expected = [[{'class': 'blank level0', 'type': 'th', 'value': '', + 'display_value': '', 'is_visible': True}, {'class': 'col_heading level0 col0', 'type': 'th', 'value': 'B', 'display_value': 'B', 'is_visible': True, 'attributes': ['colspan=1']}, {'class': 'col_heading level0 col1', 'type': 'th', 'value': 'C', 'display_value': 'C', 'is_visible': True, 'attributes': ['colspan=1']}], - [{'class': 'col_heading level2 col0', 'type': 'th', + [{'class': 'index_name level0', 'type': 'th', 'value': 'A'}, {'class': 'blank', 'type': 'th', 'value': ''}, {'class': 'blank', 'type': 'th', 'value': ''}]] @@ -192,17 +199,20 @@ def test_multiindex_name(self): df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]}) result = df.set_index(['A', 'B']).style._translate() - expected = [[{'class': 'blank', 'type': 'th', 'value': ''}, - {'class': 'blank', 'type': 'th', 'value': ''}, - {'class': 'col_heading level0 col0', 'type': 'th', - 'value': 'C', 'display_value': 'C', - 'is_visible': True, 'attributes': ['colspan=1'], - }], - [{'class': 'col_heading level2 col0', 'type': 'th', - 'value': 'A'}, - {'class': 'col_heading level2 col1', 'type': 'th', - 'value': 'B'}, - {'class': 'blank', 'type': 'th', 'value': ''}]] + expected = [[ + {'class': 'blank', 'type': 'th', 'value': '', + 'display_value': '', 'is_visible': True}, + {'class': 'blank level0', 'type': 'th', 'value': '', + 'display_value': '', 'is_visible': True}, + {'class': 'col_heading level0 col0', 'type': 'th', + 'value': 'C', 'display_value': 'C', + 'is_visible': True, 'attributes': ['colspan=1'], + }], + [{'class': 'index_name level0', 'type': 'th', + 'value': 'A'}, + {'class': 'index_name level1', 'type': 'th', + 'value': 'B'}, + {'class': 'blank', 'type': 'th', 'value': ''}]] self.assertEqual(result['head'], expected) @@ -594,7 +604,7 @@ def test_get_level_lengths(self): expected = {(0, 0): 3, (0, 3): 3, (1, 0): 1, (1, 1): 1, (1, 2): 1, (1, 3): 1, (1, 4): 1, (1, 5): 1} result = _get_level_lengths(index) - self.assertDictEqual(result, expected) + tm.assert_dict_equal(result, expected) def test_get_level_lengths_un_sorted(self): index = pd.MultiIndex.from_arrays([ @@ -604,16 +614,120 @@ def test_get_level_lengths_un_sorted(self): expected = {(0, 0): 2, (0, 2): 1, (0, 3): 1, (1, 0): 1, (1, 1): 1, (1, 2): 1, (1, 3): 1} result = _get_level_lengths(index) - self.assertDictEqual(result, expected) + tm.assert_dict_equal(result, expected) def test_mi_sparse(self): - df = pd.DataFrame({'A': [1, 2, 3, 4]}, - index=pd.MultiIndex.from_product([['a', 'b'], + df = pd.DataFrame({'A': [1, 2]}, + index=pd.MultiIndex.from_arrays([['a', 'a'], [0, 1]])) - result = df.style.render() - assert 'rowspan' in result - result = df.T.style.render() - assert 'colspan' in result + result = df.style._translate() + body_0 = result['body'][0][0] + expected_0 = { + "value": "a", "display_value": "a", "is_visible": True, + "type": "th", "attributes": ["rowspan=2"], + "class": "row_heading level0 row0", + } + tm.assert_dict_equal(body_0, expected_0) + + body_1 = result['body'][0][1] + expected_1 = { + "value": 0, "display_value": 0, "is_visible": True, + "type": "th", "attributes": ["rowspan=1"], + "class": "row_heading level1 row0", + } + tm.assert_dict_equal(body_1, expected_1) + + body_10 = result['body'][1][0] + expected_10 = { + "value": 'a', "display_value": 'a', "is_visible": False, + "type": "th", "attributes": ["rowspan=1"], + "class": "row_heading level0 row1", + } + tm.assert_dict_equal(body_10, expected_10) + + head = result['head'][0] + expected = [ + {'type': 'th', 'class': 'blank', 'value': '', + 'is_visible': True, "display_value": ''}, + {'type': 'th', 'class': 'blank level0', 'value': '', + 'is_visible': True, 'display_value': ''}, + {'attributes': ['colspan=1'], 'class': 'col_heading level0 col0', + 'is_visible': True, 'type': 'th', 'value': 'A', + 'display_value': 'A'}] + self.assertEqual(head, expected) + + def test_mi_sparse_disabled(self): + with pd.option_context('display.multi_sparse', False): + df = pd.DataFrame({'A': [1, 2]}, + index=pd.MultiIndex.from_arrays([['a', 'a'], + [0, 1]])) + result = df.style._translate() + body = result['body'] + for row in body: + self.assertEqual(row[0]['attributes'], ['rowspan=1']) + + def test_mi_sparse_index_names(self): + df = pd.DataFrame({'A': [1, 2]}, index=pd.MultiIndex.from_arrays( + [['a', 'a'], [0, 1]], + names=['idx_level_0', 'idx_level_1']) + ) + result = df.style._translate() + head = result['head'][1] + expected = [{ + 'class': 'index_name level0', 'value': 'idx_level_0', + 'type': 'th'}, + {'class': 'index_name level1', 'value': 'idx_level_1', + 'type': 'th'}, + {'class': 'blank', 'value': '', 'type': 'th'}] + + self.assertEqual(head, expected) + + def test_mi_sparse_column_names(self): + df = pd.DataFrame( + np.arange(16).reshape(4, 4), + index=pd.MultiIndex.from_arrays( + [['a', 'a', 'b', 'a'], [0, 1, 1, 2]], + names=['idx_level_0', 'idx_level_1']), + columns=pd.MultiIndex.from_arrays( + [['C1', 'C1', 'C2', 'C2'], [1, 0, 1, 0]], + names=['col_0', 'col_1'] + ) + ) + result = df.style._translate() + head = result['head'][1] + expected = [ + {'class': 'blank', 'value': '', 'display_value': '', + 'type': 'th', 'is_visible': True}, + {'class': 'index_name level1', 'value': 'col_1', + 'display_value': 'col_1', 'is_visible': True, 'type': 'th'}, + {'attributes': ['colspan=1'], + 'class': 'col_heading level1 col0', + 'display_value': 1, + 'is_visible': True, + 'type': 'th', + 'value': 1}, + {'attributes': ['colspan=1'], + 'class': 'col_heading level1 col1', + 'display_value': 0, + 'is_visible': True, + 'type': 'th', + 'value': 0}, + + {'attributes': ['colspan=1'], + 'class': 'col_heading level1 col2', + 'display_value': 1, + 'is_visible': True, + 'type': 'th', + 'value': 1}, + + {'attributes': ['colspan=1'], + 'class': 'col_heading level1 col3', + 'display_value': 0, + 'is_visible': True, + 'type': 'th', + 'value': 0}, + ] + self.assertEqual(head, expected) @tm.mplskip