From f533a3c88459b9c7e1a7fb4cb0cc8048189ddf20 Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Mon, 5 Aug 2019 13:42:58 +0200 Subject: [PATCH 1/2] bump version to 0.31-dev --- condarecipe/larray/meta.yaml | 4 +- doc/source/changes.rst | 8 ++++ doc/source/changes/version_0_31.rst.inc | 58 +++++++++++++++++++++++++ larray/__init__.py | 2 +- setup.py | 2 +- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 doc/source/changes/version_0_31.rst.inc diff --git a/condarecipe/larray/meta.yaml b/condarecipe/larray/meta.yaml index 159ed36c1..45a29ae36 100644 --- a/condarecipe/larray/meta.yaml +++ b/condarecipe/larray/meta.yaml @@ -1,9 +1,9 @@ package: name: larray - version: 0.30 + version: 0.31-dev source: - git_tag: 0.30 + git_tag: 0.31-dev git_url: https://github.com/larray-project/larray.git # git_tag: master # git_url: file://c:/Users/gdm/devel/larray/.git diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 65762e33e..53d972b8f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -1,6 +1,14 @@ Change log ########## +Version 0.31 +============ + +In development. + +.. include:: ./changes/version_0_31.rst.inc + + Version 0.30 ============ diff --git a/doc/source/changes/version_0_31.rst.inc b/doc/source/changes/version_0_31.rst.inc new file mode 100644 index 000000000..bbb2f6750 --- /dev/null +++ b/doc/source/changes/version_0_31.rst.inc @@ -0,0 +1,58 @@ +.. py:currentmodule:: larray + + +Syntax changes +^^^^^^^^^^^^^^ + +* renamed ``LArray.old_method_name()`` to :py:obj:`LArray.new_method_name()` (closes :issue:`1`). + +* renamed ``old_argument_name`` argument of :py:obj:`LArray.method_name()` to ``new_argument_name``. + + +Backward incompatible changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* other backward incompatible changes + + +New features +^^^^^^^^^^^^ + +* added a feature (see the :ref:`miscellaneous section ` for details). It works on :ref:`api-axis` and + :ref:`api-group` objects. + + Here is an example of the new feature: + + >>> arr = ndtest((2, 3)) + >>> arr + a\b b0 b1 b2 + a0 0 1 2 + a1 3 4 5 + + And it can also be used like this: + + >>> arr = ndtest("a=a0..a2") + >>> arr + a a0 a1 a2 + 0 1 2 + +* added another feature in the editor (closes :editor_issue:`1`). + + .. note:: + + - It works for foo bar ! + - It does not work for foo baz ! + + +.. _misc: + +Miscellaneous improvements +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* improved something. + + +Fixes +^^^^^ + +* fixed something (closes :issue:`1`). diff --git a/larray/__init__.py b/larray/__init__.py index a8e04ac6a..89d8c271a 100644 --- a/larray/__init__.py +++ b/larray/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, division, print_function -__version__ = '0.30' +__version__ = '0.31-dev' from larray.core.axis import Axis, AxisCollection, X diff --git a/setup.py b/setup.py index e741ff50c..ced0e58ef 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def readlocal(fname): DISTNAME = 'larray' -VERSION = '0.30' +VERSION = '0.31-dev' AUTHOR = 'Gaetan de Menten, Geert Bryon, Johan Duyck, Alix Damman' AUTHOR_EMAIL = 'gdementen@gmail.com' DESCRIPTION = "N-D labeled arrays in Python" From c5c62fe7fb02b40449dc07dfb1d39a28a7ebbb6c Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Wed, 5 Dec 2018 10:17:54 +0100 Subject: [PATCH 2/2] fix #676 : implemented ExcelReport class to generate an Excel file with multiple graphs at once --- MANIFEST.in | 3 +- doc/source/api.rst | 31 + doc/source/changes/version_0_31.rst.inc | 26 +- larray/__init__.py | 8 +- larray/example.py | 8 +- larray/inout/xw_reporting.py | 765 +++++++++++++++++++ larray/tests/excel_template/Line.crtx | Bin 0 -> 7584 bytes larray/tests/excel_template/Line_Marker.crtx | Bin 0 -> 7861 bytes larray/tests/test_excel.py | 145 +++- larray/util/misc.py | 15 + larray/util/options.py | 7 +- setup.cfg | 5 +- 12 files changed, 975 insertions(+), 38 deletions(-) create mode 100644 larray/inout/xw_reporting.py create mode 100644 larray/tests/excel_template/Line.crtx create mode 100644 larray/tests/excel_template/Line_Marker.crtx diff --git a/MANIFEST.in b/MANIFEST.in index 8832b34c5..0694b399f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include COPYING -recursive-include larray/tests/data *.csv *.xlsx *.h5 \ No newline at end of file +recursive-include larray/tests/data *.csv *.xlsx *.h5 +recursive-include larray/tests/excel_template *.crtx \ No newline at end of file diff --git a/doc/source/api.rst b/doc/source/api.rst index eb830a760..9f4d8363b 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -675,6 +675,37 @@ Excel Workbook.close Workbook.app +ExcelReport +=========== + +.. autosummary:: + :toctree: _generated/ + + ExcelReport + ExcelReport.template_dir + ExcelReport.template + ExcelReport.set_item_default_size + ExcelReport.graphs_per_row + ExcelReport.new_sheet + ExcelReport.sheet_names + ExcelReport.to_excel + +ReportSheet +=========== + +.. autosummary:: + :toctree: _generated/ + + ReportSheet + ReportSheet.template_dir + ReportSheet.template + ReportSheet.set_item_default_size + ReportSheet.graphs_per_row + ReportSheet.add_title + ReportSheet.add_graph + ReportSheet.add_graphs + ReportSheet.newline + .. _api-misc: Miscellaneous diff --git a/doc/source/changes/version_0_31.rst.inc b/doc/source/changes/version_0_31.rst.inc index bbb2f6750..6d6f1fde9 100644 --- a/doc/source/changes/version_0_31.rst.inc +++ b/doc/source/changes/version_0_31.rst.inc @@ -18,30 +18,8 @@ Backward incompatible changes New features ^^^^^^^^^^^^ -* added a feature (see the :ref:`miscellaneous section ` for details). It works on :ref:`api-axis` and - :ref:`api-group` objects. - - Here is an example of the new feature: - - >>> arr = ndtest((2, 3)) - >>> arr - a\b b0 b1 b2 - a0 0 1 2 - a1 3 4 5 - - And it can also be used like this: - - >>> arr = ndtest("a=a0..a2") - >>> arr - a a0 a1 a2 - 0 1 2 - -* added another feature in the editor (closes :editor_issue:`1`). - - .. note:: - - - It works for foo bar ! - - It does not work for foo baz ! +* added the :py:obj:`ExcelReport` class allowing to generate multiple graphs in an + Excel file at once (closes :issue:`676`). .. _misc: diff --git a/larray/__init__.py b/larray/__init__.py index 89d8c271a..9f675ecea 100644 --- a/larray/__init__.py +++ b/larray/__init__.py @@ -30,6 +30,7 @@ from larray.inout.sas import read_sas from larray.inout.stata import read_stata from larray.inout.xw_excel import open_excel, Workbook +from larray.inout.xw_reporting import ExcelReport # just make sure handlers for .pkl and .pickle are initialized import larray.inout.pickle as _pkl @@ -41,7 +42,7 @@ from larray.extra.ipfp import ipfp -from larray.example import get_example_filepath, load_example_data +from larray.example import get_example_filepath, load_example_data, EXAMPLE_EXCEL_TEMPLATES_DIR import larray.random @@ -75,7 +76,8 @@ 'real_if_close', 'interp', 'isnan', 'isinf', 'inverse', # inout 'from_lists', 'from_string', 'from_frame', 'from_series', 'read_csv', 'read_tsv', - 'read_eurostat', 'read_excel', 'read_hdf', 'read_sas', 'read_stata', 'open_excel', 'Workbook', + 'read_eurostat', 'read_excel', 'read_hdf', 'read_sas', 'read_stata', + 'open_excel', 'Workbook', 'ExcelReport', # utils 'get_options', 'set_options', # viewer @@ -83,7 +85,7 @@ # ipfp 'ipfp', # example - 'get_example_filepath', 'load_example_data', + 'get_example_filepath', 'load_example_data', 'EXAMPLE_EXCEL_TEMPLATES_DIR', ] diff --git a/larray/example.py b/larray/example.py index 302b74be8..a116998f0 100644 --- a/larray/example.py +++ b/larray/example.py @@ -2,12 +2,18 @@ import larray as la -EXAMPLE_FILES_DIR = os.path.dirname(__file__) + '/tests/data/' +_TEST_DIR = os.path.join(os.path.dirname(__file__), 'tests') + +EXAMPLE_FILES_DIR = os.path.join(_TEST_DIR, 'data') +# TODO : replace 'demography.h5' by 'population_session.h5' and remove 'demo' ? AVAILABLE_EXAMPLE_DATA = { + 'demo': os.path.join(EXAMPLE_FILES_DIR, 'population_session.h5'), 'demography': os.path.join(EXAMPLE_FILES_DIR, 'demography.h5') } AVAILABLE_EXAMPLE_FILES = os.listdir(EXAMPLE_FILES_DIR) +EXAMPLE_EXCEL_TEMPLATES_DIR = os.path.join(_TEST_DIR, 'excel_template') + def get_example_filepath(fname): """Return absolute path to an example file if exist. diff --git a/larray/inout/xw_reporting.py b/larray/inout/xw_reporting.py new file mode 100644 index 000000000..8c4470ef7 --- /dev/null +++ b/larray/inout/xw_reporting.py @@ -0,0 +1,765 @@ +import os +import warnings +from collections import OrderedDict + +from larray.util.misc import PY2, _positive_integer, _validate_dir +from larray.core.group import _translate_sheet_name +from larray.core.array import aslarray, zip_array_items +from larray.example import load_example_data, EXAMPLE_EXCEL_TEMPLATES_DIR + +try: + import xlwings as xw +except ImportError: + xw = None + + +_default_items_size = {} + + +def _validate_template_filename(filename): + filename, ext = os.path.splitext(filename) + if not ext: + ext = '.crtx' + if ext != '.crtx': + raise ValueError("Extension for the excel template file must be '.crtx' " + "instead of {}".format(ext)) + return filename + ext + + +class AbstractReportItem(object): + def __init__(self, template_dir=None, template=None, graphs_per_row=1): + self.template_dir = template_dir + self.template = template + self.default_items_size = _default_items_size.copy() + self.graphs_per_row = graphs_per_row + + @property + def template_dir(self): + r""" + Set the path to the directory containing the Excel template files (with '.crtx' extension). + This method is mainly useful if your template files are located in several directories, + otherwise pass the template directory directly the ExcelReport constructor. + + Parameters + ---------- + template_dir : str + Path to the directory containing the Excel template files. + + See Also + -------- + set_graph_template + + Examples + -------- + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + >>> # ... add some graphs using template files from 'C:\excel_templates_dir' + >>> report.template_dir = r'C:\other_templates_dir' # doctest: +SKIP + >>> # ... add some graphs using template files from 'C:\other_templates_dir' + """ + return self._template_dir + + @template_dir.setter + def template_dir(self, template_dir): + if template_dir is not None: + _validate_dir(template_dir) + template_dir = template_dir + self._template_dir = template_dir + + @property + def template(self): + r""" + Set a default Excel template file. + + Parameters + ---------- + template_file : str + Name of the template to be used as default template. + The extension '.crtx' will be added if not given. + The full path to the template file must be given if no template directory has been set. + + Examples + -------- + >>> demo = load_example_data('demo') + + Passing the name of the template (only if a template directory has been set) + + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + >>> report.template = 'Line' + + >>> sheet_pop = report.new_sheet('Population') + >>> sheet_pop.add_graph(demo.pop['Belgium'],'Belgium') + + Passing the full path of the template file + + >>> # if no default template directory has been set + >>> # or if the new template is located in another directory, + >>> # you must provide the full path + >>> sheet_pop.template = r'C:\other_templates_dir\Line_Marker.crtx' # doctest: +SKIP + >>> sheet_pop.add_graph(demo.pop['Germany'],'Germany') # doctest: +SKIP + """ + return self._template + + @template.setter + def template(self, template): + if template is not None: + if self.template_dir is None: + raise RuntimeError("Please set template_dir first") + filename = _validate_template_filename(template) + template = os.path.join(self.template_dir, filename) + self._template = template + + def set_item_default_size(self, kind, width=None, height=None): + r""" + Override the default 'width' and 'height' values for the given kind of item. + A new value must be provided at least for 'width' or 'height'. + + Parameters + ---------- + kind : str + kind of item for which default values of 'width' and/or 'height' are modified. + Currently available kinds are 'title' and 'graph'. + width : int, optional + new default width value. + height : int, optional + new default height value. + + Examples + -------- + >>> report = ExcelReport() + >>> report.set_item_default_size('graph', width=450, height=250) + """ + if width is None and height is None: + raise ValueError("No value provided for both 'width' and 'heigth'. " + "Please provide one for at least 'width' or 'heigth'") + if kind not in self.default_items_size: + raise ValueError("Type item {} is not registered. Please choose in " + "list {}".format(kind, self.default_items_size.keys())) + if width is None: + width = self.default_items_size[kind].width + if height is None: + height = self.default_items_size[kind].height + self.default_items_size[kind] = ItemSize(width, height) + + @property + def graphs_per_row(self): + r""" + Default number of graphs per row. + + Parameters + ---------- + graphs_per_row: int + + See Also + -------- + :py:obj:``~AbstractSheetReport.newline` + """ + return self._graphs_per_row + + @graphs_per_row.setter + def graphs_per_row(self, graphs_per_row): + _positive_integer(graphs_per_row) + self._graphs_per_row = graphs_per_row + + +class AbstractReportSheet(AbstractReportItem): + r""" + Represents a sheet dedicated to contains only graphical items (title banners, graphs). + See :py:obj:`ExcelReport` for use cases. + + Parameters + ---------- + template_dir : str, optional + Path to the directory containing the Excel template files (with a '.crtx' extension). + Defaults to None. + template : str, optional + Name of the template to be used as default template. + The extension '.crtx' will be added if not given. + The full path to the template file must be given if no template directory has been set. + Defaults to None. + graphs_per_row: int, optional + Default number of graphs per row. + Defaults to 1. + + See Also + -------- + ExcelReport + """ + def add_title(self, title, width=None, height=None, fontsize=11): + r""" + Add a title item to the current sheet. + Note that the current method only add a new item to the list of items to be generated. + The report Excel file is generated only when the :py:obj:`~ExcelReport.to_excel` is called. + + Parameters + ---------- + title : str + Text to write in the title item. + width : int, optional + width of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + height : int, optional + height of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + fontsize : int, optional + fontsize of the displayed text. Defaults to 11. + + Examples + -------- + >>> report = ExcelReport() + + >>> first_sheet = report.new_sheet('First_sheet') + >>> first_sheet.add_title('Title banner with default width, height and fontsize') + >>> first_sheet.add_title('Larger title banner', width=1200, height=100) + >>> first_sheet.add_title('Bigger fontsize', fontsize=13) + + >>> # do not forget to call 'to_excel' to create the report file + >>> report.to_excel('Report.xlsx') + """ + pass + + def add_graph(self, data, title=None, template=None, width=None, height=None): + r""" + Add a graph item to the current sheet. + Note that the current method only add a new item to the list of items to be generated. + The report Excel file is generated only when the :py:obj:`~ExcelReport.to_excel` is called. + + Parameters + ---------- + data : 1D or 2D array-like + 1D or 2D array representing the data associated with the graph. + The first row represents the abscissa labels. + Each additional row represents a new series and must start with the name of the current series. + title : str, optional + title of the graph. Defaults to None. + template : str, optional + name of the template to be used to generate the graph. + The full path to the template file must be provided if no template directory has not been set + or if the template file belongs to another directory. + Defaults to the defined template (see :py:obj:`~ExcelReport.set_graph_template`). + width : int, optional + width of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + height : int, optional + height of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + + Examples + -------- + >>> demo = load_example_data('demo') + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + + >>> sheet_be = report.new_sheet('Belgium') + + Specifying the 'template' + + >>> sheet_be.add_graph(demo.pop['Belgium'], 'Population', template='Line') + + Specifying the 'template', 'width' and 'height' values + + >>> sheet_be.add_graph(demo.births['Belgium'], 'Births', template='Line', width=450, height=250) + + Setting a default template + + >>> sheet_be.template = 'Line_Marker' + >>> sheet_be.add_graph(demo.deaths['Belgium'], 'Deaths') + + Dumping the report Excel file + + >>> # do not forget to call 'to_excel' to create the report file + >>> report.to_excel('Demography_Report.xlsx') + """ + pass + + def add_graphs(self, array_per_title, axis_per_loop_variable, template=None, width=None, height=None, + graphs_per_row=1): + r""" + + Parameters + ---------- + array_per_title: dict + dictionary containing pairs (title template, array). + axis_per_loop_variable: dict + dictionary containing pairs (variable used in the title template, axis). + template : str, optional + name of the template to be used to generate the graph. + The full path to the template file must be provided if no template directory has not been set + or if the template file belongs to another directory. + Defaults to the defined template (see :py:obj:`~ExcelReport.set_graph_template`). + width : int, optional + width of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + height : int, optional + height of the title item. The current default value is used if None + (see :py:obj:`~ExcelReport.set_item_default_size`). Defaults to None. + graphs_per_row: int, optional + Number of graphs per row. Defaults to 1. + + + Examples + -------- + >>> demo = load_example_data('demo') + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + + >>> sheet_pop = report.new_sheet('Population') + >>> pop = demo.pop + + >>> sheet_pop.add_graphs({'Population of {gender} by country for the year {year}': pop}, + ... {'gender': pop.gender, 'year': pop.time}, + ... template='line', width=450, height=250, graphs_per_row=2) + + >>> # do not forget to call 'to_excel' to create the report file + >>> report.to_excel('Demography_Report.xlsx') + """ + pass + + def newline(self): + r""" + Force a new row of graphs. + """ + pass + + +class AbstractExcelReport(AbstractReportItem): + r""" + Automate the generation of multiple graphs in an Excel file. + + The ExcelReport instance is initially populated with information + (data, title, destination sheet, template, size) required to create the graphs. + Once all information has been provided, the :py:obj:`~ExcelReport.to_excel` method + is called to generate an Excel file with all graphs in one step. + + Parameters + ---------- + template_dir : str, optional + Path to the directory containing the Excel template files (with a '.crtx' extension). + Defaults to None. + template : str, optional + Name of the template to be used as default template. + The extension '.crtx' will be added if not given. + The full path to the template file must be given if no template directory has been set. + Defaults to None. + graphs_per_row: int, optional + Default number of graphs per row. + Defaults to 1. + + Notes + ----- + The data associated with all graphical items is dumped in the same sheet named '__data__'. + + Examples + -------- + >>> demo = load_example_data('demo') + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + + Set a new destination sheet + + >>> sheet_be = report.new_sheet('Belgium') + + Add a new title item + + >>> sheet_be.add_title('Population, births and deaths') + + Add a new graph item (each new graph is placed right to previous one unless you use newline() or add_title()) + + >>> # using default 'width' and 'height' values + >>> sheet_be.add_graph(demo.pop['Belgium'], 'Population', template='Line') + >>> # specifying the 'width' and 'height' values + >>> sheet_be.add_graph(demo.births['Belgium'], 'Births', template='Line', width=450, height=250) + + Override the default 'width' and 'height' values for graphs + + >>> sheet_be.set_item_default_size('graph', width=450, height=250) + >>> # add a new graph with the new default 'width' and 'height' values + >>> sheet_be.add_graph(demo.deaths['Belgium'], 'Deaths') + + Set a default template for all next graphs + + >>> # if a default template directory has been set, just pass the name + >>> sheet_be.template = 'Line' + >>> # otherwise, give the full path to the template file + >>> sheet_be.template = r'C:\other_template_dir\Line_Marker.crtx' # doctest: +SKIP + >>> # add a new graph with the default template + >>> sheet_be.add_graph(demo.pop['Belgium', 'Female'], 'Population - Female') + >>> sheet_be.add_graph(demo.pop['Belgium', 'Male'], 'Population - Male') + + Specify the number of graphs per row + + >>> sheet_countries = report.new_sheet('All countries') + + >>> sheet_countries.graphs_per_row = 2 + >>> for combined_labels, subset in demo.pop.items(('time', 'gender')): + ... title = ' - '.join([str(label) for label in combined_labels]) + ... sheet_countries.add_graph(subset, title) + + Force a new row of graphs + + >>> sheet_countries.newline() + + Add multiple graphs at once + + >>> sheet_countries.add_graphs({'Population of {gender} by country for the year {year}': pop}, + ... {'gender': pop.gender, 'year': pop.time}, + ... template='line', width=450, height=250, graphs_per_row=2) + + Generate the report Excel file + + >>> report.to_excel('Demography_Report.xlsx') + """ + + def new_sheet(self, sheet_name): + r""" + Add a new empty output sheet. + This sheet will contain only graphical elements, all data are exported + to a dedicated separate sheet. + + Parameters + ---------- + sheet_name : str + name of the current sheet. + + Returns + ------- + sheet: SheetReport + + Examples + -------- + >>> demo = load_example_data('demo') + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + + >>> # prepare new output sheet named 'Belgium' + >>> sheet_be = report.new_sheet('Belgium') + + >>> # add graph to the output sheet 'Belgium' + >>> sheet_be.add_graph(demo.pop['Belgium'], 'Population', template='Line') + """ + pass + + def sheet_names(self): + r""" + Returns the names of the output sheets. + + Examples + -------- + >>> report = ExcelReport() + >>> sheet_pop = report.new_sheet('Pop') + >>> sheet_births = report.new_sheet('Births') + >>> sheet_deaths = report.new_sheet('Deaths') + >>> report.sheet_names() + ['Pop', 'Births', 'Deaths'] + """ + pass + + def to_excel(self, filepath, data_sheet_name='__data__', overwrite=True): + r""" + Generate the report Excel file. + + Parameters + ---------- + filepath : str + Path of the report file for the dump. + data_sheet_name : str, optional + name of the Excel sheet where all data associated with items is dumped. + Defaults to '__data__'. + overwrite : bool, optional + whether or not to overwrite an existing report file. + Defaults to True. + + Examples + -------- + >>> demo = load_example_data('demo') + >>> report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + >>> report.template = 'Line_Marker' + + >>> for c in demo.country: + ... sheet_country = report.new_sheet(c) + ... sheet_country.add_graph(demo.pop[c], 'Population') + ... sheet_country.add_graph(demo.births[c], 'Births') + ... sheet_country.add_graph(demo.deaths[c], 'Deaths') + + Basic usage + + >>> report.to_excel('Demography_Report.xlsx') + + Alternative data sheet name + + >>> report.to_excel('Demography_Report.xlsx', data_sheet_name='Data Tables') # doctest: +SKIP + + Check if ouput file already exists + + >>> report.to_excel('Demography_Report.xlsx', overwrite=False) # doctest: +SKIP + Traceback (most recent call last): + ... + ValueError: Sheet named 'Belgium' already present in workbook + """ + pass + + +if xw is not None: + from xlwings.constants import LegendPosition, HAlign, VAlign, ChartType, RowCol + from larray.inout.xw_excel import open_excel + + class ItemSize(object): + def __init__(self, width, height): + self.width = width + self.height = height + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + _positive_integer(width) + self._width = width + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + _positive_integer(height) + self._height = height + + + class ExcelTitleItem(ItemSize): + + _default_size = ItemSize(1000, 50) + + def __init__(self, text, fontsize, top, left, width, height): + ItemSize.__init__(self, width, height) + self.top = top + self.left = left + self.text = str(text) + _positive_integer(fontsize) + self.fontsize = fontsize + + def dump(self, sheet, data_sheet, row): + data_cells = data_sheet.Cells + # add title in data sheet + data_cells(row, 1).Value = self.text + # generate title banner in destination sheet + msoShapeRectangle = 1 + msoThemeColorBackground1 = 14 + sheet_shapes = sheet.Shapes + shp = sheet_shapes.AddShape(Type=msoShapeRectangle, Left=self.left, Top=self.top, + Width=self.width, Height=self.height) + fill = shp.Fill + fill.ForeColor.ObjectThemeColor = msoThemeColorBackground1 + fill.Solid() + shp.Line.Visible = False + frame = shp.TextFrame + chars = frame.Characters() + chars.Text = self.text + font = chars.Font + font.Color = 1 + font.Bold = True + font.Size = self.fontsize + frame.HorizontalAlignment = HAlign.xlHAlignLeft + frame.VerticalAlignment = VAlign.xlVAlignCenter + shp.SetShapesDefaultProperties() + # update and return current row position + return row + 2 + + _default_items_size['title'] = ExcelTitleItem._default_size + + class ExcelGraphItem(ItemSize): + + _default_size = ItemSize(427, 230) + + def __init__(self, data, title, template, top, left, width, height): + ItemSize.__init__(self, width, height) + self.top = top + self.left = left + self.title = str(title) if title is not None else None + data = aslarray(data) + if not (1 <= data.ndim <= 2): + raise ValueError("Expected 1D or 2D array for data argument. " + "Got array of dimensions {}".format(data.ndim)) + self.data = data + if template is not None and not os.path.isfile(template): + raise ValueError("Could not find template file {}".format(template)) + self.template = template + + def dump(self, sheet, data_sheet, row): + data_range = data_sheet.Range + data_cells = data_sheet.Cells + # write graph title in data sheet + data_cells(row, 1).Value = self.title + row += 1 + # dump data to make the graph in data sheet + data = self.data + nb_series = 1 if data.ndim == 1 else data.shape[0] + nb_xticks = data.size if data.ndim == 1 else data.shape[1] + last_row, last_col = row + nb_series, nb_xticks + 1 + data_range(data_cells(row, 1), data_cells(last_row, last_col)).Value = data.dump(na_repr=None) + data_cells(row, 1).Value = '' + # generate graph in destination sheet + sheet_charts = sheet.ChartObjects() + obj = sheet_charts.Add(self.left, self.top, self.width, self.height) + obj_chart = obj.Chart + source = data_range(data_cells(row, 1), data_cells(last_row, last_col)) + obj_chart.SetSourceData(source) + obj_chart.ChartType = ChartType.xlLine + if self.title is not None: + obj_chart.HasTitle = True + obj_chart.ChartTitle.Caption = self.title + obj_chart.Legend.Position = LegendPosition.xlLegendPositionBottom + if self.template is not None: + obj_chart.ApplyChartTemplate(self.template) + # flagflip + if nb_series > 1 and nb_xticks == 1: + obj_chart.PlotBy = RowCol.xlRows + # update and return current row position + return row + nb_series + 2 + + _default_items_size['graph'] = ExcelGraphItem._default_size + + class ReportSheet(AbstractReportSheet): + def __init__(self, excel_report, name, template_dir=None, template=None, graphs_per_row=1): + name = _translate_sheet_name(name) + self.excel_report = excel_report + self.name = name + self.items = [] + self.top = 0 + self.left = 0 + self.position_in_row = 1 + self.curline_height = 0 + if template_dir is None: + template_dir = excel_report.template_dir + if template is None: + template = excel_report.template + AbstractReportSheet.__init__(self, template_dir, template, graphs_per_row) + + def add_title(self, title, width=None, height=None, fontsize=11): + if width is None: + width = self.default_items_size['title'].width + if height is None: + height = self.default_items_size['title'].height + self.newline() + self.items.append(ExcelTitleItem(title, fontsize, self.top, 0, width, height)) + self.top += height + + def add_graph(self, data, title=None, template=None, width=None, height=None): + if width is None: + width = self.default_items_size['graph'].width + if height is None: + height = self.default_items_size['graph'].height + if template is not None: + self.template = template + template = self.template + if self.graphs_per_row is not None and self.position_in_row > self.graphs_per_row: + self.newline() + self.items.append(ExcelGraphItem(data, title, template, self.top, self.left, width, height)) + self.left += width + self.curline_height = max(self.curline_height, height) + self.position_in_row += 1 + + def add_graphs(self, array_per_title, axis_per_loop_variable, template=None, width=None, height=None, + graphs_per_row=1): + loop_variable_names = axis_per_loop_variable.keys() + axes = tuple(axis_per_loop_variable.values()) + titles = array_per_title.keys() + arrays = array_per_title.values() + if graphs_per_row is not None: + previous_graphs_per_row = self.graphs_per_row + self.graphs_per_row = graphs_per_row + if self.position_in_row > 1: + self.newline() + for loop_variable_values, arrays_chunk in zip_array_items(arrays, axes=axes): + loop_variables_dict = dict(zip(loop_variable_names, loop_variable_values)) + for title_template, array_chunk in zip(titles, arrays_chunk): + title = title_template.format(**loop_variables_dict) + self.add_graph(array_chunk, title, template, width, height) + if graphs_per_row is not None: + self.graphs_per_row = previous_graphs_per_row + + def newline(self): + self.top += self.curline_height + self.curline_height = 0 + self.left = 0 + self.position_in_row = 1 + + def _to_excel(self, workbook, data_row): + # use first sheet as data sheet + data_sheet = workbook.Worksheets(1) + data_cells = data_sheet.Cells + # write destination sheet name in data sheet + data_cells(data_row, 1).Value = self.name + data_row += 2 + # create new empty sheet in workbook (will contain output graphical items) + # Hack, since just specifying "After" is broken in certain environments + # see: https://stackoverflow.com/questions/40179804/adding-excel-sheets-to-end-of-workbook + dest_sheet = workbook.Worksheets.Add(Before=None, After=workbook.Sheets(workbook.Sheets.Count)) + dest_sheet.Name = self.name + # for each item, dump data + generate associated graphical items + for item in self.items: + data_row = item.dump(dest_sheet, data_sheet, data_row) + # reset + self.top = 0 + self.left = 0 + self.curline_height = 0 + # return current row in data sheet + return data_row + + # TODO : add a new section about this class in the tutorial + class ExcelReport(AbstractExcelReport): + def __init__(self, template_dir=None, template=None, graphs_per_row=1): + AbstractExcelReport.__init__(self, template_dir, template, graphs_per_row) + self.sheets = OrderedDict() + + def sheet_names(self): + return [sheet_name for sheet_name in self.sheets.keys()] + + def __getitem__(self, key): + return self.sheets[key] + + # TODO : Do not implement __setitem__ and move code below to new_sheet()? + def __setitem__(self, key, value): + if not isinstance(value, ReportSheet): + raise ValueError('Expected SheetReport object. ' + 'Got {} object instead.'.format(type(value).__name__)) + if key in self.sheet_names(): + warnings.warn("Sheet '{}' already exists in the report and will be reset".format(key)) + self.sheets[key] = value + + def __delitem__(self, key): + del self.sheets[key] + + def __repr__(self): + return 'sheets: {}'.format(self.sheet_names()) + + def new_sheet(self, sheet_name): + sheet = ReportSheet(self, sheet_name, self.template_dir, self.template, self.graphs_per_row) + self[sheet_name] = sheet + return sheet + + def to_excel(self, filepath, data_sheet_name='__data__', overwrite=True): + with open_excel(filepath, overwrite_file=overwrite) as wb: + # from here on, we use pure win32com objects instead of + # larray.excel or xlwings objects as this is faster + xl_wb = wb.api + + # rename first sheet + xl_wb.Worksheets(1).Name = data_sheet_name + + # dump items for each output sheet + data_sheet_row = 1 + for sheet in self.sheets.values(): + data_sheet_row = sheet._to_excel(xl_wb, data_sheet_row) + wb.save() + # reset + self.sheets.clear() +else: + class ReportSheet(AbstractReportSheet): + def __init__(self): + raise Exception("SheetReport class cannot be instantiated because xlwings is not installed") + + class ExcelReport(AbstractExcelReport): + def __init__(self): + raise Exception("ExcelReport class cannot be instantiated because xlwings is not installed") + + +if not PY2: + ExcelReport.__doc__ = AbstractExcelReport.__doc__ + ReportSheet.__doc__ = AbstractReportSheet.__doc__ diff --git a/larray/tests/excel_template/Line.crtx b/larray/tests/excel_template/Line.crtx new file mode 100644 index 0000000000000000000000000000000000000000..58c4beaf5898a6edaf1351b1f0eede43b734d90a GIT binary patch literal 7584 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 rFQN7)%Ab+5~b%# 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 AQUCw| literal 0 HcmV?d00001 diff --git a/larray/tests/test_excel.py b/larray/tests/test_excel.py index ebb50ef4d..0a24ec79e 100644 --- a/larray/tests/test_excel.py +++ b/larray/tests/test_excel.py @@ -1,13 +1,15 @@ from __future__ import absolute_import, division, print_function import re +import os import pytest import numpy as np -from larray.tests.common import needs_xlwings -from larray import ndtest, open_excel, aslarray, Axis +from larray.tests.common import needs_xlwings, TESTDATADIR +from larray import ndtest, open_excel, aslarray, Axis, nan, ExcelReport from larray.inout import xw_excel +from larray.example import load_example_data, EXAMPLE_EXCEL_TEMPLATES_DIR @needs_xlwings @@ -254,5 +256,144 @@ def test_repr(self): 1 3 4 5""" +# ================ # +# Test ExcelReport # +# ================ # + + +@needs_xlwings +def test_excel_report_init(): + # No argument + ExcelReport() + # with template dir + ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + # with graphs_per_row + ExcelReport(graphs_per_row=2) + + +@needs_xlwings +def test_excel_report_setting_template(): + excel_report = ExcelReport() + + # test setting template dir + # 1) wrong template dir + wrong_template_dir = r"C:\Wrong\Directory\Path" + msg = "Template directory {} could not been found".format(wrong_template_dir) + with pytest.raises(ValueError, message=msg): + excel_report.template_dir = wrong_template_dir + # 2) correct path + excel_report.template_dir = EXAMPLE_EXCEL_TEMPLATES_DIR + assert excel_report.template_dir == EXAMPLE_EXCEL_TEMPLATES_DIR + + # test setting template file + # 1) wrong extension + template_file = 'wrong_extension.txt' + msg = "Extension for the template file must be '.crtx' instead of .txt" + with pytest.raises(ValueError, message=msg): + excel_report.template = template_file + # 2) add .crtx extension if no extension + template_name = 'Line' + excel_report.template = template_name + assert excel_report.template == os.path.join(EXAMPLE_EXCEL_TEMPLATES_DIR, template_name + '.crtx') + + +@needs_xlwings +def test_excel_report_sheets(): + report = ExcelReport() + # test adding new sheets + sheet_pop = report.new_sheet('Pop') + sheet_births = report.new_sheet('Births') + sheet_deaths = report.new_sheet('Deaths') + # test warning if sheet already exists + with pytest.warns(UserWarning) as caught_warnings: + sheet_pop2 = report.new_sheet('Pop') + assert len(caught_warnings) == 1 + warn_msg = "Sheet 'Pop' already exists in the report and will be reset" + assert caught_warnings[0].message.args[0] == warn_msg + # test sheet_names() + assert report.sheet_names() == ['Pop', 'Births', 'Deaths'] + + +@needs_xlwings +def test_excel_report_titles(): + excel_report = ExcelReport() + + # test dumping titles + sheet_titles = excel_report.new_sheet('Titles') + # 1) default + sheet_titles.add_title('Default title') + # 2) specify width and height + width, height = 1100, 100 + sheet_titles.add_title('Width = {} and Height = {}'.format(width, height), + width=width, height=height) + # 3) specify fontsize + fontsize = 13 + sheet_titles.add_title('Fontsize = {}'.format(fontsize), fontsize=fontsize) + + # generate Excel file + fpath = 'test_excel_report_titles.xlsx' + excel_report.to_excel(fpath) + + +@needs_xlwings +def test_excel_report_arrays(): + excel_report = ExcelReport(EXAMPLE_EXCEL_TEMPLATES_DIR) + demo = load_example_data('demo') + pop = demo.pop + pop_be = pop['Belgium'] + pop_be_nan = pop_be.astype(float) + pop_be_nan[2013] = nan + + # test dumping arrays + sheet_graphs = excel_report.new_sheet('Graphs') + # 1) default + sheet_graphs.add_title('No template') + sheet_graphs.add_graph(pop_be['Female'], 'Pop Belgium Female') + sheet_graphs.add_graph(pop_be, 'Pop Belgium') + sheet_graphs.add_graph(pop_be_nan, 'Pop Belgium with nans') + # 2) no title + sheet_graphs.add_title('No title graph') + sheet_graphs.add_graph(pop_be) + # 3) specify width and height + sheet_graphs.add_title('Alternative Width and Height') + width, height = 500, 300 + sheet_graphs.add_graph(pop_be, 'width = {} and Height = {}'.format(width, height), width=width, height=height) + # 4) specify template + template_name = 'Line_Marker' + sheet_graphs.add_title('Template = {}'.format(template_name)) + sheet_graphs.add_graph(pop_be, 'Template = {}'.format(template_name), template_name) + + # test setting default size + # 1) pass a not registered kind of item + type_item = 'unknown_item' + msg = "Type item {} is not registered. Please choose in " \ + "list ['title', 'graph']".format(type_item) + with pytest.raises(ValueError, message=msg): + sheet_graphs.set_item_default_size(type_item, width, height) + # 2) update default size for graphs + sheet_graphs.set_item_default_size('graph', width, height) + sheet_graphs.add_title('Using Defined Sizes For All Graphs') + sheet_graphs.add_graph(pop_be, 'Pop Belgium') + + # test setting default number of graphs per row + sheet_graphs = excel_report.new_sheet('Graphs2') + sheet_graphs.graphs_per_row = 2 + sheet_graphs.add_title('graphs_per_row = 2') + for combined_labels, subset in pop.items(('time', 'gender')): + title = ' - '.join([str(label) for label in combined_labels]) + sheet_graphs.add_graph(subset, title) + + # testing add_graphs + sheet_graphs = excel_report.new_sheet('Graphs3') + sheet_graphs.add_title('add_graphs') + sheet_graphs.add_graphs({'Population of {gender} by country for the year {year}': pop}, + {'gender': pop.gender, 'year': pop.time}, + template='line', width=450, height=250, graphs_per_row=2) + + # generate Excel file + fpath = 'test_excel_report_arrays.xlsx' + excel_report.to_excel(fpath) + + if __name__ == "__main__": pytest.main() diff --git a/larray/util/misc.py b/larray/util/misc.py index 186aaf3d5..b08a89e7e 100644 --- a/larray/util/misc.py +++ b/larray/util/misc.py @@ -6,6 +6,7 @@ import __main__ import math import itertools +import os import sys import operator import warnings @@ -1014,3 +1015,17 @@ def ensure_no_numpy_type(array): if array.dtype.kind == 'O': array = _kill_np_types(array) return array.tolist() + + +# ################# # +# validator funcs # +# ################# # + +def _positive_integer(value): + if not (isinstance(value, int) and value > 0): + raise ValueError("Expected positive integer") + + +def _validate_dir(directory): + if not os.path.isdir(directory): + raise ValueError("The directory {} could not been found".format(directory)) diff --git a/larray/util/options.py b/larray/util/options.py index 5e0a52e64..03029bb85 100644 --- a/larray/util/options.py +++ b/larray/util/options.py @@ -1,5 +1,5 @@ from __future__ import absolute_import, division, print_function - +from larray.util.misc import _positive_integer DISPLAY_PRECISION = 'display_precision' DISPLAY_WIDTH = 'display_width' @@ -15,11 +15,6 @@ } -def _positive_integer(value): - if not (isinstance(value, int) and value > 0): - raise ValueError("Expected positive integer") - - def _integer_maxlines(value): if not isinstance(value, int) and value >= -1: raise ValueError("Expected integer") diff --git a/setup.cfg b/setup.cfg index 2dfadb1a2..fb74a09ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,10 @@ test=pytest testpaths = larray # exclude (doc)tests from ufuncs (because docstrings are copied from numpy # and many of those doctests are failing -addopts = -v --doctest-modules --ignore=larray/core/npufuncs.py --ignore=larray/ipfp +addopts = -v --doctest-modules + --ignore=larray/core/npufuncs.py + --ignore=larray/ipfp + --ignore=larray/inout/xw_reporting.py --pep8 #--cov # E122: continuation line missing indentation or outdented