Skip to content

Commit

Permalink
ENH: support Styler in ExcelFormatter
Browse files Browse the repository at this point in the history
closes #1663

Author: Joel Nothman <joel.nothman@gmail.com>

Closes #15530 from jnothman/excel_style and squashes the following commits:

c7a51ca [Joel Nothman] Test currently fails on openpyxl1 due to version incompatibilities
836f39e [Joel Nothman] Revert changes to xlwt
de53808 [Joel Nothman] Remove debug code
a5d51f9 [Joel Nothman] Merge branch 'master' into excel_style
934df06 [Joel Nothman] Display df, not styled
6465913 [Joel Nothman] More pytest-like test_styler_to_excel; enhancements to xlwt
6168765 [Joel Nothman] Recommended changes to what's new
9669d7d [Joel Nothman] Require jinja in test with df.style
14035c5 [Joel Nothman] Merge branch 'master' into excel_style
3071bac [Joel Nothman] Complete tests
ceb9171 [Joel Nothman] reasons for xfails
e2cfa77 [Joel Nothman] Test Styler.to_excel
d5db0ac [Joel Nothman] Remove obsolete TODO
0256fc6 [Joel Nothman] Return after unhandled font size warning
60d6a3b [Joel Nothman] add doc/source/styled.xlsx to the gitignore
4e72993 [Joel Nothman] Fix what's new heading
d144fdf [Joel Nothman] Font name strings
61fdc69 [Joel Nothman] Complete testing basic CSS -> Excel conversions
6ff8a46 [Joel Nothman] Fix loose character; sorry
6d3ffc6 [Joel Nothman] Lint
79eae41 [Joel Nothman] Documentation tweaks
c4f59c6 [Joel Nothman] Doc tweaks
2c3d015 [Joel Nothman] Fix JSON syntax in IPynb
b1d774b [Joel Nothman] What's new heading
096f26c [Joel Nothman] Merge remote-tracking branch 'upstream/master' into excel_style
433be03 [Joel Nothman] Documentation
9a62699 [Joel Nothman] Fix tests and add TODOs to tests
7c54a69 [Joel Nothman] Fix test failures; avoid hair border which renders strangely
8e9a567 [Joel Nothman] Fixes from integration testing
c1fc232 [Joel Nothman] Remove debugging print statements
a43d6b7 [Joel Nothman] Cleaner imports
a1127f6 [Joel Nothman] Merge branch 'master' into excel_style
306eebe [Joel Nothman] Module-level docstring
350eab5 [Joel Nothman] remove spurious blank line
efce9b6 [Joel Nothman] More CSS to Excel testing; define ExcelFormatter.write
f17a0f4 [Joel Nothman] Some border style tests
1a8818f [Joel Nothman] Lint
9a5b791 [Joel Nothman] Fix testing ImportError
1984cab [Joel Nothman] Fix making get_level_lengths non-private
eb02cc1 [Joel Nothman] Fix testing ImportError
3b26087 [Joel Nothman] Make get_level_lengths non-private
f62f02d [Joel Nothman] File restructure
dc953d4 [Joel Nothman] Font size and border width
7db59c0 [Joel Nothman] Test inherited styles in converter
d103f61 [Joel Nothman] Refactoring and initial tests for CSS to Excel
176e51c [Joel Nothman] Fix NameError
c589c35 [Joel Nothman] Fix some lint errors (yes, the code needs testing)
cb5cf02 [Joel Nothman] Fix bug where inherited not being passed; avoid classmethods
0ce72f9 [Joel Nothman] Use inherited font size for em_pt
8780076 [Joel Nothman] Merge branch 'master' into excel_style
96680f9 [Joel Nothman] Largely complete CSSToExcelConverter and Styler.to_excel()
f1cde08 [Joel Nothman] FIX column offset incorrect in refactor
ada5101 [Joel Nothman] ENH: support Styler in ExcelFormatter
  • Loading branch information
