Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

agriconnect / pandas   python

Repository URL to install this package:

/ io / formats / excel.py

"""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 ...