"""Utilities for conversion to writer-agnostic Excel representation
"""
import itertools
import re
import warnings
import numpy as np
from pandas.compat import reduce
from pandas.core.dtypes import missing
from pandas.core.dtypes.common import is_float, is_scalar
from pandas.core.dtypes.generic import ABCMultiIndex, ABCPeriodIndex
from pandas import Index
import pandas.core.common as com
from pandas.io.formats.css import CSSResolver, CSSWarning
from pandas.io.formats.format import get_level_lengths
from pandas.io.formats.printing import pprint_thing
class ExcelCell(object):
__fields__ = ('row', 'col', 'val', 'style', 'mergestart', 'mergeend')
__slots__ = __fields__
def __init__(self, row, col, val, style=None, mergestart=None,
mergeend=None):
self.row = row
self.col = col
self.val = val
self.style = style
self.mergestart = mergestart
self.mergeend = mergeend
class CSSToExcelConverter(object):
"""A callable for converting CSS declarations to ExcelWriter styles
Supports parts of CSS 2.2, with minimal CSS 3.0 support (e.g. text-shadow),
focusing on font styling, backgrounds, borders and alignment.
Operates by first computing CSS styles in a fairly generic
way (see :meth:`compute_css`) then determining Excel style
properties from CSS properties (see :meth:`build_xlstyle`).
Parameters
----------
inherited : str, optional
CSS declarations understood to be the containing scope for the
CSS processed by :meth:`__call__`.
"""
# NB: Most of the methods here could be classmethods, as only __init__
# and __call__ make use of instance attributes. We leave them as
# instancemethods so that users can easily experiment with extensions
# without monkey-patching.
def __init__(self, inherited=None):
if inherited is not None:
inherited = self.compute_css(inherited,
self.compute_css.INITIAL_STYLE)
self.inherited = inherited
compute_css = CSSResolver()
def __call__(self, declarations_str):
"""Convert CSS declarations to ExcelWriter style
Parameters
----------
declarations_str : str
List of CSS declarations.
e.g. "font-weight: bold; background: blue"
Returns
-------
xlstyle : dict
A style as interpreted by ExcelWriter when found in
ExcelCell.style.
"""
# TODO: memoize?
properties = self.compute_css(declarations_str, self.inherited)
return self.build_xlstyle(properties)
def build_xlstyle(self, props):
out = {
'alignment': self.build_alignment(props),
'border': self.build_border(props),
'fill': self.build_fill(props),
'font': self.build_font(props),
'number_format': self.build_number_format(props),
}
# TODO: handle cell width and height: needs support in pandas.io.excel
def remove_none(d):
"""Remove key where value is None, through nested dicts"""
for k, v in list(d.items()):
if v is None:
del d[k]
elif isinstance(v, dict):
remove_none(v)
if not v:
del d[k]
remove_none(out)
return out
VERTICAL_MAP = {
'top': 'top',
'text-top': 'top',
'middle': 'center',
'baseline': 'bottom',
'bottom': 'bottom',
'text-bottom': 'bottom',
# OpenXML also has 'justify', 'distributed'
}
def build_alignment(self, props):
# TODO: text-indent, padding-left -> alignment.indent
return {'horizontal': props.get('text-align'),
'vertical': self.VERTICAL_MAP.get(props.get('vertical-align')),
'wrap_text': (None if props.get('white-space') is None else
props['white-space'] not in
('nowrap', 'pre', 'pre-line'))
}
def build_border(self, props):
return {side: {
'style': self._border_style(props.get('border-{side}-style'
.format(side=side)),
props.get('border-{side}-width'
.format(side=side))),
'color': self.color_to_excel(
props.get('border-{side}-color'.format(side=side))),
} for side in ['top', 'right', 'bottom', 'left']}
def _border_style(self, style, width):
# convert styles and widths to openxml, one of:
# 'dashDot'
# 'dashDotDot'
# 'dashed'
# 'dotted'
# 'double'
# 'hair'
# 'medium'
# 'mediumDashDot'
# 'mediumDashDotDot'
# 'mediumDashed'
# 'slantDashDot'
# 'thick'
# 'thin'
if width is None and style is None:
return None
if style == 'none' or style == 'hidden':
return None
if width is None:
width = '2pt'
width = float(width[:-2])
if width < 1e-5:
return None
elif width < 1.3:
width_name = 'thin'
elif width < 2.8:
width_name = 'medium'
else:
width_name = 'thick'
if style in (None, 'groove', 'ridge', 'inset', 'outset'):
# not handled
style = 'solid'
if style == 'double':
return 'double'
if style == 'solid':
return width_name
if style == 'dotted':
if width_name in ('hair', 'thin'):
return 'dotted'
return 'mediumDashDotDot'
if style == 'dashed':
if width_name in ('hair', 'thin'):
return 'dashed'
return 'mediumDashed'
def build_fill(self, props):
# TODO: perhaps allow for special properties
# -excel-pattern-bgcolor and -excel-pattern-type
fill_color = props.get('background-color')
if fill_color not in (None, 'transparent', 'none'):
return {
'fgColor': self.color_to_excel(fill_color),
'patternType': 'solid',
}
BOLD_MAP = {'bold': True, 'bolder': True, '600': True, '700': True,
'800': True, '900': True,
'normal': False, 'lighter': False, '100': False, '200': False,
'300': False, '400': False, '500': False}
ITALIC_MAP = {'normal': False, 'italic': True, 'oblique': True}
def build_font(self, props):
size = props.get('font-size')
if size is not None:
assert size.endswith('pt')
size = float(size[:-2])
font_names_tmp = re.findall(r'''(?x)
(
"(?:[^"]|\\")+"
|
'(?:[^']|\\')+'
|
[^'",]+
)(?=,|\s*$)
''', props.get('font-family', ''))
font_names = []
for name in font_names_tmp:
if name[:1] == '"':
name = name[1:-1].replace('\\"', '"')
elif name[:1] == '\'':
name = name[1:-1].replace('\\\'', '\'')
else:
name = name.strip()
if name:
font_names.append(name)
family = None
for name in font_names:
if name == 'serif':
family = 1 # roman
break
elif name == 'sans-serif':
family = 2 # swiss
break
elif name == 'cursive':
family = 4 # script
break
elif name == 'fantasy':
family = 5 # decorative
break
decoration = props.get('text-decoration')
if decoration is not None:
decoration = decoration.split()
else:
decoration = ()
return {
'name': font_names[0] if font_names else None,
'family': family,
'size': size,
'bold': self.BOLD_MAP.get(props.get('font-weight')),
'italic': self.ITALIC_MAP.get(props.get('font-style')),
'underline': ('single' if
'underline' in decoration
else None),
'strike': ('line-through' in decoration) or None,
'color': self.color_to_excel(props.get('color')),
# shadow if nonzero digit before shadow color
'shadow': (bool(re.search('^[^#(]*[1-9]',
props['text-shadow']))
if 'text-shadow' in props else None),
# 'vertAlign':,
# 'charset': ,
# 'scheme': ,
# 'outline': ,
# 'condense': ,
}
NAMED_COLORS = {
'maroon': '800000',
'brown': 'A52A2A',
'red': 'FF0000',
'pink': 'FFC0CB',
'orange': 'FFA500',
'yellow': 'FFFF00',
'olive': '808000',
'green': '008000',
'purple': '800080',
'fuchsia': 'FF00FF',
'lime': '00FF00',
'teal': '008080',
'aqua': '00FFFF',
'blue': '0000FF',
'navy': '000080',
'black': '000000',
'gray': '808080',
'grey': '808080',
'silver': 'C0C0C0',
'white': 'FFFFFF',
}
def color_to_excel(self, val):
if val is None:
return None
if val.startswith('#') and len(val) == 7:
return val[1:].upper()
if val.startswith('#') and len(val) == 4:
return (val[1] * 2 + val[2] * 2 + val[3] * 2).upper()
try:
return self.NAMED_COLORS[val]
except KeyError:
warnings.warn('Unhandled color format: {val!r}'.format(val=val),
CSSWarning)
def build_number_format(self, props):
return {'format_code': props.get('number-format')}
class ExcelFormatter(object):
"""
Class for formatting a DataFrame to a list of ExcelCells,
Parameters
----------
df : DataFrame or Styler
na_rep: na representation
float_format : string, default None
Format string for floating point numbers
cols : sequence, optional
Columns to write
header : boolean or list of string, default True
Write out column names. If a list of string is given it is
assumed to be aliases for the column names
index : boolean, default True
output row names (index)
index_label : string or sequence, default None
Column label for index column(s) if desired. If None is given, and
`header` and `index` are True, then the index names are used. A
sequence should be given if the DataFrame uses MultiIndex.
merge_cells : boolean, default False
Format MultiIndex and Hierarchical Rows as merged cells.
inf_rep : string, default `'inf'`
representation for np.inf values (which aren't representable in Excel)
A `'-'` sign will be added in front of -inf.
style_converter : callable, optional
This translates Styler styles (CSS) into ExcelWriter styles.
Defaults to ``CSSToExcelConverter()``.
It should have signature css_declarations string -> excel style.
This is only called for body cells.
"""
Loading ...