jnothman authored and jreback committed Apr 20, 2017
1 parent dd5cef5 commit 1b52b12
Show file tree
Hide file tree
Showing 13 changed files with 1,670 additions and 380 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@ doc/source/index.rst
doc/build/html/index.html
# Windows specific leftover:
doc/tmp.sv
doc/source/styled.xlsx
doc/source/templates/
Binary file added doc/source/_static/style-excel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 56 additions & 21 deletions doc/source/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# HTML Styling\n",
"# Styling\n",
"\n",
"*New in version 0.17.1*\n",
"\n",
"<p style=\"color: red\">*Provisional: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*<p style=\"color: red\">\n",
"<span style=\"color: red\">*Provisional: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*</span>\n",
"\n",
"This document is written as a Jupyter Notebook, and can be viewed or downloaded [here](http://nbviewer.ipython.org/github/pandas-dev/pandas/blob/master/doc/source/html-styling.ipynb).\n",
"\n",
Expand Down Expand Up @@ -49,7 +49,6 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"nbsphinx": "hidden"
},
"outputs": [],
Expand All @@ -62,9 +61,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
Expand Down Expand Up @@ -130,9 +127,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"def color_negative_red(val):\n",
Expand Down Expand Up @@ -186,9 +181,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"def highlight_max(s):\n",
Expand Down Expand Up @@ -240,7 +233,7 @@
"source": [
"Above we used `Styler.apply` to pass in each column one at a time.\n",
"\n",
"<p style=\"background-color: #DEDEBE\">*Debugging Tip*: If you're having trouble writing your style function, try just passing it into <code style=\"background-color: #DEDEBE\">DataFrame.apply</code>. Internally, <code style=\"background-color: #DEDEBE\">Styler.apply</code> uses <code style=\"background-color: #DEDEBE\">DataFrame.apply</code> so the result should be the same.</p>\n",
"<span style=\"background-color: #DEDEBE\">*Debugging Tip*: If you're having trouble writing your style function, try just passing it into <code style=\"background-color: #DEDEBE\">DataFrame.apply</code>. Internally, <code style=\"background-color: #DEDEBE\">Styler.apply</code> uses <code style=\"background-color: #DEDEBE\">DataFrame.apply</code> so the result should be the same.</span>\n",
"\n",
"What if you wanted to highlight just the maximum value in the entire table?\n",
"Use `.apply(function, axis=None)` to indicate that your function wants the entire table, not one column or row at a time. Let's try that next.\n",
Expand All @@ -251,9 +244,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"def highlight_max(data, color='yellow'):\n",
Expand Down Expand Up @@ -819,9 +810,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"def magnify():\n",
Expand Down Expand Up @@ -854,6 +843,53 @@
" .set_table_styles(magnify())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Export to Excel\n",
"\n",
"*New in version 0.20.0*\n",
"\n",
"<span style=\"color: red\">*Experimental: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*</span>\n",
"\n",
"Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` engine. CSS2.2 properties handled include:\n",
"\n",
"- `background-color`\n",
"- `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n",
"- `color`\n",
"- `font-family`\n",
"- `font-style`\n",
"- `font-weight`\n",
"- `text-align`\n",
"- `text-decoration`\n",
"- `vertical-align`\n",
"- `white-space: nowrap`\n",
"\n",
"Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df.style.\\\n",
" applymap(color_negative_red).\\\n",
" apply(highlight_max).\\\n",
" to_excel('styled.xlsx', engine='openpyxl')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A screenshot of the output:\n",
"\n",
"![Excel spreadsheet with styled DataFrame](_static/style-excel.png)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -1039,8 +1075,7 @@
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.1"
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
Expand Down
34 changes: 34 additions & 0 deletions doc/source/whatsnew/v0.20.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Highlights include:
- Improved user API when accessing levels in ``.groupby()``, see :ref:`here <whatsnew_0200.enhancements.groupby_access>`
- Improved support for ``UInt64`` dtypes, see :ref:`here <whatsnew_0200.enhancements.uint64_support>`
- A new orient for JSON serialization, ``orient='table'``, that uses the :ref:`Table Schema spec <whatsnew_0200.enhancements.table_schema>`
- Experimental support for exporting ``DataFrame.style`` formats to Excel , see :ref:`here <whatsnew_0200.enhancements.style_excel>`
- Window Binary Corr/Cov operations now return a MultiIndexed ``DataFrame`` rather than a ``Panel``, as ``Panel`` is now deprecated, see :ref:`here <whatsnew_0200.api_breaking.rolling_pairwise>`
- Support for S3 handling now uses ``s3fs``, see :ref:`here <whatsnew_0200.api_breaking.s3>`
- Google BigQuery support now uses the ``pandas-gbq`` library, see :ref:`here <whatsnew_0200.api_breaking.gbq>`
Expand Down Expand Up @@ -398,6 +399,39 @@ To convert a ``SparseDataFrame`` back to sparse SciPy matrix in COO format, you

sdf.to_coo()

.. _whatsnew_0200.enhancements.style_excel:

Excel output for styled DataFrames
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Experimental support has been added to export ``DataFrame.style`` formats to Excel using the ``openpyxl`` engine. (:issue:`15530`)

For example, after running the following, ``styled.xlsx`` renders as below:

.. ipython:: python

np.random.seed(24)
df = pd.DataFrame({'A': np.linspace(1, 10, 10)})
df = pd.concat([df, pd.DataFrame(np.random.RandomState(24).randn(10, 4),
columns=list('BCDE'))],
axis=1)
df.iloc[0, 2] = np.nan
df
styled = df.style.\
applymap(lambda val: 'color: %s' % 'red' if val < 0 else 'black').\
apply(lambda s: ['background-color: yellow' if v else ''
for v in s == s.max()])
styled.to_excel('styled.xlsx', engine='openpyxl')

.. image:: _static/style-excel.png

.. ipython:: python
:suppress:
import os
os.remove('styled.xlsx')

See the :ref:`Style documentation <style>` for more detail.

.. _whatsnew_0200.enhancements.intervalindex:

IntervalIndex
Expand Down
33 changes: 11 additions & 22 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,28 +1419,17 @@ def to_excel(self, excel_writer, sheet_name='Sheet1', na_rep='',
index_label=None, startrow=0, startcol=0, engine=None,
merge_cells=True, encoding=None, inf_rep='inf', verbose=True,
freeze_panes=None):
from pandas.io.excel import ExcelWriter
need_save = False
if encoding is None:
encoding = 'ascii'

if isinstance(excel_writer, compat.string_types):
excel_writer = ExcelWriter(excel_writer, engine=engine)
need_save = True

formatter = fmt.ExcelFormatter(self, na_rep=na_rep, cols=columns,
header=header,
float_format=float_format, index=index,
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep)

formatted_cells = formatter.get_formatted_cells()
excel_writer.write_cells(formatted_cells, sheet_name,
startrow=startrow, startcol=startcol,
freeze_panes=freeze_panes)
if need_save:
excel_writer.save()

from pandas.io.formats.excel import ExcelFormatter
formatter = ExcelFormatter(self, na_rep=na_rep, cols=columns,
header=header,
float_format=float_format, index=index,
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep)
formatter.write(excel_writer, sheet_name=sheet_name, startrow=startrow,
startcol=startcol, freeze_panes=freeze_panes,
engine=engine)

def to_stata(self, fname, convert_dates=None, write_index=True,
encoding="latin-1", byteorder=None, time_stamp=None,
Expand Down
44 changes: 44 additions & 0 deletions pandas/io/formats/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
Common helper methods used in different submodules of pandas.io.formats
"""


def get_level_lengths(levels, sentinel=''):
"""For each index in each level the function returns lengths of indexes.
Parameters
----------
levels : list of lists
List of values on for level.
sentinel : string, optional
Value which states that no new index starts on there.
Returns
----------
Returns list of maps. For each level returns map of indexes (key is index
in row and value is length of index).
"""
if len(levels) == 0:
return []

control = [True for x in levels[0]]

result = []
for level in levels:
last_index = 0

lengths = {}
for i, key in enumerate(level):
if control[i] and key == sentinel:
pass
else:
control[i] = False
lengths[last_index] = i - last_index
last_index = i

lengths[last_index] = len(level) - last_index

result.append(lengths)

return result
Loading

0 comments on commit 1b52b12

Please sign in to comment.