diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt
index 4799d2711231b..48375e9a8e0dd 100644
--- a/doc/source/whatsnew/v0.20.0.txt
+++ b/doc/source/whatsnew/v0.20.0.txt
@@ -318,3 +318,5 @@ Bug Fixes
- Require at least 0.23 version of cython to avoid problems with character encodings (:issue:`14699`)
- Bug in converting object elements of array-like objects to unsigned 64-bit integers (:issue:`4471`)
- Bug in ``pd.pivot_table()`` where no error was raised when values argument was not in the columns (:issue:`14938`)
+- Bug in HTML display with MultiIndex and truncation (:issue:`14882`)
+
diff --git a/pandas/formats/format.py b/pandas/formats/format.py
index 0cf6050e515e0..211e61842c7a7 100644
--- a/pandas/formats/format.py
+++ b/pandas/formats/format.py
@@ -1248,6 +1248,7 @@ def _write_hierarchical_rows(self, fmt_values, indent):
# Insert ... row and adjust idx_values and
# level_lengths to take this into account.
ins_row = self.fmt.tr_row_num
+ inserted = False
for lnum, records in enumerate(level_lengths):
rec_new = {}
for tag, span in list(records.items()):
@@ -1255,9 +1256,17 @@ def _write_hierarchical_rows(self, fmt_values, indent):
rec_new[tag + 1] = span
elif tag + span > ins_row:
rec_new[tag] = span + 1
- dot_row = list(idx_values[ins_row - 1])
- dot_row[-1] = u('...')
- idx_values.insert(ins_row, tuple(dot_row))
+
+ # GH 14882 - Make sure insertion done once
+ if not inserted:
+ dot_row = list(idx_values[ins_row - 1])
+ dot_row[-1] = u('...')
+ idx_values.insert(ins_row, tuple(dot_row))
+ inserted = True
+ else:
+ dot_row = list(idx_values[ins_row])
+ dot_row[inner_lvl - lnum] = u('...')
+ idx_values[ins_row] = tuple(dot_row)
else:
rec_new[tag] = span
# If ins_row lies between tags, all cols idx cols
@@ -1267,6 +1276,12 @@ def _write_hierarchical_rows(self, fmt_values, indent):
if lnum == 0:
idx_values.insert(ins_row, tuple(
[u('...')] * len(level_lengths)))
+
+ # GH 14882 - Place ... in correct level
+ elif inserted:
+ dot_row = list(idx_values[ins_row])
+ dot_row[inner_lvl - lnum] = u('...')
+ idx_values[ins_row] = tuple(dot_row)
level_lengths[lnum] = rec_new
level_lengths[inner_lvl][ins_row] = 1
diff --git a/pandas/tests/formats/test_format.py b/pandas/tests/formats/test_format.py
index e7c32a4baa4ea..f709d3e3e97ba 100644
--- a/pandas/tests/formats/test_format.py
+++ b/pandas/tests/formats/test_format.py
@@ -1214,6 +1214,554 @@ def test_to_html_multiindex_sparsify(self):
self.assertEqual(result, expected)
+ # GH 14882 - Issue on truncation with odd length DataFrame
+ def test_to_html_multiindex_odd_even_truncate(self):
+ mi = MultiIndex.from_product([[100, 200, 300],
+ [10, 20, 30],
+ [1, 2, 3, 4, 5, 6, 7]],
+ names=['a','b','c'])
+ df = DataFrame({'n' : range(len(mi))}, index = mi)
+ result = df.to_html(max_rows=60)
+ expected = """\
+
+
+
+ |
+ |
+ |
+ n |
+
+
+ a |
+ b |
+ c |
+ |
+
+
+
+
+ 100 |
+ 10 |
+ 1 |
+ 0 |
+
+
+ 2 |
+ 1 |
+
+
+ 3 |
+ 2 |
+
+
+ 4 |
+ 3 |
+
+
+ 5 |
+ 4 |
+
+
+ 6 |
+ 5 |
+
+
+ 7 |
+ 6 |
+
+
+ 20 |
+ 1 |
+ 7 |
+
+
+ 2 |
+ 8 |
+
+
+ 3 |
+ 9 |
+
+
+ 4 |
+ 10 |
+
+
+ 5 |
+ 11 |
+
+
+ 6 |
+ 12 |
+
+
+ 7 |
+ 13 |
+
+
+ 30 |
+ 1 |
+ 14 |
+
+
+ 2 |
+ 15 |
+
+
+ 3 |
+ 16 |
+
+
+ 4 |
+ 17 |
+
+
+ 5 |
+ 18 |
+
+
+ 6 |
+ 19 |
+
+
+ 7 |
+ 20 |
+
+
+ 200 |
+ 10 |
+ 1 |
+ 21 |
+
+
+ 2 |
+ 22 |
+
+
+ 3 |
+ 23 |
+
+
+ 4 |
+ 24 |
+
+
+ 5 |
+ 25 |
+
+
+ 6 |
+ 26 |
+
+
+ 7 |
+ 27 |
+
+
+ 20 |
+ 1 |
+ 28 |
+
+
+ 2 |
+ 29 |
+
+
+ ... |
+ ... |
+
+
+ 6 |
+ 33 |
+
+
+ 7 |
+ 34 |
+
+
+ 30 |
+ 1 |
+ 35 |
+
+
+ 2 |
+ 36 |
+
+
+ 3 |
+ 37 |
+
+
+ 4 |
+ 38 |
+
+
+ 5 |
+ 39 |
+
+
+ 6 |
+ 40 |
+
+
+ 7 |
+ 41 |
+
+
+ 300 |
+ 10 |
+ 1 |
+ 42 |
+
+
+ 2 |
+ 43 |
+
+
+ 3 |
+ 44 |
+
+
+ 4 |
+ 45 |
+
+
+ 5 |
+ 46 |
+
+
+ 6 |
+ 47 |
+
+
+ 7 |
+ 48 |
+
+
+ 20 |
+ 1 |
+ 49 |
+
+
+ 2 |
+ 50 |
+
+
+ 3 |
+ 51 |
+
+
+ 4 |
+ 52 |
+
+
+ 5 |
+ 53 |
+
+
+ 6 |
+ 54 |
+
+
+ 7 |
+ 55 |
+
+
+ 30 |
+ 1 |
+ 56 |
+
+
+ 2 |
+ 57 |
+
+
+ 3 |
+ 58 |
+
+
+ 4 |
+ 59 |
+
+
+ 5 |
+ 60 |
+
+
+ 6 |
+ 61 |
+
+
+ 7 |
+ 62 |
+
+
+
"""
+ self.assertEqual(result, expected)
+
+ # Test that ... appears in a middle level
+ result = df.to_html(max_rows=56)
+ expected = """\
+
+
+
+ |
+ |
+ |
+ n |
+
+
+ a |
+ b |
+ c |
+ |
+
+
+
+
+ 100 |
+ 10 |
+ 1 |
+ 0 |
+
+
+ 2 |
+ 1 |
+
+
+ 3 |
+ 2 |
+
+
+ 4 |
+ 3 |
+
+
+ 5 |
+ 4 |
+
+
+ 6 |
+ 5 |
+
+
+ 7 |
+ 6 |
+
+
+ 20 |
+ 1 |
+ 7 |
+
+
+ 2 |
+ 8 |
+
+
+ 3 |
+ 9 |
+
+
+ 4 |
+ 10 |
+
+
+ 5 |
+ 11 |
+
+
+ 6 |
+ 12 |
+
+
+ 7 |
+ 13 |
+
+
+ 30 |
+ 1 |
+ 14 |
+
+
+ 2 |
+ 15 |
+
+
+ 3 |
+ 16 |
+
+
+ 4 |
+ 17 |
+
+
+ 5 |
+ 18 |
+
+
+ 6 |
+ 19 |
+
+
+ 7 |
+ 20 |
+
+
+ 200 |
+ 10 |
+ 1 |
+ 21 |
+
+
+ 2 |
+ 22 |
+
+
+ 3 |
+ 23 |
+
+
+ 4 |
+ 24 |
+
+
+ 5 |
+ 25 |
+
+
+ 6 |
+ 26 |
+
+
+ 7 |
+ 27 |
+
+
+ ... |
+ ... |
+ ... |
+
+
+ 30 |
+ 1 |
+ 35 |
+
+
+ 2 |
+ 36 |
+
+
+ 3 |
+ 37 |
+
+
+ 4 |
+ 38 |
+
+
+ 5 |
+ 39 |
+
+
+ 6 |
+ 40 |
+
+
+ 7 |
+ 41 |
+
+
+ 300 |
+ 10 |
+ 1 |
+ 42 |
+
+
+ 2 |
+ 43 |
+
+
+ 3 |
+ 44 |
+
+
+ 4 |
+ 45 |
+
+
+ 5 |
+ 46 |
+
+
+ 6 |
+ 47 |
+
+
+ 7 |
+ 48 |
+
+
+ 20 |
+ 1 |
+ 49 |
+
+
+ 2 |
+ 50 |
+
+
+ 3 |
+ 51 |
+
+
+ 4 |
+ 52 |
+
+
+ 5 |
+ 53 |
+
+
+ 6 |
+ 54 |
+
+
+ 7 |
+ 55 |
+
+
+ 30 |
+ 1 |
+ 56 |
+
+
+ 2 |
+ 57 |
+
+
+ 3 |
+ 58 |
+
+
+ 4 |
+ 59 |
+
+
+ 5 |
+ 60 |
+
+
+ 6 |
+ 61 |
+
+
+ 7 |
+ 62 |
+
+
+
"""
+ self.assertEqual(result, expected)
+
def test_to_html_index_formatter(self):
df = DataFrame([[0, 1], [2, 3], [4, 5], [6, 7]], columns=['foo', None],
index=lrange(4))