Skip to content

Commit 80b74a2

Browse files
jnothmanpeterpanmj
authored andcommitted
Support more styles for xlsxwriter (pandas-dev#16149)
1 parent b83f490 commit 80b74a2

File tree

4 files changed

+242
-127
lines changed

4 files changed

+242
-127
lines changed

doc/source/style.ipynb

+1-1
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@
935935
"\n",
936936
"<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",
937937
"\n",
938-
"Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` engine. CSS2.2 properties handled include:\n",
938+
"Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` or `XlsxWriter` engines. CSS2.2 properties handled include:\n",
939939
"\n",
940940
"- `background-color`\n",
941941
"- `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n",

doc/source/whatsnew/v0.22.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ New features
2222
Other Enhancements
2323
^^^^^^^^^^^^^^^^^^
2424

25-
-
25+
- Better support for ``Dataframe.style.to_excel()`` output with the ``xlsxwriter`` engine. (:issue:`16149`)
2626
-
2727
-
2828

pandas/io/excel.py

+146-46
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,149 @@ def _convert_to_style(cls, style_dict, num_format_str=None):
15781578
register_writer(_XlwtWriter)
15791579

15801580

1581+
class _XlsxStyler(object):
1582+
# Map from openpyxl-oriented styles to flatter xlsxwriter representation
1583+
# Ordering necessary for both determinism and because some are keyed by
1584+
# prefixes of others.
1585+
STYLE_MAPPING = {
1586+
'font': [
1587+
(('name',), 'font_name'),
1588+
(('sz',), 'font_size'),
1589+
(('size',), 'font_size'),
1590+
(('color', 'rgb',), 'font_color'),
1591+
(('color',), 'font_color'),
1592+
(('b',), 'bold'),
1593+
(('bold',), 'bold'),
1594+
(('i',), 'italic'),
1595+
(('italic',), 'italic'),
1596+
(('u',), 'underline'),
1597+
(('underline',), 'underline'),
1598+
(('strike',), 'font_strikeout'),
1599+
(('vertAlign',), 'font_script'),
1600+
(('vertalign',), 'font_script'),
1601+
],
1602+
'number_format': [
1603+
(('format_code',), 'num_format'),
1604+
((), 'num_format',),
1605+
],
1606+
'protection': [
1607+
(('locked',), 'locked'),
1608+
(('hidden',), 'hidden'),
1609+
],
1610+
'alignment': [
1611+
(('horizontal',), 'align'),
1612+
(('vertical',), 'valign'),
1613+
(('text_rotation',), 'rotation'),
1614+
(('wrap_text',), 'text_wrap'),
1615+
(('indent',), 'indent'),
1616+
(('shrink_to_fit',), 'shrink'),
1617+
],
1618+
'fill': [
1619+
(('patternType',), 'pattern'),
1620+
(('patterntype',), 'pattern'),
1621+
(('fill_type',), 'pattern'),
1622+
(('start_color', 'rgb',), 'fg_color'),
1623+
(('fgColor', 'rgb',), 'fg_color'),
1624+
(('fgcolor', 'rgb',), 'fg_color'),
1625+
(('start_color',), 'fg_color'),
1626+
(('fgColor',), 'fg_color'),
1627+
(('fgcolor',), 'fg_color'),
1628+
(('end_color', 'rgb',), 'bg_color'),
1629+
(('bgColor', 'rgb',), 'bg_color'),
1630+
(('bgcolor', 'rgb',), 'bg_color'),
1631+
(('end_color',), 'bg_color'),
1632+
(('bgColor',), 'bg_color'),
1633+
(('bgcolor',), 'bg_color'),
1634+
],
1635+
'border': [
1636+
(('color', 'rgb',), 'border_color'),
1637+
(('color',), 'border_color'),
1638+
(('style',), 'border'),
1639+
(('top', 'color', 'rgb',), 'top_color'),
1640+
(('top', 'color',), 'top_color'),
1641+
(('top', 'style',), 'top'),
1642+
(('top',), 'top'),
1643+
(('right', 'color', 'rgb',), 'right_color'),
1644+
(('right', 'color',), 'right_color'),
1645+
(('right', 'style',), 'right'),
1646+
(('right',), 'right'),
1647+
(('bottom', 'color', 'rgb',), 'bottom_color'),
1648+
(('bottom', 'color',), 'bottom_color'),
1649+
(('bottom', 'style',), 'bottom'),
1650+
(('bottom',), 'bottom'),
1651+
(('left', 'color', 'rgb',), 'left_color'),
1652+
(('left', 'color',), 'left_color'),
1653+
(('left', 'style',), 'left'),
1654+
(('left',), 'left'),
1655+
],
1656+
}
1657+
1658+
@classmethod
1659+
def convert(cls, style_dict, num_format_str=None):
1660+
"""
1661+
converts a style_dict to an xlsxwriter format dict
1662+
1663+
Parameters
1664+
----------
1665+
style_dict: style dictionary to convert
1666+
num_format_str: optional number format string
1667+
"""
1668+
1669+
# Create a XlsxWriter format object.
1670+
props = {}
1671+
1672+
if num_format_str is not None:
1673+
props['num_format'] = num_format_str
1674+
1675+
if style_dict is None:
1676+
return props
1677+
1678+
if 'borders' in style_dict:
1679+
style_dict = style_dict.copy()
1680+
style_dict['border'] = style_dict.pop('borders')
1681+
1682+
for style_group_key, style_group in style_dict.items():
1683+
for src, dst in cls.STYLE_MAPPING.get(style_group_key, []):
1684+
# src is a sequence of keys into a nested dict
1685+
# dst is a flat key
1686+
if dst in props:
1687+
continue
1688+
v = style_group
1689+
for k in src:
1690+
try:
1691+
v = v[k]
1692+
except (KeyError, TypeError):
1693+
break
1694+
else:
1695+
props[dst] = v
1696+
1697+
if isinstance(props.get('pattern'), string_types):
1698+
# TODO: support other fill patterns
1699+
props['pattern'] = 0 if props['pattern'] == 'none' else 1
1700+
1701+
for k in ['border', 'top', 'right', 'bottom', 'left']:
1702+
if isinstance(props.get(k), string_types):
1703+
try:
1704+
props[k] = ['none', 'thin', 'medium', 'dashed', 'dotted',
1705+
'thick', 'double', 'hair', 'mediumDashed',
1706+
'dashDot', 'mediumDashDot', 'dashDotDot',
1707+
'mediumDashDotDot', 'slantDashDot'].\
1708+
index(props[k])
1709+
except ValueError:
1710+
props[k] = 2
1711+
1712+
if isinstance(props.get('font_script'), string_types):
1713+
props['font_script'] = ['baseline', 'superscript', 'subscript'].\
1714+
index(props['font_script'])
1715+
1716+
if isinstance(props.get('underline'), string_types):
1717+
props['underline'] = {'none': 0, 'single': 1, 'double': 2,
1718+
'singleAccounting': 33,
1719+
'doubleAccounting': 34}[props['underline']]
1720+
1721+
return props
1722+
1723+
15811724
class _XlsxWriter(ExcelWriter):
15821725
engine = 'xlsxwriter'
15831726
supported_extensions = ('.xlsx',)
@@ -1612,7 +1755,7 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
16121755
wks = self.book.add_worksheet(sheet_name)
16131756
self.sheets[sheet_name] = wks
16141757

1615-
style_dict = {}
1758+
style_dict = {'null': None}
16161759

16171760
if _validate_freeze_panes(freeze_panes):
16181761
wks.freeze_panes(*(freeze_panes))
@@ -1633,7 +1776,8 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
16331776
if stylekey in style_dict:
16341777
style = style_dict[stylekey]
16351778
else:
1636-
style = self._convert_to_style(cell.style, num_format_str)
1779+
style = self.book.add_format(
1780+
_XlsxStyler.convert(cell.style, num_format_str))
16371781
style_dict[stylekey] = style
16381782

16391783
if cell.mergestart is not None and cell.mergeend is not None:
@@ -1647,49 +1791,5 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
16471791
startcol + cell.col,
16481792
val, style)
16491793

