Repository: pandas-dev/pandas

Issue:
HTML table styling in notebook has awkward alignment when text wraps or when index is named
Following up on issue #15379 regarding the formatting of Multiindex rows in jupyter notebook, I believe the fix for that issue (pull request #16080) affected the formatting of all dataframes, whether they have a Multiindex or not. The solution in pull request #16080 was to make all row indices top-aligned in cells. This looks fine when the data in the table all stays on one line. However, I have wide dataframes that contain longer-than-average text strings, and the default behavior when displaying the table in notebook is to wrap the text onto another line or lines. When this happens, having the row indices be top-aligned is very awkward-looking because all the data in the table remains vertically middle-aligned.

I have tried to fix this issue using custom css, but it gets overridden every time by the css produced by the code [here](https://github.com/pandas-dev/pandas/blob/master/pandas/io/formats/format.py#L1114), which was added in the referenced pull request.

#### Reproduce
Put the following lines of code into a jupyter notebook:
```python
import pandas as pd
df = pd.read_csv('http://www.baseballprospectus.com/standings/index.php?odate=2017-06-27&otype=csv')
df.head(8)
```

#### Current Output (Row Indices Top-aligned)

<img width="1089" alt="screen shot 2017-06-28 at 1 58 50 pm" src="https://user-images.githubusercontent.com/5471483/27652521-1ac296ec-5c0a-11e7-8f7d-5355fa008dbe.png">

#### Better Output (Row Indices Middle-aligned)

<img width="1089" alt="screen shot 2017-06-28 at 1 59 44 pm" src="https://user-images.githubusercontent.com/5471483/27652529-2324bac2-5c0a-11e7-93bd-20cca873060e.png">

I noticed this issue when upgrading from pandas 0.19.2 to 0.20.2 over the weekend.

pandas 0.20.2
notebook 5.0.0
Python 2.7.13

Full version stuff:
<details>

```
INSTALLED VERSIONS
------------------
commit: None
python: 2.7.13.final.0
python-bits: 64
OS: Darwin
OS-release: 15.6.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: None.None

pandas: 0.20.2
pytest: None
pip: 9.0.1
setuptools: 36.0.1
Cython: None
numpy: 1.13.0
scipy: None
xarray: None
IPython: 5.4.1
sphinx: None
patsy: None
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: 0.999999999
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
pandas_gbq: None
pandas_datareader: None
```
</details>


## EDIT 6/30/17

I just discovered an additional issue and figured I should add it to this. For cases where the dataframe's index is named, the table header is entirely left-aligned, including the index name, even when the columns are not a Multiindex. All the cells in the body of the table, including the index, remain right-aligned.

#### Reproduce
Put the following lines of code into a jupyter notebook:
```python
import pandas as pd
df = pd.read_csv('http://www.baseballprospectus.com/standings/index.php?odate=2017-06-27&otype=csv')
df.set_index('NAME', inplace=True)
df.head(7)
```

#### Current Output
Table header left-aligned with named index and non-Multiindex columns

<img width="1109" alt="screen shot 2017-06-30 at 2 43 49 pm" src="https://user-images.githubusercontent.com/5471483/27749691-94885112-5da2-11e7-9edf-93aeffea9d35.png">

#### Better Output
Table header right-aligned with named index and non-Multiindex columns

<img width="1098" alt="screen shot 2017-06-30 at 2 47 28 pm" src="https://user-images.githubusercontent.com/5471483/27749802-13c1c5a8-5da3-11e7-9144-dc21365ba04e.png">



Discussion:
I think this issue is [this](https://github.com/pandas-dev/pandas/blob/664348c440ccb4dab3d4c420d6aaee7c688c9b0e/pandas/io/formats/format.py#L1125) selector, which should only apply when where are multiple levels to the index (a MultiIndex)

I'm not sure how to express this in CSS, so if anyone knows more please chime in. I suppose we could maybe do something like (untested)

```css
# Always vertical align, for MultiIndex...

.dataframe tbody tr th {
    vertical-align: top;
}

# but override for single
.dataframe tbody tr th:only-child {
    vertical-align: middle;
}
```
@maxwasserman Thanks for opening this! I also just noted it some days ago with a notebook with a lot of cell content.

If we want to fix this by adding a different css to the frame's output depending on whether it has a multi-index or not, then every frame needs to get an unique id?
Did a small test, and this seems to do the trick:

```
<style>
    .dataframe thead tr:only-child th {
        text-align: right;
    }
    .dataframe thead th {
        text-align: left;
    }
    
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }
    .dataframe tbody tr th {
        vertical-align: top;
    }
</style
```

@maxwasserman Would you like to do a PR?

The difference with Tom's snippet is that I use `only-of-type` instead of `only-child`, since for a row, the header cell (so index label) is not the only child in that row, but the only child of type th
I edited the original issue report with a new visual issue I just found. It looks like the css selectors as they are also have the unintended effect of formatting a DataFrame with a named index as if it had Multiindex columns.

@jorisvandenbossche I'll try to start one this weekend.

Resolving patch:
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -1132,20 +1132,42 @@ def write_tr(self, line, indent=0, indent_delta=4, header=False,
         self.write('</tr>', indent)
 
     def write_style(self):
-        template = dedent("""\
-            <style>
-                .dataframe thead tr:only-child th {
-                    text-align: right;
-                }
-
-                .dataframe thead th {
-                    text-align: left;
-                }
-
-                .dataframe tbody tr th {
-                    vertical-align: top;
-                }
-            </style>""")
+        # We use the "scoped" attribute here so that the desired
+        # style properties for the data frame are not then applied
+        # throughout the entire notebook.
+        template_first = """\
+            <style scoped>"""
+        template_last = """\
+            </style>"""
+        template_select = """\
+                .dataframe %s {
+                    %s: %s;
+                }"""
+        element_props = [('tbody tr th:only-of-type',
+                          'vertical-align',
+                          'middle'),
+                         ('tbody tr th',
+                          'vertical-align',
+                          'top')]
+        if isinstance(self.columns, MultiIndex):
+            element_props.append(('thead tr th',
+                                  'text-align',
+                                  'left'))
+            if all((self.fmt.has_index_names,
+                    self.fmt.index,
+                    self.fmt.show_index_names)):
+                element_props.append(('thead tr:last-of-type th',
+                                      'text-align',
+                                      'right'))
+        else:
+            element_props.append(('thead th',
+                                  'text-align',
+                                  'right'))
+        template_mid = '\n\n'.join(map(lambda t: template_select % t,
+                                       element_props))
+        template = dedent('\n'.join((template_first,
+                                     template_mid,
+                                     template_last)))
         if self.notebook:
             self.write(template)
 


Test patch:
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -1868,12 +1868,16 @@ def test_to_html_no_index_max_rows(self):
     def test_to_html_notebook_has_style(self):
         df = pd.DataFrame({"A": [1, 2, 3]})
         result = df.to_html(notebook=True)
-        assert "thead tr:only-child" in result
+        assert "tbody tr th:only-of-type" in result
+        assert "vertical-align: middle;" in result
+        assert "thead th" in result
 
     def test_to_html_notebook_has_no_style(self):
         df = pd.DataFrame({"A": [1, 2, 3]})
         result = df.to_html()
-        assert "thead tr:only-child" not in result
+        assert "tbody tr th:only-of-type" not in result
+        assert "vertical-align: middle;" not in result
+        assert "thead th" not in result
 
     def test_to_html_with_index_names_false(self):
         # gh-16493