1650-
def _convert_to_style(self, style_dict, num_format_str=None):
1651-
"""
1652-
converts a style_dict to an xlsxwriter format object
1653-
Parameters
1654-
----------
1655-
style_dict: style dictionary to convert
1656-
num_format_str: optional number format string
1657-
"""
1658-
1659-
# If there is no formatting we don't create a format object.
1660-
if num_format_str is None and style_dict is None:
1661-
return None
1662-
1663-
# Create a XlsxWriter format object.
1664-
xl_format = self.book.add_format()
1665-
1666-
if num_format_str is not None:
1667-
xl_format.set_num_format(num_format_str)
1668-
1669-
if style_dict is None:
1670-
return xl_format
1671-
1672-
# Map the cell font to XlsxWriter font properties.
1673-
if style_dict.get('font'):
1674-
font = style_dict['font']
1675-
if font.get('bold'):
1676-
xl_format.set_bold()
1677-
1678-
# Map the alignment to XlsxWriter alignment properties.
1679-
alignment = style_dict.get('alignment')
1680-
if alignment:
1681-
if (alignment.get('horizontal') and
1682-
alignment['horizontal'] == 'center'):
1683-
xl_format.set_align('center')
1684-
if (alignment.get('vertical') and
1685-
alignment['vertical'] == 'top'):
1686-
xl_format.set_align('top')
1687-
1688-
# Map the cell borders to XlsxWriter border properties.
1689-
if style_dict.get('borders'):
1690-
xl_format.set_border()
1691-
1692-
return xl_format
1693-
16941794

16951795
register_writer(_XlsxWriter)

0 commit comments

Comments
 (0)