diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..1f2082e --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,75 @@ +This project was started by Eric Gazoni. In 2013 Charlie Clark became +co-maintainer of the project. + +It was initiallay *heavily* inspired by the PHPExcel library: +http://www.phpexcel.net/ + +Thanks to all those who participate in the project (in alphabetical order): + +* Stephane Bard +* Day Barr +* Stefan Behnel +* Bernt R. Brenna +* Sven Burk +* Max Bolingbroke +* Anders Chrigstrom +* ccoacley +* Maarten De Paepe +* Etienne Desautels +* Dmitriy Chernyshov +* Eric Chlebek +* Alexandre Fayolle +* Don Freeman +* Eric Gazoni +* Brice Gelineau +* Mark Gemmill +* Alex Gronholm +* Yaroslav Halchenko +* Fumito Hamamura +* Khchine Hamza +* Josh Haywood +* Jeff Holman +* Brent Hoover +* Eric Hurkman +* Jean Pierre Huart +* JarekPS +* Heikki Junes +* Chi Ho Kwok +* Yingjie Lan +* Detlef Lannert +* Laurent Laporte +* Nicholas Laver +* Greg Lehmann +* Adam Lofts +* Marko Loparic +* Samuel Loretan +* Amin Mirzaee +* Adam Morris +* aceMueller +* Gabi Nagy +* Thomas Nygards +* Felipe Ochoa +* Jun Omae +* Waldemar Osuch +* Jonathan Peirce +* Sergey Pikhovkin +* Ted Pollari +* Elias Rabel +* Rick Rankin +* ramn_se +* Philip Roche +* Wojciech Rola +* James Smagala +* Wolfgane Scherer +* Joseph Tate +* Gar Thompson +* Dieter Vandenbussche +* Paul Van Der Linden +* Gerald Van Huffelen +* Koert van der Veer +* Laurent Vasseur +* Kay Webber +* Shibukawa Yoshiki + +Project logo designed by Eric Gazoni, font by claudeserieux +(http://www.dafont.com/profile.php?user=337503) diff --git a/LICENCE.rst b/LICENCE.rst new file mode 100644 index 0000000..82213c5 --- /dev/null +++ b/LICENCE.rst @@ -0,0 +1,23 @@ +This software is under the MIT Licence +====================================== + +Copyright (c) 2010 openpyxl + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..04cdea9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +prune openpyxl/tests +prune openpyxl/sample +prune openpyxl/benchmarks +prune openpyxl/develop +prune scratchpad + +recursive-exclude openpyxl test_*.py tests/*.py + +include *.rst +include openpyxl/.constants.json diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4a739b6 --- /dev/null +++ b/README.rst @@ -0,0 +1,60 @@ +Introduction +============ +This is a fork from bitbucket of openpyxl package: https://bitbucket.org/openpyxl/openpyxl/src/default/ +I have repaired the image and chart loading bugs in this revised version. + +openpyxl +======== + +openpyxl is a Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm files. + +It was born from lack of existing library to read/write natively from Python +the Office Open XML format. + +All kudos to the PHPExcel team as openpyxl was initially based on PHPExcel. + + +Mailing List +============ + +Official user list can be found on +http://groups.google.com/group/openpyxl-users + + +Sample code:: + + from openpyxl import Workbook + wb = Workbook() + + # grab the active worksheet + ws = wb.active + + # Data can be assigned directly to cells + ws['A1'] = 42 + + # Rows can also be appended + ws.append([1, 2, 3]) + + # Python types will automatically be converted + import datetime + ws['A2'] = datetime.datetime.now() + + # Save the file + wb.save("sample.xlsx") + + +Official documentation +====================== + +The documentation is at: https://openpyxl.readthedocs.io + +* installation methods +* code examples +* instructions for contributing + +Release notes: https://openpyxl.readthedocs.io/en/latest/changes.html + +Fix the image and chart loading bugs +====================================== + +Now you can add image or chart to the excel as you wish. They can be reserved when reopening the excel file. diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml new file mode 100644 index 0000000..50df006 --- /dev/null +++ b/bitbucket-pipelines.yml @@ -0,0 +1,9 @@ +image: openpyxl/openpyxl-ci + +pipelines: + default: + - step: + caches: + - pip + script: + - /tools/clean-launch.sh tox --skip-missing-interpreters -- -qrf \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..c7e9d1f --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/openpyxl.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/openpyxl.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/openpyxl" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/openpyxl" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/_static/.placeholder b/doc/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/doc/changes.rst b/doc/changes.rst new file mode 100644 index 0000000..6a4c896 --- /dev/null +++ b/doc/changes.rst @@ -0,0 +1,1224 @@ +2.5.3 (2018-04-18) +================== + + +Bugfixes +-------- + +* `#983 `_ Warning level too aggressive. +* `#1015 `_ Alignment and protection values not saved for named styles. +* `#1017 `_ Deleting elements from a legend doesn't work. +* `#1018 `_ Index names repeated for every row in dataframe. +* `#1020 `_ Worksheet protection not being stored. +* `#1023 `_ Exception raised when reading a tooltip. + + +2.5.2 (2018-04-06) +================== + + +Bugfixes +-------- + +* `#949 `_ High memory use when reading text-heavy files. +* `#970 `_ Copying merged cells copies references. +* `#978 `_ Cannot set comment size. +* `#985 `_ Exception when trying to save workbooks with no views. +* `#995 `_ Cannot delete last row or column. +* `#1002 `_ Cannot read Drawings containing embedded images. + + +Minor changes +------------- + +* Support for dataframes with multiple columns and multiple indices. + + +2.5.1 (2018-03-12) +================== + + +Bugfixes +-------- + +* `#934 `_ Headers and footers not included in write-only mode. +* `#960 `_ Deprecation warning raised when using ad-hoc access in read-only mode. +* `#964 `_ Not all cells removed when deleting multiple rows. +* `#966 `_ Cannot read 3d bar chart correctly. +* `#967 `_ Problems reading some charts. +* `#968 `_ Worksheets with SHA protection become corrupted after saving. +* `#974 `_ Problem when deleting ragged rows or columns. +* `#976 `_ GroupTransforms and GroupShapeProperties have incorrect descriptors +* Make sure that headers and footers in chartsheets are included in the file + + + +2.5.0 (2018-01-24) +================== + + +Minor changes +------------- + +* Correct definition for Connection Shapes. Related to # 958 + + +2.5.0-b2 (2018-01-19) +===================== + + +Bugfixes +-------- + +* `#915 `_ TableStyleInfo has no required attributes +* `#925 `_ Cannot read files with 3D drawings +* `#926 `_ Incorrect version check in installer +* Cell merging uses transposed parameters +* `#928 `_ ExtLst missing keyword for PivotFields +* `#932 `_ Inf causes problems for Excel +* `#952 `_ Cannot load table styles with custom names + + +Major Changes +------------- + +* You can now insert and delete rows and columns in worksheets + + +Minor Changes +------------- + +* pip now handles which Python versions can be used. + + +2.5.0-b1 (2017-10-19) +===================== + + +Bugfixes +-------- +* `#812 `_ Explicitly support for multiple cell ranges in conditonal formatting +* `#827 `_ Non-contiguous cell ranges in validators get merged +* `#837 `_ Empty data validators create invalid Excel files +* `#860 `_ Large validation ranges use lots of memory +* `#876 `_ Unicode in chart axes not handled correctly in Python 2 +* `#882 `_ ScatterCharts have defective axes +* `#885 `_ Charts with empty numVal elements cannot be read +* `#894 `_ Scaling options from existing files ignored +* `#895 `_ Charts with PivotSource cannot be read +* `#903 `_ Cannot read gradient fills +* `#904 `_ Quotes in number formats could be treated as datetimes + + +Major Changes +------------- + +`worksheet.cell()` no longer accepts a `coordinate` parameter. The syntax is now `ws.cell(row, column, value=None)` + + +Minor Changes +------------- + +Added CellRange and MultiCellRange types (thanks to Laurent LaPorte for the +suggestion) as a utility type for things like data validations, conditional +formatting and merged cells. + + +Deprecations +------------ + +ws.merged_cell_ranges has been deprecated because MultiCellRange provides sufficient functionality + + +2.5.0-a3 (2017-08-14) +===================== + + +Bugfixes +-------- +* `#848 `_ Reading workbooks with Pie Charts raises an exception +* `#857 `_ Pivot Tables without Worksheet Sources raise an exception + + +2.5.0-a2 (2017-06-25) +===================== + + +Major Changes +------------- + +* Read support for charts + + +Bugfixes +-------- +* `#833 `_ Cannot access chartsheets by title +* `#834 `_ Preserve workbook views +* `#841 `_ Incorrect classification of a datetime + + +2.5.0-a1 (2017-05-30) +===================== + + +Compatibility +------------- + +* Dropped support for Python 2.6 and 3.3. openpyxl will not run with Python 2.6 + + +Major Changes +------------- + +* Read/write support for pivot tables + + +Deprecations +------------ + +* Dropped the anchor method from images and additional constructor arguments + + +Bugfixes +-------- +* `#779 `_ Fails to recognise Chinese date format` +* `#828 `_ Include hidden cells in charts` + + +Pull requests +------------- +* `163 `_ Improved GradientFill + + +Minor changes +------------- + +* Remove deprecated methods from Cell +* Remove deprecated methods from Worksheet +* Added read/write support for the datetime type for cells + + +2.4.11 (2018-01-24) +=================== + +* #957 ``_ Relationship type for tables is borked + + +2.4.10 (2018-01-19) +=================== + +Bugfixes +-------- + +* #912 ``_ Copying objects uses shallow copy +* #921 ``_ API documentation not generated automatically +* #927 ``_ Exception raised when adding coloured borders together +* #931 ``_ Number formats not correctly deduplicated + + +Pull requests +------------- + +* 203 ``_ Correction to worksheet protection description +* 210 ``_ Some improvements to the API docs +* 211 ``_ Improved deprecation decorator +* 218 ``_ Fix problems with deepcopy + + +2.4.9 (2017-10-19) +================== + + +Bugfixes +-------- + +* `#809 `_ Incomplete documentation of `copy_worksheet` method +* `#811 `_ Scoped definedNames not removed when worksheet is deleted +* `#824 `_ Raise an exception if a chart is used in multiple sheets +* `#842 `_ Non-ASCII table column headings cause an exception in Python 2 +* `#846 `_ Conditional formats not supported in write-only mode +* `#849 `_ Conditional formats with no sqref cause an exception +* `#859 `_ Headers that start with a number conflict with font size +* `#902 `_ TableStyleElements don't always have a condtional format +* `#908 `_ Read-only mode sometimes returns too many cells + + + +Pull requests +------------- + +* `#179 `_ Cells kept in a set +* `#180 `_ Support for Workbook protection +* `#182 `_ Read support for page breaks +* `#183 `_ Improve documentation of `copy_worksheet` method +* `#198 `_ Fix for #908 + + +2.4.8 (2017-05-30) +================== + + +Bugfixes +-------- + +* AutoFilter.sortState being assignd to the ws.sortState +* `#766 `_ Sheetnames with apostrophes need additional escaping +* `#729 `_ Cannot open files created by Microsoft Dynamics +* `#819 `_ Negative percents not case correctly +* `#821 `_ Runtime imports can cause deadlock +* `#855 `_ Print area containing only columns leads to corrupt file + + +Minor changes +------------- +* Preserve any table styles + + +2.4.7 (2017-04-24) +================== + + +Bugfixes +-------- +* `#807 `_ Sample files being included by mistake in sdist + + +2.4.6 (2017-04-14) +================== + + +Bugfixes +-------- +* `#776 `_ Cannot apply formatting to plot area +* `#780 `_ Exception when element attributes are Python keywords +* `#781 `_ Exception raised when saving files with styled columns +* `#785 `_ Number formats for data labels are incorrect +* `#788 `_ Worksheet titles not quoted in defined names +* `#800 `_ Font underlines not read correctly + + +2.4.5 (2017-03-07) +================== + + +Bugfixes +-------- +* `#750 `_ Adding images keeps file handles open +* `#772 `_ Exception for column-only ranges +* `#773 `_ Cannot copy worksheets with non-ascii titles on Python 2 + + +Pull requests +------------- + +* `161 `_ Support for non-standard names for Workbook part. +* `162 `_ Documentation correction + + +2.4.4 (2017-02-23) +================== + + +Bugfixes +-------- + +* `#673 `_ Add close method to workbooks +* `#762 `_ openpyxl can create files with invalid style indices +* `#729 `_ Allow images in write-only mode +* `#744 `_ Rounded corners for charts +* `#747 `_ Use repr when handling non-convertible objects +* `#764 `_ Hashing function is incorrect +* `#765 `_ Named styles share underlying array + + +Minor Changes +------------- + +* Add roundtrip support for worksheet tables. + + +Pull requests +------------- + +* `160 `_ Don't init mimetypes more than once. + + +2.4.3 (unreleased) +================== +bad release + + +2.4.2 (2017-01-31) +================== + + +Bug fixes +--------- + +* `#727 `_ DeprecationWarning is incorrect +* `#734 `_ Exception raised if userName is missing +* `#739 `_ Always provide a date1904 attribute +* `#740 `_ Hashes should be stored as Base64 +* `#743 `_ Print titles broken on sheetnames with spaces +* `#748 `_ Workbook breaks when active sheet is removed +* `#754 `_ Incorrect descriptor for Filter values +* `#756 `_ Potential XXE vulerability +* `#758 `_ Cannot create files with page breaks and charts +* `#759 `_ Problems with worksheets with commas in their titles + + +Minor Changes +------------- + +* Add unicode support for sheet name incrementation. + + +2.4.1 (2016-11-23) +================== + + +Bug fixes +--------- + +* `#643 `_ Make checking for duplicate sheet titles case insensitive +* `#647 `_ Trouble handling LibreOffice files with named styles +* `#687 `_ Directly assigned new named styles always refer to "Normal" +* `#690 `_ Cannot parse print titles with multiple sheet names +* `#691 `_ Cannot work with macro files created by LibreOffice +* Prevent duplicate differential styles +* `#694 `_ Allow sheet titles longer than 31 characters +* `#697 `_ Cannot unset hyperlinks +* `#699 `_ Exception raised when format objects use cell references +* `#703 `_ Copy height and width when copying comments +* `#705 `_ Incorrect content type for VBA macros +* `#707 `_ IndexError raised in read-only mode when accessing individual cells +* `#711 `_ Files with external links become corrupted +* `#715 `_ Cannot read files containing macro sheets +* `#717 `_ Details from named styles not preserved when reading files +* `#722 `_ Remove broken Print Title and Print Area definitions + + +Minor changes +------------- + +* Add support for Python 3.6 +* Correct documentation for headers and footers + + +Deprecations +------------ + +Worksheet methods `get_named_range()` and `get_sqaured_range()` + + +Bug fixes +--------- + + +2.4.0 (2016-09-15) +================== + + +Bug fixes +--------- + +* `#652 `_ Exception raised when epoch is 1904 +* `#642 `_ Cannot handle unicode in headers and footers in Python 2 +* `#646 `_ Cannot handle unicode sheetnames in Python 2 +* `#658 `_ Chart styles, and axis units should not be 0 +* `#663 `_ Strings in external workbooks not unicode + + +Major changes +------------- + +* Add support for builtin styles and include one for Pandas + + +Minor changes +------------- + +* Add a `keep_links` option to `load_workbook`. External links contain cached + copies of the external workbooks. If these are big it can be advantageous to + be able to disable them. +* Provide an example for using cell ranges in DataValidation. +* PR 138 - add copy support to comments. + + +2.4.0-b1 (2016-06-08) +===================== + + +Minor changes +------------- + +* Add an the alias `hide_drop_down` to DataValidation for `showDropDown` because that is how Excel works. + + +Bug fixes +--------- + +* `#625 `_ Exception raises when inspecting EmptyCells in read-only mode +* `#547 `_ Functions for handling OOXML "escaped" ST_XStrings +* `#629 `_ Row Dimensions not supported in write-only mode +* `#530 `_ Problems when removing worksheets with charts +* `#630 `_ Cannot use SheetProtection in write-only mode + + +Features +-------- + +* Add write support for worksheet tables + + +2.4.0-a1 (2016-04-11) +===================== + + +Minor changes +------------- + +* Remove deprecated methods from DataValidation +* Remove deprecated methods from PrintPageSetup +* Convert AutoFilter to Serialisable and extend support for filters +* Add support for SortState +* Removed `use_iterators` keyword when loading workbooks. Use `read_only` instead. +* Removed `optimized_write` keyword for new workbooks. Use `write_only` instead. +* Improve print title support +* Add print area support +* New implementation of defined names +* New implementation of page headers and footers +* Add support for Python's NaN +* Added iter_cols method for worksheets +* ws.rows and ws.columns now always return generators and start at the top of the worksheet +* Add a `values` property for worksheets +* Default column width changed to 8 as per the specification + + +Deprecations +------------ + +* Cell anchor method +* Worksheet point_pos method +* Worksheet add_print_title method +* Worksheet HeaderFooter attribute, replaced by individual ones +* Flatten function for cells +* Workbook get_named_range, add_named_range, remove_named_range, get_sheet_names, get_sheet_by_name +* Comment text attribute +* Use of range strings deprecated for ws.iter_rows() +* Use of coordinates deprecated for ws.cell() +* Deprecate .copy() method for StyleProxy objects + + +Bug fixes +--------- + +* `#152 `_ Hyperlinks lost when reading files +* `#171 `_ Add function for copying worksheets +* `#386 `_ Cells with inline strings considered empty +* `#397 `_ Add support for ranges of rows and columns +* `#446 `_ Workbook with definedNames corrupted by openpyxl +* `#481 `_ "safe" reserved ranges are not read from workbooks +* `#501 `_ Discarding named ranges can lead to corrupt files +* `#574 `_ Exception raised when using the class method to parse Relationships +* `#579 `_ Crashes when reading defined names with no content +* `#597 `_ Cannot read worksheets without coordinates +* `#617 `_ Customised named styles not correctly preserved + + +2.3.5 (2016-04-11) +================== + + +Bug fixes +--------- + +* `#618 `_ Comments not written in write-only mode + + +2.3.4 (2016-03-16) +================== + + +Bug fixes +--------- + +* `#594 `_ Content types might be missing when keeping VBA +* `#599 `_ Cells with only one cell look empty +* `#607 `_ Serialise NaN as '' + + +Minor changes +------------- + +* Preserve the order of external references because formualae use numerical indices. +* Typo corrected in cell unit tests (PR 118) + + +2.3.3 (2016-01-18) +================== + + +Bug fixes +--------- + +* `#540 `_ Cannot read merged cells in read-only mode +* `#565 `_ Empty styled text blocks cannot be parsed +* `#569 `_ Issue warning rather than raise Exception raised for unparsable definedNames +* `#575 `_ Cannot open workbooks with embdedded OLE files +* `#584 `_ Exception when saving borders with attributes + + +Minor changes +------------- + +* `PR 103 `_ Documentation about chart scaling and axis limits +* Raise an exception when trying to copy cells from other workbooks. + + +2.3.2 (2015-12-07) +================== + + +Bug fixes +--------- + +* `#554 `_ Cannot add comments to a worksheet when preserving VBA +* `#561 `_ Exception when reading phonetic text +* `#562 `_ DARKBLUE is the same as RED +* `#563 `_ Minimum for row and column indexes not enforced + + +Minor changes +------------- + +* `PR 97 `_ One VML file per worksheet. +* `PR 96 `_ Correct descriptor for CharacterProperties.rtl +* `#498 `_ Metadata is not essential to use the package. + + +2.3.1 (2015-11-20) +================== + + +Bug fixes +--------- + +* `#534 `_ Exception when using columns property in read-only mode. +* `#536 `_ Incorrectly handle comments from Google Docs files. +* `#539 `_ Flexible value types for conditional formatting. +* `#542 `_ Missing content types for images. +* `#543 `_ Make sure images fit containers on all OSes. +* `#544 `_ Gracefully handle missing cell styles. +* `#546 `_ ExternalLink duplicated when editing a file with macros. +* `#548 `_ Exception with non-ASCII worksheet titles +* `#551 `_ Combine multiple LineCharts + + +Minor changes +------------- + +* `PR 88 `_ Fix page margins in parser. + + +2.3.0 (2015-10-20) +================== + + +Major changes +------------- + +* Support the creation of chartsheets + + +Bug fixes +--------- + +* `#532 `_ Problems when cells have no style in read-only mode. + + +Minor changes +------------- + +* PR 79 Make PlotArea editable in charts +* Use graphicalProperties as the alias for spPr + + +2.3.0-b2 (2015-09-04) +===================== + + +Bug fixes +--------- + +* `#488 `_ Support hashValue attribute for sheetProtection +* `#493 `_ Warn that unsupported extensions will be dropped +* `#494 `_ Cells with exponentials causes a ValueError +* `#497 `_ Scatter charts are broken +* `#499 `_ Inconsistent conversion of localised datetimes +* `#500 `_ Adding images leads to unreadable files +* `#509 `_ Improve handling of sheet names +* `#515 `_ Non-ascii titles have bad repr +* `#516 `_ Ignore unassigned worksheets + + +Minor changes +------------- + +* Worksheets are now iterable by row. +* Assign individual cell styles only if they are explicitly set. + + +2.3.0-b1 (2015-06-29) +===================== + + +Major changes +------------- + +* Shift to using (row, column) indexing for cells. Cells will at some point *lose* coordinates. +* New implementation of conditional formatting. Databars now partially preserved. +* et_xmlfile is now a standalone library. +* Complete rewrite of chart package +* Include a tokenizer for fomulae to be able to adjust cell references in them. PR 63 + + +Minor changes +------------- + +* Read-only and write-only worksheets renamed. +* Write-only workbooks support charts and images. +* `PR76 `_ Prevent comment images from conflicting with VBA + + +Bug fixes +--------- + +* `#81 `_ Support stacked bar charts +* `#88 `_ Charts break hyperlinks +* `#97 `_ Pie and combination charts +* `#99 `_ Quote worksheet names in chart references +* `#150 `_ Support additional chart options +* `#172 `_ Support surface charts +* `#381 `_ Preserve named styles +* `#470 `_ Adding more than 10 worksheets with the same name leads to duplicates sheet names and an invalid file + + +2.2.6 (unreleased) +================== + + +Bug fixes +--------- + +* `#502 `_ Unexpected keyword "mergeCell" +* `#503 `_ tostring missing in dump_worksheet +* `#506 `_ Non-ASCII formulae cannot be parsed +* `#508 `_ Cannot save files with coloured tabs +* Regex for ignoring named ranges is wrong (character class instead of prefix) + + +2.2.5 (2015-06-29) +================== + + +Bug fixes +--------- + +* `#463 `_ Unexpected keyword "mergeCell" +* `#484 `_ Unusual dimensions breaks read-only mode +* `#485 `_ Move return out of loop + + +2.2.4 (2015-06-17) +================== + + +Bug fixes +--------- + +* `#464 `_ Cannot use images when preserving macros +* `#465 `_ ws.cell() returns an empty cell on read-only workbooks +* `#467 `_ Cannot edit a file with ActiveX components +* `#471 `_ Sheet properties elements must be in order +* `#475 `_ Do not redefine class __slots__ in subclasses +* `#477 `_ Write-only support for SheetProtection +* `#478 `_ Write-only support for DataValidation +* Improved regex when checking for datetime formats + + +2.2.3 (2015-05-26) +================== + + +Bug fixes +--------- + +* `#451 `_ fitToPage setting ignored +* `#458 `_ Trailing spaces lost when saving files. +* `#459 `_ setup.py install fails with Python 3 +* `#462 `_ Vestigial rId conflicts when adding charts, images or comments +* `#455 `_ Enable Zip64 extensions for all versions of Python + + +2.2.2 (2015-04-28) +================== + + +Bug fixes +--------- + +* `#447 `_ Uppercase datetime number formats not recognised. +* `#453 `_ Borders broken in shared_styles. + + +2.2.1 (2015-03-31) +================== + + +Minor changes +------------- + +* `PR54 `_ Improved precision on times near midnight. +* `PR55 `_ Preserve macro buttons + + +Bug fixes +--------- + +* `#429 `_ Workbook fails to load because header and footers cannot be parsed. +* `#433 `_ File-like object with encoding=None +* `#434 `_ SyntaxError when writing page breaks. +* `#436 `_ Read-only mode duplicates empty rows. +* `#437 `_ Cell.offset raises an exception +* `#438 `_ Cells with pivotButton and quotePrefix styles cannot be read +* `#440 `_ Error when customised versions of builtin formats +* `#442 `_ Exception raised when a fill element contains no children +* `#444 `_ Styles cannot be copied + + +2.2.0 (2015-03-11) +================== + + +Bug fixes +--------- +* `#415 `_ Improved exception when passing in invalid in memory files. + + +2.2.0-b1 (2015-02-18) +===================== + + +Major changes +------------- +* Cell styles deprecated, use formatting objects (fonts, fills, borders, etc.) directly instead +* Charts will no longer try and calculate axes by default +* Support for template file types - PR21 +* Moved ancillary functions and classes into utils package - single place of reference +* `PR 34 `_ Fully support page setup +* Removed SAX-based XML Generator. Special thanks to Elias Rabel for implementing xmlfile for xml.etree +* Preserve sheet view definitions in existing files (frozen panes, zoom, etc.) + + +Bug fixes +--------- +* `#103 `_ Set the zoom of a sheet +* `#199 `_ Hide gridlines +* `#215 `_ Preserve sheet view setings +* `#262 `_ Set the zoom of a sheet +* `#392 `_ Worksheet header not read +* `#387 `_ Cannot read files without styles.xml +* `#410 `_ Exception when preserving whitespace in strings +* `#417 `_ Cannot create print titles +* `#420 `_ Rename confusing constants +* `#422 `_ Preserve color index in a workbook if it differs from the standard + + +Minor changes +------------- +* Use a 2-way cache for column index lookups +* Clean up tests in cells +* `PR 40 `_ Support frozen panes and autofilter in write-only mode +* Use ws.calculate_dimension(force=True) in read-only mode for unsized worksheets + + +2.1.5 (2015-02-18) +================== + + +Bug fixes +--------- +* `#403 `_ Cannot add comments in write-only mode +* `#401 `_ Creating cells in an empty row raises an exception +* `#408 `_ from_excel adjustment for Julian dates 1 < x < 60 +* `#409 `_ refersTo is an optional attribute + + +Minor changes +------------- +* Allow cells to be appended to standard worksheets for code compatibility with write-only mode. + + +2.1.4 (2014-12-16) +================== + + +Bug fixes +--------- + +* `#393 `_ IterableWorksheet skips empty cells in rows +* `#394 `_ Date format is applied to all columns (while only first column contains dates) +* `#395 `_ temporary files not cleaned properly +* `#396 `_ Cannot write "=" in Excel file +* `#398 `_ Cannot write empty rows in write-only mode with LXML installed + + +Minor changes +------------- +* Add relation namespace to root element for compatibility with iWork +* Serialize comments relation in LXML-backend + + +2.1.3 (2014-12-09) +================== + + +Minor changes +------------- +* `PR 31 `_ Correct tutorial +* `PR 32 `_ See #380 +* `PR 37 `_ Bind worksheet to ColumnDimension objects + + +Bug fixes +--------- +* `#379 `_ ws.append() doesn't set RowDimension Correctly +* `#380 `_ empty cells formatted as datetimes raise exceptions + + +2.1.2 (2014-10-23) +================== + + +Minor changes +------------- +* `PR 30 `_ Fix regex for positive exponentials +* `PR 28 `_ Fix for #328 + + +Bug fixes +--------- +* `#120 `_, `#168 `_ defined names with formulae raise exceptions, `#292 `_ +* `#328 `_ ValueError when reading cells with hyperlinks +* `#369 `_ IndexError when reading definedNames +* `#372 `_ number_format not consistently applied from styles + + +2.1.1 (2014-10-08) +================== + + +Minor changes +------------- +* PR 20 Support different workbook code names +* Allow auto_axis keyword for ScatterCharts + + +Bug fixes +--------- + +* `#332 `_ Fills lost in ConditionalFormatting +* `#360 `_ Support value="none" in attributes +* `#363 `_ Support undocumented value for textRotation +* `#364 `_ Preserve integers in read-only mode +* `#366 `_ Complete read support for DataValidation +* `#367 `_ Iterate over unsized worksheets + + +2.1.0 (2014-09-21) +================== + +Major changes +------------- +* "read_only" and "write_only" new flags for workbooks +* Support for reading and writing worksheet protection +* Support for reading hidden rows +* Cells now manage their styles directly +* ColumnDimension and RowDimension object manage their styles directly +* Use xmlfile for writing worksheets if available - around 3 times faster +* Datavalidation now part of the worksheet package + + +Minor changes +------------- +* Number formats are now just strings +* Strings can be used for RGB and aRGB colours for Fonts, Fills and Borders +* Create all style tags in a single pass +* Performance improvement when appending rows +* Cleaner conversion of Python to Excel values +* PR6 reserve formatting for empty rows +* standard worksheets can append from ranges and generators + + +Bug fixes +--------- +* `#153 `_ Cannot read visibility of sheets and rows +* `#181 `_ No content type for worksheets +* `241 `_ Cannot read sheets with inline strings +* `322 `_ 1-indexing for merged cells +* `339 `_ Correctly handle removal of cell protection +* `341 `_ Cells with formulae do not round-trip +* `347 `_ Read DataValidations +* `353 `_ Support Defined Named Ranges to external workbooks + + +2.0.5 (2014-08-08) +================== + + +Bug fixes +--------- +* `#348 `_ incorrect casting of boolean strings +* `#349 `_ roundtripping cells with formulae + + +2.0.4 (2014-06-25) +================== + +Minor changes +------------- +* Add a sample file illustrating colours + + +Bug fixes +--------- + +* `#331 `_ DARKYELLOW was incorrect +* Correctly handle extend attribute for fonts + + +2.0.3 (2014-05-22) +================== + +Minor changes +------------- + +* Updated docs + + +Bug fixes +--------- + +* `#319 `_ Cannot load Workbooks with vertAlign styling for fonts + + +2.0.2 (2014-05-13) +================== + +2.0.1 (2014-05-13) brown bag +============================= + +2.0.0 (2014-05-13) brown bag +============================= + + +Major changes +------------- + +* This is last release that will support Python 3.2 +* Cells are referenced with 1-indexing: A1 == cell(row=1, column=1) +* Use jdcal for more efficient and reliable conversion of datetimes +* Significant speed up when reading files +* Merged immutable styles +* Type inference is disabled by default +* RawCell renamed ReadOnlyCell +* ReadOnlyCell.internal_value and ReadOnlyCell.value now behave the same as Cell +* Provide no size information on unsized worksheets +* Lower memory footprint when reading files + + +Minor changes +------------- + +* All tests converted to pytest +* Pyflakes used for static code analysis +* Sample code in the documentation is automatically run +* Support GradientFills +* BaseColWidth set + + +Pull requests +------------- +* #70 Add filterColumn, sortCondition support to AutoFilter +* #80 Reorder worksheets parts +* #82 Update API for conditional formatting +* #87 Add support for writing Protection styles, others +* #89 Better handling of content types when preserving macros + + +Bug fixes +--------- +* `#46 `_ ColumnDimension style error +* `#86 `_ reader.worksheet.fast_parse sets booleans to integers +* `#98 `_ Auto sizing column widths does not work +* `#137 `_ Workbooks with chartsheets +* `#185 `_ Invalid PageMargins +* `#230 `_ Using \v in cells creates invalid files +* `#243 `_ - IndexError when loading workbook +* `#263 `_ - Forded conversion of line breaks +* `#267 `_ - Raise exceptions when passed invalid types +* `#270 `_ - Cannot open files which use non-standard sheet names or reference Ids +* `#269 `_ - Handling unsized worksheets in IterableWorksheet +* `#270 `_ - Handling Workbooks with non-standard references +* `#275 `_ - Handling auto filters where there are only custom filters +* `#277 `_ - Harmonise chart and cell coordinates +* `#280 `_- Explicit exception raising for invalid characters +* `#286 `_ - Optimized writer can not handle a datetime.time value +* `#296 `_ - Cell coordinates not consistent with documentation +* `#300 `_ - Missing column width causes load_workbook() exception +* `#304 `_ - Handling Workbooks with absolute paths for worksheets (from Sharepoint) + + +1.8.6 (2014-05-05) +================== + +Minor changes +------------- +Fixed typo for import Elementtree + +Bugfixes +-------- +* `#279 `_ Incorrect path for comments files on Windows + + +1.8.5 (2014-03-25) +================== + +Minor changes +------------- +* The '=' string is no longer interpreted as a formula +* When a client writes empty xml tags for cells (e.g. ), reader will not crash + + +1.8.4 (2014-02-25) +================== + +Bugfixes +-------- +* `#260 `_ better handling of undimensioned worksheets +* `#268 `_ non-ascii in formualae +* `#282 `_ correct implementation of register_namepsace for Python 2.6 + + +1.8.3 (2014-02-09) +================== + +Major changes +------------- +Always parse using cElementTree + +Minor changes +------------- +Slight improvements in memory use when parsing + +* `#256 `_ - error when trying to read comments with optimised reader +* `#260 `_ - unsized worksheets +* `#264 `_ - only numeric cells can be dates + + +1.8.2 (2014-01-17) +================== + +* `#247 `_ - iterable worksheets open too many files +* `#252 `_ - improved handling of lxml +* `#253 `_ - better handling of unique sheetnames + + +1.8.1 (2014-01-14) +================== + +* `#246 `_ + + +1.8.0 (2014-01-08) +================== + +Compatibility +------------- + +Support for Python 2.5 dropped. + +Major changes +------------- + +* Support conditional formatting +* Support lxml as backend +* Support reading and writing comments +* pytest as testrunner now required +* Improvements in charts: new types, more reliable + + +Minor changes +------------- + +* load_workbook now accepts data_only to allow extracting values only from + formulae. Default is false. +* Images can now be anchored to cells +* Docs updated +* Provisional benchmarking +* Added convenience methods for accessing worksheets and cells by key + + +1.7.0 (2013-10-31) +================== + + +Major changes +------------- + +Drops support for Python < 2.5 and last version to support Python 2.5 + + +Compatibility +------------- + +Tests run on Python 2.5, 2.6, 2.7, 3.2, 3.3 + + +Merged pull requests +-------------------- + +* 27 Include more metadata +* 41 Able to read files with chart sheets +* 45 Configurable Worksheet classes +* 3 Correct serialisation of Decimal +* 36 Preserve VBA macros when reading files +* 44 Handle empty oddheader and oddFooter tags +* 43 Fixed issue that the reader never set the active sheet +* 33 Reader set value and type explicitly and TYPE_ERROR checking +* 22 added page breaks, fixed formula serialization +* 39 Fix Python 2.6 compatibility +* 47 Improvements in styling + + +Known bugfixes +-------------- + +* `#109 `_ +* `#165 `_ +* `#209 `_ +* `#112 `_ +* `#166 `_ +* `#109 `_ +* `#223 `_ +* `#124 `_ +* `#157 `_ + + +Miscellaneous +------------- + +Performance improvements in optimised writer + +Docs updated diff --git a/doc/charts/area.png b/doc/charts/area.png new file mode 100644 index 0000000..d44cf74 Binary files /dev/null and b/doc/charts/area.png differ diff --git a/doc/charts/area.py b/doc/charts/area.py new file mode 100644 index 0000000..85433c2 --- /dev/null +++ b/doc/charts/area.py @@ -0,0 +1,37 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + AreaChart, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +rows = [ + ['Number', 'Batch 1', 'Batch 2'], + [2, 40, 30], + [3, 40, 25], + [4, 50, 30], + [5, 30, 10], + [6, 25, 5], + [7, 50, 10], +] + +for row in rows: + ws.append(row) + +chart = AreaChart() +chart.title = "Area Chart" +chart.style = 13 +chart.x_axis.title = 'Test' +chart.y_axis.title = 'Percentage' + +cats = Reference(ws, min_col=1, min_row=1, max_row=7) +data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=7) +chart.add_data(data, titles_from_data=True) +chart.set_categories(cats) + +ws.add_chart(chart, "A10") + +wb.save("area.xlsx") diff --git a/doc/charts/area.rst b/doc/charts/area.rst new file mode 100644 index 0000000..2b64a0a --- /dev/null +++ b/doc/charts/area.rst @@ -0,0 +1,29 @@ +Area Charts +=========== + + +2D Area Charts +-------------- + +Area charts are similar to line charts with the addition that the area underneath the plotted line is filled. +Different variants are available by setting the grouping to "standard", "stacked" or "percentStacked"; "standard" is the default. + +.. literalinclude:: area.py + + +.. image:: area.png + :alt: "Sample area charts" + + +3D Area Charts +-------------- + +You can also create 3D area charts + +.. literalinclude:: area3d.py + + +This produces a simple 3D area chart where third axis can be used to replace the legend: + +.. image:: area3D.png + :alt: "Sample 3D area chart with a series axis" diff --git a/doc/charts/area3D.png b/doc/charts/area3D.png new file mode 100644 index 0000000..4be6a52 Binary files /dev/null and b/doc/charts/area3D.png differ diff --git a/doc/charts/area3d.py b/doc/charts/area3d.py new file mode 100644 index 0000000..6f537ca --- /dev/null +++ b/doc/charts/area3d.py @@ -0,0 +1,38 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + AreaChart3D, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +rows = [ + ['Number', 'Batch 1', 'Batch 2'], + [2, 30, 40], + [3, 25, 40], + [4 ,30, 50], + [5 ,10, 30], + [6, 5, 25], + [7 ,10, 50], +] + +for row in rows: + ws.append(row) + +chart = AreaChart3D() +chart.title = "Area Chart" +chart.style = 13 +chart.x_axis.title = 'Test' +chart.y_axis.title = 'Percentage' +chart.legend = None + +cats = Reference(ws, min_col=1, min_row=1, max_row=7) +data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=7) +chart.add_data(data, titles_from_data=True) +chart.set_categories(cats) + +ws.add_chart(chart, "A10") + +wb.save("area3D.xlsx") diff --git a/doc/charts/bar.png b/doc/charts/bar.png new file mode 100644 index 0000000..0c46401 Binary files /dev/null and b/doc/charts/bar.png differ diff --git a/doc/charts/bar.py b/doc/charts/bar.py new file mode 100644 index 0000000..f90b18b --- /dev/null +++ b/doc/charts/bar.py @@ -0,0 +1,65 @@ +from openpyxl import Workbook +from openpyxl.chart import BarChart, Series, Reference + +wb = Workbook(write_only=True) +ws = wb.create_sheet() + +rows = [ + ('Number', 'Batch 1', 'Batch 2'), + (2, 10, 30), + (3, 40, 60), + (4, 50, 70), + (5, 20, 10), + (6, 10, 40), + (7, 50, 30), +] + + +for row in rows: + ws.append(row) + + +chart1 = BarChart() +chart1.type = "col" +chart1.style = 10 +chart1.title = "Bar Chart" +chart1.y_axis.title = 'Test number' +chart1.x_axis.title = 'Sample length (mm)' + +data = Reference(ws, min_col=2, min_row=1, max_row=7, max_col=3) +cats = Reference(ws, min_col=1, min_row=2, max_row=7) +chart1.add_data(data, titles_from_data=True) +chart1.set_categories(cats) +chart1.shape = 4 +ws.add_chart(chart1, "A10") + +from copy import deepcopy + +chart2 = deepcopy(chart1) +chart2.style = 11 +chart2.type = "bar" +chart2.title = "Horizontal Bar Chart" + +ws.add_chart(chart2, "G10") + + +chart3 = deepcopy(chart1) +chart3.type = "col" +chart3.style = 12 +chart3.grouping = "stacked" +chart3.overlap = 100 +chart3.title = 'Stacked Chart' + +ws.add_chart(chart3, "A27") + + +chart4 = deepcopy(chart1) +chart4.type = "bar" +chart4.style = 13 +chart4.grouping = "percentStacked" +chart4.overlap = 100 +chart4.title = 'Percent Stacked Chart' + +ws.add_chart(chart4, "G27") + +wb.save("bar.xlsx") diff --git a/doc/charts/bar.rst b/doc/charts/bar.rst new file mode 100644 index 0000000..86c9832 --- /dev/null +++ b/doc/charts/bar.rst @@ -0,0 +1,41 @@ +Bar and Column Charts +===================== + +In bar charts values are plotted as either horizontal bars or vertical columns. + +Vertical, Horizontal and Stacked Bar Charts +------------------------------------------- + +.. note:: + + The following settings affect the different chart types. + + Switch between vertical and horizontal bar charts by setting `type` to + `col` or `bar` respectively. + + When using stacked charts the `overlap` needs to be set to 100. + + If bars are horizontal, x and y axes are revesed. + + +.. image:: bar.png + :alt: "Sample bar charts" + + +.. literalinclude:: bar.py + +This will produce four charts illustrating the various possibilities. + + +3D Bar Charts +------------- + +You can also create 3D bar charts + +.. literalinclude:: bar3d.py + + +This produces a simple 3D bar chart + +.. image:: bar3D.png + :alt: "Sample 3D bar chart" diff --git a/doc/charts/bar3D.png b/doc/charts/bar3D.png new file mode 100644 index 0000000..1537cba Binary files /dev/null and b/doc/charts/bar3D.png differ diff --git a/doc/charts/bar3d.py b/doc/charts/bar3d.py new file mode 100644 index 0000000..0f959fc --- /dev/null +++ b/doc/charts/bar3d.py @@ -0,0 +1,29 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + Reference, + Series, + BarChart3D, +) + +wb = Workbook() +ws = wb.active + +rows = [ + (None, 2013, 2014), + ("Apples", 5, 4), + ("Oranges", 6, 2), + ("Pears", 8, 3) +] + +for row in rows: + ws.append(row) + +data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=4) +titles = Reference(ws, min_col=1, min_row=2, max_row=4) +chart = BarChart3D() +chart.title = "3D Bar Chart" +chart.add_data(data=data, titles_from_data=True) +chart.set_categories(titles) + +ws.add_chart(chart, "E5") +wb.save("bar3d.xlsx") diff --git a/doc/charts/bubble.png b/doc/charts/bubble.png new file mode 100644 index 0000000..9858345 Binary files /dev/null and b/doc/charts/bubble.png differ diff --git a/doc/charts/bubble.py b/doc/charts/bubble.py new file mode 100644 index 0000000..73f7092 --- /dev/null +++ b/doc/charts/bubble.py @@ -0,0 +1,46 @@ +""" +Sample bubble chart +""" + +from openpyxl import Workbook +from openpyxl.chart import Series, Reference, BubbleChart + +wb = Workbook() +ws = wb.active + +rows = [ + ("Number of Products", "Sales in USD", "Market share"), + (14, 12200, 15), + (20, 60000, 33), + (18, 24400, 10), + (22, 32000, 42), + (), + (12, 8200, 18), + (15, 50000, 30), + (19, 22400, 15), + (25, 25000, 50), +] + +for row in rows: + ws.append(row) + +chart = BubbleChart() +chart.style = 18 # use a preset style + +# add the first series of data +xvalues = Reference(ws, min_col=1, min_row=2, max_row=5) +yvalues = Reference(ws, min_col=2, min_row=2, max_row=5) +size = Reference(ws, min_col=3, min_row=2, max_row=5) +series = Series(values=yvalues, xvalues=xvalues, zvalues=size, title="2013") +chart.series.append(series) + +# add the second +xvalues = Reference(ws, min_col=1, min_row=7, max_row=10) +yvalues = Reference(ws, min_col=2, min_row=7, max_row=10) +size = Reference(ws, min_col=3, min_row=7, max_row=10) +series = Series(values=yvalues, xvalues=xvalues, zvalues=size, title="2014") +chart.series.append(series) + +# place the chart starting in cell E1 +ws.add_chart(chart, "E1") +wb.save("bubble.xlsx") diff --git a/doc/charts/bubble.rst b/doc/charts/bubble.rst new file mode 100644 index 0000000..7dc85ed --- /dev/null +++ b/doc/charts/bubble.rst @@ -0,0 +1,13 @@ +Bubble Charts +============= + +Bubble charts are similar to scatter charts but use a third dimension to determine the size of the bubbles. +Charts can include multiple series. + +.. literalinclude:: bubble.py + + +This will produce bubble chart with two series and should look something like this + +.. image:: bubble.png + :alt: "Sample bubble chart" diff --git a/doc/charts/chart_layout.png b/doc/charts/chart_layout.png new file mode 100644 index 0000000..3776b68 Binary files /dev/null and b/doc/charts/chart_layout.png differ diff --git a/doc/charts/chart_layout.py b/doc/charts/chart_layout.py new file mode 100644 index 0000000..b993825 --- /dev/null +++ b/doc/charts/chart_layout.py @@ -0,0 +1,78 @@ +from openpyxl import Workbook, load_workbook +from openpyxl.chart import ScatterChart, Series, Reference +from openpyxl.chart.layout import Layout, ManualLayout + +wb = Workbook() +ws = wb.active + +rows = [ + ['Size', 'Batch 1', 'Batch 2'], + [2, 40, 30], + [3, 40, 25], + [4, 50, 30], + [5, 30, 25], + [6, 25, 35], + [7, 20, 40], +] + +for row in rows: + ws.append(row) + +ch1 = ScatterChart() +xvalues = Reference(ws, min_col=1, min_row=2, max_row=7) +for i in range(2, 4): + values = Reference(ws, min_col=i, min_row=1, max_row=7) + series = Series(values, xvalues, title_from_data=True) + ch1.series.append(series) + + +ch1.title = "Default layout" +ch1.style = 13 +ch1.x_axis.title = 'Size' +ch1.y_axis.title = 'Percentage' +ch1.legend.position = 'r' + +ws.add_chart(ch1, "B10") + +from copy import deepcopy + +# Half-size chart, bottom right +ch2 = deepcopy(ch1) +ch2.title = "Manual chart layout" +ch2.legend.position = "tr" +ch2.layout=Layout( + manualLayout=ManualLayout( + x=0.25, y=0.25, + h=0.5, w=0.5, + ) +) +ws.add_chart(ch2, "H10") + +# Half-size chart, centred +ch3 = deepcopy(ch1) +ch3.layout = Layout( + ManualLayout( + x=0.25, y=0.25, + h=0.5, w=0.5, + xMode="edge", + yMode="edge", + ) +) +ch3.title = "Manual chart layout, edge mode" +ws.add_chart(ch3, "B27") + +# Manually position the legend bottom left +ch4 = deepcopy(ch1) +ch4.title = "Manual legend layout" +ch4.legend.layout = Layout( + manualLayout=ManualLayout( + yMode='edge', + xMode='edge', + x=0, y=0.9, + h=0.1, w=0.5 + ) +) + +ws.add_chart(ch4, "H27") + +wb.save("chart_layout.xlsx") diff --git a/doc/charts/chart_layout.rst b/doc/charts/chart_layout.rst new file mode 100644 index 0000000..216c315 --- /dev/null +++ b/doc/charts/chart_layout.rst @@ -0,0 +1,71 @@ +Changing the layout of plot area and legend +=========================================== + + +The layout of the chart within the canvas can be set by using the layout +property an instance of a layout class. + + +Chart layout +------------ + +Size and position ++++++++++++++++++ + +The chart can be positioned within its container. ``x`` and ``y`` adjust +position, ``w`` and ``h`` adjust the size . The units are proportions of the +container. A chart cannot be positioned outside of its container and the +width and height are the dominant constraints: if x + w > 1, then x = 1 - w. + +x is the horizontal position from the left +y is the vertical position the top +h is the height of the chart relative to its container +w is the width of the box + + +Mode +++++ + +In addition to the size and position the mode for the relevant attribute can +also be set to either `factor` or `edge`. Factor is the default: + +.. code:: + + layout.xMode = edge + + +Target +++++++ + +The layoutTarget can be set to ``outer`` or ``inner``. The default is ``outer``: + +.. code:: + + layout.layoutTarget = inner + + +Legend layout +------------- + +The position of the legend can be controlled either by setting its position: +``r``, ``l``, ``t``, ``b``, and ``tr``, for right, left, top, bottom and top +right respectively. The default is ``r``. + +.. code:: + + legend.position = 'tr' + +or applying a manual layout: + +.. code:: + + legend.layout = ManualLayout() + + +.. literalinclude:: chart_layout.py + + +This produces four charts illustrating various possibilities: + +.. image:: chart_layout.png + :alt: "Different chart and legend layouts" diff --git a/doc/charts/chart_layout_default.png b/doc/charts/chart_layout_default.png new file mode 100644 index 0000000..7baaab6 Binary files /dev/null and b/doc/charts/chart_layout_default.png differ diff --git a/doc/charts/chartsheet.png b/doc/charts/chartsheet.png new file mode 100644 index 0000000..66b5655 Binary files /dev/null and b/doc/charts/chartsheet.png differ diff --git a/doc/charts/chartsheet.py b/doc/charts/chartsheet.py new file mode 100644 index 0000000..f3a6e24 --- /dev/null +++ b/doc/charts/chartsheet.py @@ -0,0 +1,27 @@ +from openpyxl import Workbook + +from openpyxl.chart import PieChart, Reference, Series + +wb = Workbook() +ws = wb.active +cs = wb.create_chartsheet() + +rows = [ + ["Bob", 3], + ["Harry", 2], + ["James", 4], +] + +for row in rows: + ws.append(row) + + +chart = PieChart() +labels = Reference(ws, min_col=1, min_row=1, max_row=3) +data = Reference(ws, min_col=2, min_row=1, max_row=3) +chart.series = (Series(data),) +chart.title = "PieChart" + +cs.add_chart(chart) + +wb.save("demo.xlsx") diff --git a/doc/charts/chartsheet.rst b/doc/charts/chartsheet.rst new file mode 100644 index 0000000..347d3a7 --- /dev/null +++ b/doc/charts/chartsheet.rst @@ -0,0 +1,11 @@ +Chartsheets +=========== + +Chartsheets are special worksheets which only contain charts. All the data +for the chart must be on a different worksheet. + +.. literalinclude:: chartsheet.py + + +.. image:: chartsheet.png + :alt: "Sample chartsheet" diff --git a/doc/charts/doughnut.png b/doc/charts/doughnut.png new file mode 100644 index 0000000..06025c3 Binary files /dev/null and b/doc/charts/doughnut.png differ diff --git a/doc/charts/doughnut.py b/doc/charts/doughnut.py new file mode 100644 index 0000000..d0d489d --- /dev/null +++ b/doc/charts/doughnut.py @@ -0,0 +1,55 @@ +from openpyxl import Workbook + +from openpyxl.chart import ( + DoughnutChart, + Reference, + Series, +) +from openpyxl.chart.series import DataPoint + +data = [ + ['Pie', 2014, 2015], + ['Plain', 40, 50], + ['Jam', 2, 10], + ['Lime', 20, 30], + ['Chocolate', 30, 40], +] + +wb = Workbook() +ws = wb.active + +for row in data: + ws.append(row) + +chart = DoughnutChart() +labels = Reference(ws, min_col=1, min_row=2, max_row=5) +data = Reference(ws, min_col=2, min_row=1, max_row=5) +chart.add_data(data, titles_from_data=True) +chart.set_categories(labels) +chart.title = "Doughnuts sold by category" +chart.style = 26 + +# Cut the first slice out of the doughnut +slices = [DataPoint(idx=i) for i in range(4)] +plain, jam, lime, chocolate = slices +chart.series[0].data_points = slices +plain.graphicalProperties.solidFill = "FAE1D0" +jam.graphicalProperties.solidFill = "BB2244" +lime.graphicalProperties.solidFill = "22DD22" +chocolate.graphicalProperties.solidFill = "61210B" +chocolate.explosion = 10 + +ws.add_chart(chart, "E1") + +from copy import deepcopy + +chart2 = deepcopy(chart) +chart2.title = None +data = Reference(ws, min_col=3, min_row=1, max_row=5) +series2 = Series(data, title_from_data=True) +series2.data_points = slices +chart2.series.append(series2) + +ws.add_chart(chart2, "E17") + +wb.save("doughnut.xlsx") diff --git a/doc/charts/doughnut.rst b/doc/charts/doughnut.rst new file mode 100644 index 0000000..21b97e6 --- /dev/null +++ b/doc/charts/doughnut.rst @@ -0,0 +1,12 @@ +Doughnut Charts +--------------- + +Doughnut charts are similar to pie charts except that they use a ring instead +of a circle. They can also plot several series of data as concentric rings. + + +.. literalinclude:: doughnut.py + + +.. image:: doughnut.png + :alt: "Sample doughnut charts" diff --git a/doc/charts/gauge.png b/doc/charts/gauge.png new file mode 100644 index 0000000..e48c41c Binary files /dev/null and b/doc/charts/gauge.png differ diff --git a/doc/charts/gauge.py b/doc/charts/gauge.py new file mode 100644 index 0000000..a9b0f64 --- /dev/null +++ b/doc/charts/gauge.py @@ -0,0 +1,57 @@ +from openpyxl import Workbook + +from openpyxl.chart import PieChart, DoughnutChart, Series, Reference +from openpyxl.chart.series import DataPoint + + +data = [ + ["Donut", "Pie"], + [25, 75], + [50, 1], + [25, 124], + [100], +] + +# based on http://www.excel-easy.com/examples/gauge-chart.html + +wb = Workbook() +ws = wb.active +for row in data: + ws.append(row) + +# First chart is a doughnut chart +c1 = DoughnutChart(firstSliceAng=270, holeSize=50) +c1.title = "Code coverage" +c1.legend = None + +ref = Reference(ws, min_col=1, min_row=2, max_row=5) +s1 = Series(ref, title_from_data=False) + +slices = [DataPoint(idx=i) for i in range(4)] +slices[0].graphicalProperties.solidFill = "FF3300" # red +slices[1].graphicalProperties.solidFill = "FCF305" # yellow +slices[2].graphicalProperties.solidFill = "1FB714" # green +slices[3].graphicalProperties.noFill = True # invisible + +s1.data_points = slices +c1.series = [s1] + +# Second chart is a pie chart +c2 = PieChart(firstSliceAng=270) +c2.legend = None + +ref = Reference(ws, min_col=2, min_row=2, max_col=2, max_row=4) +s2 = Series(ref, title_from_data=False) + +slices = [DataPoint(idx=i) for i in range(3)] +slices[0].graphicalProperties.noFill = True # invisible +slices[1].graphicalProperties.solidFill = "000000" # black needle +slices[2].graphicalProperties.noFill = True # invisible +s2.data_points = slices +c2.series = [s2] + +c1 += c2 # combine charts + +ws.add_chart(c1, "D1") + +wb.save("gauge.xlsx") diff --git a/doc/charts/gauge.rst b/doc/charts/gauge.rst new file mode 100644 index 0000000..1245e11 --- /dev/null +++ b/doc/charts/gauge.rst @@ -0,0 +1,15 @@ +Gauge Charts +============ + + +Gauge charts combine a pie chart and a doughnut chart to create a "gauge". The first chart is a doughnut chart with four slices. The first three slices correspond to the colours of the gauge; the fourth slice, which is half of the doughnut, is made invisible. + +A pie chart containing three slices is added. The first and third slice are invisible so that the second slice can act as the needle on the gauge. + +The effects are done using the graphical properties of individual data points in a data series. + +.. literalinclude:: gauge.py + + +.. image:: gauge.png + :alt: "Sample gauge chart" diff --git a/doc/charts/introduction.rst b/doc/charts/introduction.rst new file mode 100644 index 0000000..e53a508 --- /dev/null +++ b/doc/charts/introduction.rst @@ -0,0 +1,95 @@ +Charts +====== + + +Chart types +----------- + +The following charts are available: + +.. toctree:: + + area + bar + bubble + line + scatter + pie + doughnut + radar + stock + surface + + +Creating a chart +---------------- + +Charts are composed of at least one series of one or more data points. Series +themselves are comprised of references to cell ranges. + +.. :: doctest + +>>> from openpyxl import Workbook +>>> wb = Workbook() +>>> ws = wb.active +>>> for i in range(10): +... ws.append([i]) +>>> +>>> from openpyxl.chart import BarChart, Reference, Series +>>> values = Reference(ws, min_col=1, min_row=1, max_col=1, max_row=10) +>>> chart = BarChart() +>>> chart.add_data(values) +>>> ws.add_chart(chart, "E15") +>>> wb.save("SampleChart.xlsx") + + +By default the top-left corner of a chart is anchored to cell E15 and the +size is 15 x 7.5 cm (approximately 5 columns by 14 rows). This can be changed +by setting the `anchor`, `width` and `height` properties of the chart. The +actual size will depend on operating system and device. Other anchors are +possible see :mod:`openpyxl.drawing.spreadsheet_drawing` for further information. + + +Working with axes +----------------- + +.. toctree:: + + limits_and_scaling + secondary + + +Change the chart layout +----------------------- + +.. toctree:: + + chart_layout + + +Styling charts +-------------- + +.. toctree:: + + pattern + + +Advanced charts +--------------- + +Charts can be combined to create new charts: + +.. toctree:: + + gauge + + +Using chartsheets +----------------- + +Charts can be added to special worksheets called chartsheets: + +.. toctree:: + + chartsheet diff --git a/doc/charts/limits_and_scaling.rst b/doc/charts/limits_and_scaling.rst new file mode 100644 index 0000000..5d1b411 --- /dev/null +++ b/doc/charts/limits_and_scaling.rst @@ -0,0 +1,60 @@ +Axis Limits and Scale +===================== + +Minima and Maxima +----------------- + +Axis minimum and maximum values can be set manually to display specific regions +on a chart. + +.. literalinclude:: limits_and_scaling_minmax.py + + +.. image:: limits_and_scaling_minmax.png + :alt: "Sample charts with examples of axis clipping" + + +.. note:: + + In some cases such as the one shown, setting the axis limits is effectively + equivalent to displaying a sub-range of the data. For large datasets, + rendering of scatter plots (and possibly others) will be much faster when + using subsets of the data rather than axis limits in both Excel and + Open/Libre Office. + + +Logarithmic Scaling +------------------- + +Both the x- and y-axes can be scaled logarithmically. The base of the logarithm +can be set to any valid float. If the x-axis is scaled logarithmically, negative +values in the domain will be discarded. + +.. literalinclude:: limits_and_scaling_log.py + + +This produces five charts that look something like this: + +.. image:: limits_and_scaling_log.png + :alt: "Sample charts with examples of axis log scaling" + +The first four charts show the same data unscaled, scaled logarithmically in +each axis and in both axes, with the logarithm base set to 10. The final chart +shows the same data with both axes scaled, but the base of the logarithm set to +``e``. + +Axis Orientation +---------------- + +Axes can be displayed "normally" or in reverse. Axis orientation is controlled +by the scaling ``orientation`` property, which can have a value of either +``'minMax'`` for normal orientation or ``'maxMin'`` for reversed. + +.. literalinclude:: limits_and_scaling_orientation.py + + +This produces four charts with the axes in each possible combination of +orientations that look something like this: + +.. image:: limits_and_scaling_orientation.png + :alt: "Sample charts with different axis orientations" diff --git a/doc/charts/limits_and_scaling_log.png b/doc/charts/limits_and_scaling_log.png new file mode 100644 index 0000000..7bb63ce Binary files /dev/null and b/doc/charts/limits_and_scaling_log.png differ diff --git a/doc/charts/limits_and_scaling_log.py b/doc/charts/limits_and_scaling_log.py new file mode 100644 index 0000000..9699a49 --- /dev/null +++ b/doc/charts/limits_and_scaling_log.py @@ -0,0 +1,67 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + ScatterChart, + Reference, + Series, +) +import math + +wb = Workbook() +ws = wb.active + +ws.append(['X', 'Gaussian']) +for i, x in enumerate(range(-10, 11)): + ws.append([x, "=EXP(-(($A${row}/6)^2))".format(row = i + 2)]) + +chart1 = ScatterChart() +chart1.title = "No Scaling" +chart1.x_axis.title = 'x' +chart1.y_axis.title = 'y' +chart1.legend = None + +chart2 = ScatterChart() +chart2.title = "X Log Scale" +chart2.x_axis.title = 'x (log10)' +chart2.y_axis.title = 'y' +chart2.legend = None +chart2.x_axis.scaling.logBase = 10 + +chart3 = ScatterChart() +chart3.title = "Y Log Scale" +chart3.x_axis.title = 'x' +chart3.y_axis.title = 'y (log10)' +chart3.legend = None +chart3.y_axis.scaling.logBase = 10 + +chart4 = ScatterChart() +chart4.title = "Both Log Scale" +chart4.x_axis.title = 'x (log10)' +chart4.y_axis.title = 'y (log10)' +chart4.legend = None +chart4.x_axis.scaling.logBase = 10 +chart4.y_axis.scaling.logBase = 10 + +chart5 = ScatterChart() +chart5.title = "Log Scale Base e" +chart5.x_axis.title = 'x (ln)' +chart5.y_axis.title = 'y (ln)' +chart5.legend = None +chart5.x_axis.scaling.logBase = math.e +chart5.y_axis.scaling.logBase = math.e + +x = Reference(ws, min_col=1, min_row=2, max_row=22) +y = Reference(ws, min_col=2, min_row=2, max_row=22) +s = Series(y, xvalues=x) +chart1.append(s) +chart2.append(s) +chart3.append(s) +chart4.append(s) +chart5.append(s) + +ws.add_chart(chart1, "C1") +ws.add_chart(chart2, "I1") +ws.add_chart(chart3, "C15") +ws.add_chart(chart4, "I15") +ws.add_chart(chart5, "F30") + +wb.save("log.xlsx") diff --git a/doc/charts/limits_and_scaling_minmax.png b/doc/charts/limits_and_scaling_minmax.png new file mode 100644 index 0000000..0db9c0b Binary files /dev/null and b/doc/charts/limits_and_scaling_minmax.png differ diff --git a/doc/charts/limits_and_scaling_minmax.py b/doc/charts/limits_and_scaling_minmax.py new file mode 100644 index 0000000..4f41e6b --- /dev/null +++ b/doc/charts/limits_and_scaling_minmax.py @@ -0,0 +1,42 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + ScatterChart, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +ws.append(['X', '1/X']) +for x in range(-10, 11): + if x: + ws.append([x, 1.0 / x]) + +chart1 = ScatterChart() +chart1.title = "Full Axes" +chart1.x_axis.title = 'x' +chart1.y_axis.title = '1/x' +chart1.legend = None + +chart2 = ScatterChart() +chart2.title = "Clipped Axes" +chart2.x_axis.title = 'x' +chart2.y_axis.title = '1/x' +chart2.legend = None + +chart2.x_axis.scaling.min = 0 +chart2.y_axis.scaling.min = 0 +chart2.x_axis.scaling.max = 11 +chart2.y_axis.scaling.max = 1.5 + +x = Reference(ws, min_col=1, min_row=2, max_row=22) +y = Reference(ws, min_col=2, min_row=2, max_row=22) +s = Series(y, xvalues=x) +chart1.append(s) +chart2.append(s) + +ws.add_chart(chart1, "C1") +ws.add_chart(chart2, "C15") + +wb.save("minmax.xlsx") diff --git a/doc/charts/limits_and_scaling_orientation.png b/doc/charts/limits_and_scaling_orientation.png new file mode 100644 index 0000000..26f13cc Binary files /dev/null and b/doc/charts/limits_and_scaling_orientation.png differ diff --git a/doc/charts/limits_and_scaling_orientation.py b/doc/charts/limits_and_scaling_orientation.py new file mode 100644 index 0000000..c80c256 --- /dev/null +++ b/doc/charts/limits_and_scaling_orientation.py @@ -0,0 +1,60 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + ScatterChart, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +ws["A1"] = "Archimedean Spiral" +ws.append(["T", "X", "Y"]) +for i, t in enumerate(range(100)): + ws.append([t / 16.0, "=$A${row}*COS($A${row})".format(row = i + 3), + "=$A${row}*SIN($A${row})".format(row = i + 3)]) + +chart1 = ScatterChart() +chart1.title = "Default Orientation" +chart1.x_axis.title = 'x' +chart1.y_axis.title = 'y' +chart1.legend = None + +chart2 = ScatterChart() +chart2.title = "Flip X" +chart2.x_axis.title = 'x' +chart2.y_axis.title = 'y' +chart2.legend = None +chart2.x_axis.scaling.orientation = "maxMin" +chart2.y_axis.scaling.orientation = "minMax" + +chart3 = ScatterChart() +chart3.title = "Flip Y" +chart3.x_axis.title = 'x' +chart3.y_axis.title = 'y' +chart3.legend = None +chart3.x_axis.scaling.orientation = "minMax" +chart3.y_axis.scaling.orientation = "maxMin" + +chart4 = ScatterChart() +chart4.title = "Flip Both" +chart4.x_axis.title = 'x' +chart4.y_axis.title = 'y' +chart4.legend = None +chart4.x_axis.scaling.orientation = "maxMin" +chart4.y_axis.scaling.orientation = "maxMin" + +x = Reference(ws, min_col=2, min_row=2, max_row=102) +y = Reference(ws, min_col=3, min_row=2, max_row=102) +s = Series(y, xvalues=x) +chart1.append(s) +chart2.append(s) +chart3.append(s) +chart4.append(s) + +ws.add_chart(chart1, "D1") +ws.add_chart(chart2, "J1") +ws.add_chart(chart3, "D15") +ws.add_chart(chart4, "J15") + +wb.save("orientation.xlsx") diff --git a/doc/charts/line.png b/doc/charts/line.png new file mode 100644 index 0000000..17703b6 Binary files /dev/null and b/doc/charts/line.png differ diff --git a/doc/charts/line.py b/doc/charts/line.py new file mode 100644 index 0000000..2833d4e --- /dev/null +++ b/doc/charts/line.py @@ -0,0 +1,81 @@ +from datetime import date + +from openpyxl import Workbook +from openpyxl.chart import ( + LineChart, + Reference, +) +from openpyxl.chart.axis import DateAxis + +wb = Workbook() +ws = wb.active + +rows = [ + ['Date', 'Batch 1', 'Batch 2', 'Batch 3'], + [date(2015,9, 1), 40, 30, 25], + [date(2015,9, 2), 40, 25, 30], + [date(2015,9, 3), 50, 30, 45], + [date(2015,9, 4), 30, 25, 40], + [date(2015,9, 5), 25, 35, 30], + [date(2015,9, 6), 20, 40, 35], +] + +for row in rows: + ws.append(row) + +c1 = LineChart() +c1.title = "Line Chart" +c1.style = 13 +c1.y_axis.title = 'Size' +c1.x_axis.title = 'Test Number' + +data = Reference(ws, min_col=2, min_row=1, max_col=4, max_row=7) +c1.add_data(data, titles_from_data=True) + +# Style the lines +s1 = c1.series[0] +s1.marker.symbol = "triangle" +s1.marker.graphicalProperties.solidFill = "FF0000" # Marker filling +s1.marker.graphicalProperties.line.solidFill = "FF0000" # Marker outline + +s1.graphicalProperties.line.noFill = True + +s2 = c1.series[1] +s2.graphicalProperties.line.solidFill = "00AAAA" +s2.graphicalProperties.line.dashStyle = "sysDot" +s2.graphicalProperties.line.width = 100050 # width in EMUs + +s2 = c1.series[2] +s2.smooth = True # Make the line smooth + +ws.add_chart(c1, "A10") + +from copy import deepcopy +stacked = deepcopy(c1) +stacked.grouping = "stacked" +stacked.title = "Stacked Line Chart" +ws.add_chart(stacked, "A27") + +percent_stacked = deepcopy(c1) +percent_stacked.grouping = "percentStacked" +percent_stacked.title = "Percent Stacked Line Chart" +ws.add_chart(percent_stacked, "A44") + +# Chart with date axis +c2 = LineChart() +c2.title = "Date Axis" +c2.style = 12 +c2.y_axis.title = "Size" +c2.y_axis.crossAx = 500 +c2.x_axis = DateAxis(crossAx=100) +c2.x_axis.number_format = 'd-mmm' +c2.x_axis.majorTimeUnit = "days" +c2.x_axis.title = "Date" + +c2.add_data(data, titles_from_data=True) +dates = Reference(ws, min_col=1, min_row=2, max_row=7) +c2.set_categories(dates) + +ws.add_chart(c2, "A61") + +wb.save("line.xlsx") diff --git a/doc/charts/line.rst b/doc/charts/line.rst new file mode 100644 index 0000000..ae48cd9 --- /dev/null +++ b/doc/charts/line.rst @@ -0,0 +1,33 @@ +Line Charts +=========== + +Line Charts +----------- + +Line charts allow data to be plotted against a fixed axis. They are similar +to scatter charts, the main difference is that with line charts each data +series is plotted against the same values. Different kinds of axes can be +used for the secondary axes. + +Similar to bar charts there are three kinds of line charts: standard, stacked +and percentStacked. + + +.. literalinclude:: line.py + + +.. image:: line.png + :alt: "Sample line charts" + + +3D Line Charts +-------------- + +In 3D line charts the third axis is the same as the legend for the series. + + +.. literalinclude:: line3D.py + + +.. image:: line3D.png + :alt: "Sample 3D line chart" diff --git a/doc/charts/line3D.png b/doc/charts/line3D.png new file mode 100644 index 0000000..fbcbad6 Binary files /dev/null and b/doc/charts/line3D.png differ diff --git a/doc/charts/line3D.py b/doc/charts/line3D.py new file mode 100644 index 0000000..1fbdcfb --- /dev/null +++ b/doc/charts/line3D.py @@ -0,0 +1,38 @@ +from datetime import date + +from openpyxl import Workbook +from openpyxl.chart import ( + LineChart3D, + Reference, +) +from openpyxl.chart.axis import DateAxis + +wb = Workbook() +ws = wb.active + +rows = [ + ['Date', 'Batch 1', 'Batch 2', 'Batch 3'], + [date(2015,9, 1), 40, 30, 25], + [date(2015,9, 2), 40, 25, 30], + [date(2015,9, 3), 50, 30, 45], + [date(2015,9, 4), 30, 25, 40], + [date(2015,9, 5), 25, 35, 30], + [date(2015,9, 6), 20, 40, 35], +] + +for row in rows: + ws.append(row) + +c1 = LineChart3D() +c1.title = "3D Line Chart" +c1.legend = None +c1.style = 15 +c1.y_axis.title = 'Size' +c1.x_axis.title = 'Test Number' + +data = Reference(ws, min_col=2, min_row=1, max_col=4, max_row=7) +c1.add_data(data, titles_from_data=True) + +ws.add_chart(c1, "A10") + +wb.save("line3D.xlsx") diff --git a/doc/charts/pattern.png b/doc/charts/pattern.png new file mode 100644 index 0000000..de5ead7 Binary files /dev/null and b/doc/charts/pattern.png differ diff --git a/doc/charts/pattern.py b/doc/charts/pattern.py new file mode 100644 index 0000000..b1da0e2 --- /dev/null +++ b/doc/charts/pattern.py @@ -0,0 +1,45 @@ +from openpyxl import Workbook +from openpyxl.chart import BarChart, Reference +from openpyxl.chart.marker import DataPoint + +from openpyxl.drawing.fill import PatternFillProperties, ColorChoice + +wb = Workbook() +ws = wb.active + +rows = [ + ("Sample",), + (1,), + (2,), + (3,), + (2,), + (3,), + (3,), + (1,), + (2,), +] + +for r in rows: + ws.append(r) + + +c = BarChart() +data = Reference(ws, min_col=1, min_row=1, max_row=8) +c.add_data(data, titles_from_data=True) +c.title = "Chart with patterns" + +# set a pattern for the whole series +series = c.series[0] +fill = PatternFillProperties(prst="pct5") +fill.foreground = ColorChoice(prstClr="red") +fill.background = ColorChoice(prstClr="blue") +series.graphicalProperties.pattFill = fill + +# set a pattern for a 6th data point (0-indexed) +pt = DataPoint(idx=5) +pt.graphicalProperties.pattFill = PatternFillProperties(prst="ltHorz") +series.dPt.append(pt) + +ws.add_chart(c, "C1") + +wb.save("pattern.xlsx") diff --git a/doc/charts/pattern.rst b/doc/charts/pattern.rst new file mode 100644 index 0000000..5c5789f --- /dev/null +++ b/doc/charts/pattern.rst @@ -0,0 +1,12 @@ +Adding Patterns +--------------- + + +Whole data series and individual data points can be extensively styled through the `graphicalProperties`. Getting things just right may take some time. + + +.. literalinclude:: pattern.py + + +.. image:: pattern.png + :alt: "Sample bar chart with patterned columns" diff --git a/doc/charts/pie.png b/doc/charts/pie.png new file mode 100644 index 0000000..fc5a087 Binary files /dev/null and b/doc/charts/pie.png differ diff --git a/doc/charts/pie.py b/doc/charts/pie.py new file mode 100644 index 0000000..bb372d7 --- /dev/null +++ b/doc/charts/pie.py @@ -0,0 +1,68 @@ +from openpyxl import Workbook + +from openpyxl.chart import ( + PieChart, + ProjectedPieChart, + Reference +) +from openpyxl.chart.series import DataPoint + +data = [ + ['Pie', 'Sold'], + ['Apple', 50], + ['Cherry', 30], + ['Pumpkin', 10], + ['Chocolate', 40], +] + +wb = Workbook() +ws = wb.active + +for row in data: + ws.append(row) + +pie = PieChart() +labels = Reference(ws, min_col=1, min_row=2, max_row=5) +data = Reference(ws, min_col=2, min_row=1, max_row=5) +pie.add_data(data, titles_from_data=True) +pie.set_categories(labels) +pie.title = "Pies sold by category" + +# Cut the first slice out of the pie +slice = DataPoint(idx=0, explosion=20) +pie.series[0].data_points = [slice] + +ws.add_chart(pie, "D1") + + +ws = wb.create_sheet(title="Projection") + +data = [ + ['Page', 'Views'], + ['Search', 95], + ['Products', 4], + ['Offers', 0.5], + ['Sales', 0.5], +] + +for row in data: + ws.append(row) + +projected_pie = ProjectedPieChart() +projected_pie.type = "pie" +projected_pie.splitType = "val" # split by value +labels = Reference(ws, min_col=1, min_row=2, max_row=5) +data = Reference(ws, min_col=2, min_row=1, max_row=5) +projected_pie.add_data(data, titles_from_data=True) +projected_pie.set_categories(labels) + +ws.add_chart(projected_pie, "A10") + +from copy import deepcopy +projected_bar = deepcopy(projected_pie) +projected_bar.type = "bar" +projected_bar.splitType = 'pos' # split by position + +ws.add_chart(projected_bar, "A27") + +wb.save("pie.xlsx") diff --git a/doc/charts/pie.rst b/doc/charts/pie.rst new file mode 100644 index 0000000..46ff973 --- /dev/null +++ b/doc/charts/pie.rst @@ -0,0 +1,45 @@ +Pie Charts +========== + + +Pie Charts +---------- + +Pie charts plot data as slices of a circle with each slice representing the +percentage of the whole. Slices are plotted in a clockwise direction with 0° +being at the top of the circle. Pie charts can only take a single series of +data. The title of the chart will default to being the title of the series. + + +.. literalinclude:: pie.py + + +.. image:: pie.png + :alt: "Sample pie chart" + + +Projected Pie Charts +-------------------- + +Projected pie charts extract some slices from a pie chart and project them +into a second pie or bar chart. This is useful when there are several smaller +items in the data series. The chart can be split according percent, val(ue) +or pos(ition). If nothing is set then the application decides which to use. +In addition custom splits can be defined. + + +.. image:: projected-pie.png + :alt: "Sample pie chart with projections" + + +3D Pie Charts +------------- + +Pie charts can also be created with a 3D effect. + + +.. literalinclude:: pie3D.py + + +.. image:: pie3D.png + :alt: "Sample 3D pie chart" diff --git a/doc/charts/pie3D.png b/doc/charts/pie3D.png new file mode 100644 index 0000000..65de8e3 Binary files /dev/null and b/doc/charts/pie3D.png differ diff --git a/doc/charts/pie3D.py b/doc/charts/pie3D.py new file mode 100644 index 0000000..bcf2a42 --- /dev/null +++ b/doc/charts/pie3D.py @@ -0,0 +1,32 @@ +from openpyxl import Workbook + +from openpyxl.chart import ( + PieChart3D, + Reference +) + +data = [ + ['Pie', 'Sold'], + ['Apple', 50], + ['Cherry', 30], + ['Pumpkin', 10], + ['Chocolate', 40], +] + +wb = Workbook() +ws = wb.active + +for row in data: + ws.append(row) + +pie = PieChart3D() +labels = Reference(ws, min_col=1, min_row=2, max_row=5) +data = Reference(ws, min_col=2, min_row=1, max_row=5) +pie.add_data(data, titles_from_data=True) +pie.set_categories(labels) +pie.title = "Pies sold by category" + + +ws.add_chart(pie, "D1") + +wb.save("pie3D.xlsx") diff --git a/doc/charts/projected-pie.png b/doc/charts/projected-pie.png new file mode 100644 index 0000000..b87b340 Binary files /dev/null and b/doc/charts/projected-pie.png differ diff --git a/doc/charts/radar.png b/doc/charts/radar.png new file mode 100644 index 0000000..3222779 Binary files /dev/null and b/doc/charts/radar.png differ diff --git a/doc/charts/radar.py b/doc/charts/radar.py new file mode 100644 index 0000000..b66da4b --- /dev/null +++ b/doc/charts/radar.py @@ -0,0 +1,41 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + RadarChart, + Reference, +) + +wb = Workbook() +ws = wb.active + +rows = [ + ['Month', "Bulbs", "Seeds", "Flowers", "Trees & shrubs"], + ['Jan', 0, 2500, 500, 0,], + ['Feb', 0, 5500, 750, 1500], + ['Mar', 0, 9000, 1500, 2500], + ['Apr', 0, 6500, 2000, 4000], + ['May', 0, 3500, 5500, 3500], + ['Jun', 0, 0, 7500, 1500], + ['Jul', 0, 0, 8500, 800], + ['Aug', 1500, 0, 7000, 550], + ['Sep', 5000, 0, 3500, 2500], + ['Oct', 8500, 0, 2500, 6000], + ['Nov', 3500, 0, 500, 5500], + ['Dec', 500, 0, 100, 3000 ], +] + +for row in rows: + ws.append(row) + +chart = RadarChart() +chart.type = "filled" +labels = Reference(ws, min_col=1, min_row=2, max_row=13) +data = Reference(ws, min_col=2, max_col=5, min_row=1, max_row=13) +chart.add_data(data, titles_from_data=True) +chart.set_categories(labels) +chart.style = 26 +chart.title = "Garden Centre Sales" +chart.y_axis.delete = True + +ws.add_chart(chart, "A17") + +wb.save("radar.xlsx") diff --git a/doc/charts/radar.rst b/doc/charts/radar.rst new file mode 100644 index 0000000..f587563 --- /dev/null +++ b/doc/charts/radar.rst @@ -0,0 +1,18 @@ +Radar Charts +------------ + +Data that is arranged in columns or rows on a worksheet can be plotted in a +radar chart. Radar charts compare the aggregate values of multiple data +series. It is effectively a projection of an area chart on a circular x-axis. + +There are two types of radar chart: standard, where the area is marked with a +line; and filled where the where the whole area is filled. The additional +type "marker" has no effect. If markers are desired these can be set for the +relevant series. + + +.. literalinclude:: radar.py + + +.. image:: radar.png + :alt: "Sample radar chart" diff --git a/doc/charts/scatter.png b/doc/charts/scatter.png new file mode 100644 index 0000000..5a9f8cc Binary files /dev/null and b/doc/charts/scatter.png differ diff --git a/doc/charts/scatter.py b/doc/charts/scatter.py new file mode 100644 index 0000000..c4ad4cd --- /dev/null +++ b/doc/charts/scatter.py @@ -0,0 +1,38 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + ScatterChart, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +rows = [ + ['Size', 'Batch 1', 'Batch 2'], + [2, 40, 30], + [3, 40, 25], + [4, 50, 30], + [5, 30, 25], + [6, 25, 35], + [7, 20, 40], +] + +for row in rows: + ws.append(row) + +chart = ScatterChart() +chart.title = "Scatter Chart" +chart.style = 13 +chart.x_axis.title = 'Size' +chart.y_axis.title = 'Percentage' + +xvalues = Reference(ws, min_col=1, min_row=2, max_row=7) +for i in range(2, 4): + values = Reference(ws, min_col=i, min_row=1, max_row=7) + series = Series(values, xvalues, title_from_data=True) + chart.series.append(series) + +ws.add_chart(chart, "A10") + +wb.save("scatter.xlsx") diff --git a/doc/charts/scatter.rst b/doc/charts/scatter.rst new file mode 100644 index 0000000..6fe0c35 --- /dev/null +++ b/doc/charts/scatter.rst @@ -0,0 +1,22 @@ +Scatter Charts +============== + + +Scatter, or xy, charts are similar to some line charts. The main difference +is that one series of values is plotted against another. This is useful where +values are unordered. + +.. literalinclude:: scatter.py + + +.. image:: scatter.png + :alt: "Sample scatter chart" + + +.. Note:: + + The specification says that there are the following types of scatter charts: + 'line', 'lineMarker', 'marker', 'smooth', 'smoothMarker'. However, at least + in Microsoft Excel, this is just a shortcut for other settings that otherwise + no effect. For consistency with line charts, the style for each series should + be set manually. diff --git a/doc/charts/secondary.png b/doc/charts/secondary.png new file mode 100644 index 0000000..41599c1 Binary files /dev/null and b/doc/charts/secondary.png differ diff --git a/doc/charts/secondary.py b/doc/charts/secondary.py new file mode 100644 index 0000000..2f13c48 --- /dev/null +++ b/doc/charts/secondary.py @@ -0,0 +1,43 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + LineChart, + BarChart, + Reference, + Series, +) + +wb = Workbook() +ws = wb.active + +rows = [ + ['Aliens', 2, 3, 4, 5, 6, 7], + ['Humans', 10, 40, 50, 20, 10, 50], +] + +for row in rows: + ws.append(row) + +c1 = BarChart() +v1 = Reference(ws, min_col=1, min_row=1, max_col=7) +c1.add_data(v1, titles_from_data=True, from_rows=True) + +c1.x_axis.title = 'Days' +c1.y_axis.title = 'Aliens' +c1.y_axis.majorGridlines = None +c1.title = 'Survey results' + + +# Create a second chart +c2 = LineChart() +v2 = Reference(ws, min_col=1, min_row=2, max_col=7) +c2.add_data(v2, titles_from_data=True, from_rows=True) +c2.y_axis.axId = 200 +c2.y_axis.title = "Humans" + +# Display y-axis of the second chart on the right by setting it to cross the x-axis at its maximum +c1.y_axis.crosses = "max" +c1 += c2 + +ws.add_chart(c1, "D4") + +wb.save("secondary.xlsx") diff --git a/doc/charts/secondary.rst b/doc/charts/secondary.rst new file mode 100644 index 0000000..fee96a3 --- /dev/null +++ b/doc/charts/secondary.rst @@ -0,0 +1,13 @@ +Adding a second axis +===================== + +Adding a second axis actually involves creating a second chart that shares a +common x-axis with the first chart but has a separate y-axis. + +.. literalinclude:: secondary.py + + +This produces a combined line and bar chart looking something like this: + +.. image:: secondary.png + :alt: "Sample chart with a second y-axis" diff --git a/doc/charts/stock.png b/doc/charts/stock.png new file mode 100644 index 0000000..0cb7c56 Binary files /dev/null and b/doc/charts/stock.png differ diff --git a/doc/charts/stock.py b/doc/charts/stock.py new file mode 100644 index 0000000..327b049 --- /dev/null +++ b/doc/charts/stock.py @@ -0,0 +1,102 @@ +from datetime import date + +from openpyxl import Workbook + +from openpyxl.chart import ( + BarChart, + StockChart, + Reference, + Series, +) +from openpyxl.chart.axis import DateAxis, ChartLines +from openpyxl.chart.updown_bars import UpDownBars + +wb = Workbook() +ws = wb.active + +rows = [ + ['Date', 'Volume','Open', 'High', 'Low', 'Close'], + ['2015-01-01', 20000, 26.2, 27.20, 23.49, 25.45, ], + ['2015-01-02', 10000, 25.45, 25.03, 19.55, 23.05, ], + ['2015-01-03', 15000, 23.05, 24.46, 20.03, 22.42, ], + ['2015-01-04', 2000, 22.42, 23.97, 20.07, 21.90, ], + ['2015-01-05', 12000, 21.9, 23.65, 19.50, 21.51, ], +] + +for row in rows: + ws.append(row) + +# High-low-close +c1 = StockChart() +labels = Reference(ws, min_col=1, min_row=2, max_row=6) +data = Reference(ws, min_col=4, max_col=6, min_row=1, max_row=6) +c1.add_data(data, titles_from_data=True) +c1.set_categories(labels) +for s in c1.series: + s.graphicalProperties.line.noFill = True +# marker for close +s.marker.symbol = "dot" +s.marker.size = 5 +c1.title = "High-low-close" +c1.hiLowLines = ChartLines() + +# Excel is broken and needs a cache of values in order to display hiLoLines :-/ +from openpyxl.chart.data_source import NumData, NumVal +pts = [NumVal(idx=i) for i in range(len(data) - 1)] +cache = NumData(pt=pts) +c1.series[-1].val.numRef.numCache = cache + +ws.add_chart(c1, "A10") + +# Open-high-low-close +c2 = StockChart() +data = Reference(ws, min_col=3, max_col=6, min_row=1, max_row=6) +c2.add_data(data, titles_from_data=True) +c2.set_categories(labels) +for s in c2.series: + s.graphicalProperties.line.noFill = True +c2.hiLowLines = ChartLines() +c2.upDownBars = UpDownBars() +c2.title = "Open-high-low-close" + +# add dummy cache +c2.series[-1].val.numRef.numCache = cache + +ws.add_chart(c2, "G10") + +# Create bar chart for volume + +bar = BarChart() +data = Reference(ws, min_col=2, min_row=1, max_row=6) +bar.add_data(data, titles_from_data=True) +bar.set_categories(labels) + +from copy import deepcopy + +# Volume-high-low-close +b1 = deepcopy(bar) +c3 = deepcopy(c1) +c3.y_axis.majorGridlines = None +c3.y_axis.title = "Price" +b1.y_axis.axId = 20 +b1.z_axis = c3.y_axis +b1.y_axis.crosses = "max" +b1 += c3 + +c3.title = "High low close volume" + +ws.add_chart(b1, "A27") + +## Volume-open-high-low-close +b2 = deepcopy(bar) +c4 = deepcopy(c2) +c4.y_axis.majorGridlines = None +c4.y_axis.title = "Price" +b2.y_axis.axId = 20 +b2.z_axis = c4.y_axis +b2.y_axis.crosses = "max" +b2 += c4 + +ws.add_chart(b2, "G27") + +wb.save("stock.xlsx") diff --git a/doc/charts/stock.rst b/doc/charts/stock.rst new file mode 100644 index 0000000..1e91b92 --- /dev/null +++ b/doc/charts/stock.rst @@ -0,0 +1,44 @@ +Stock Charts +============ + +Data that is arranged in columns or rows in a specific order on a worksheet +can be plotted in a stock chart. As its name implies, a stock chart is most +often used to illustrate the fluctuation of stock prices. However, this chart +may also be used for scientific data. For example, you could use a stock +chart to indicate the fluctuation of daily or annual temperatures. You must +organize your data in the correct order to create stock charts. + +The way stock chart data is organized in the worksheet is very important. For +example, to create a simple high-low-close stock chart, you should arrange +your data with High, Low, and Close entered as column headings, in that +order. + +Although stock charts are a distinct type, the various types are just +shortcuts for particular formatting options: + + * high-low-close is essentially a line chart with no lines and the marker + set to XYZ. It also sets hiLoLines to True + + * open-high-low-close is the as a high-low-close chart with the marker for + each data point set to XZZ and upDownLines. + + +Volume can be added by combining the stock chart with a bar chart for the volume. + + +.. literalinclude:: stock.py + + +.. warning:: + + Due to a bug in Excel high-low lines will only be shown if at least one of + the data series has some dummy values. This can be done with the following hack:: + + from openpyxl.chart.data_source import NumData, NumVal + pts = [NumVal(idx=i) for i in range(len(data) - 1)] + cache = NumData(pt=pts) + c1.series[-1].val.numRef.numCache = cache + + +.. image:: stock.png + :alt: "Sample stock charts" diff --git a/doc/charts/surface.png b/doc/charts/surface.png new file mode 100644 index 0000000..2aaba9a Binary files /dev/null and b/doc/charts/surface.png differ diff --git a/doc/charts/surface.py b/doc/charts/surface.py new file mode 100644 index 0000000..6aa144b --- /dev/null +++ b/doc/charts/surface.py @@ -0,0 +1,62 @@ +from openpyxl import Workbook +from openpyxl.chart import ( + SurfaceChart, + SurfaceChart3D, + Reference, + Series, +) +from openpyxl.chart.axis import SeriesAxis + +wb = Workbook() +ws = wb.active + +data = [ + [None, 10, 20, 30, 40, 50,], + [0.1, 15, 65, 105, 65, 15,], + [0.2, 35, 105, 170, 105, 35,], + [0.3, 55, 135, 215, 135, 55,], + [0.4, 75, 155, 240, 155, 75,], + [0.5, 80, 190, 245, 190, 80,], + [0.6, 75, 155, 240, 155, 75,], + [0.7, 55, 135, 215, 135, 55,], + [0.8, 35, 105, 170, 105, 35,], + [0.9, 15, 65, 105, 65, 15], +] + +for row in data: + ws.append(row) + + +c1 = SurfaceChart() +ref = Reference(ws, min_col=2, max_col=6, min_row=1, max_row=10) +labels = Reference(ws, min_col=1, min_row=2, max_row=10) +c1.add_data(ref, titles_from_data=True) +c1.set_categories(labels) +c1.title = "Contour" + +ws.add_chart(c1, "A12") + +from copy import deepcopy + +# wireframe +c2 = deepcopy(c1) +c2.wireframe = True +c2.title = "2D Wireframe" + +ws.add_chart(c2, "G12") + +# 3D Surface +c3 = SurfaceChart3D() +c3.add_data(ref, titles_from_data=True) +c3.set_categories(labels) +c3.title = "Surface" + +ws.add_chart(c3, "A29") + +c4 = deepcopy(c3) +c4.wireframe = True +c4.title = "3D Wireframe" + +ws.add_chart(c4, "G29") + +wb.save("surface.xlsx") diff --git a/doc/charts/surface.rst b/doc/charts/surface.rst new file mode 100644 index 0000000..b828869 --- /dev/null +++ b/doc/charts/surface.rst @@ -0,0 +1,18 @@ +Surface charts +============== + + +Data that is arranged in columns or rows on a worksheet can be plotted in a +surface chart. A surface chart is useful when you want to find optimum +combinations between two sets of data. As in a topographic map, colors and +patterns indicate areas that are in the same range of values. + +By default all surface charts are 3D. 2D wireframe and contour charts are +created by setting the rotation and perspective. + + +.. literalinclude:: surface.py + + +.. image:: surface.png + :alt: "Sample surface charts" diff --git a/doc/comments.rst b/doc/comments.rst new file mode 100644 index 0000000..7d918f7 --- /dev/null +++ b/doc/comments.rst @@ -0,0 +1,95 @@ +Comments +======== + +.. warning:: + + Openpyxl currently supports the reading and writing of comment text only. + Formatting information is lost. Comment dimensions are lost upon reading, + but can be written. Comments are not currently supported if + `read_only=True` is used. + + +Adding a comment to a cell +-------------------------- + +Comments have a text attribute and an author attribute, which must both be set + +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.comments import Comment +>>> wb = Workbook() +>>> ws = wb.active +>>> comment = ws["A1"].comment +>>> comment = Comment('This is the comment text', 'Comment Author') +>>> comment.text +'This is the comment text' +>>> comment.author +'Comment Author' + +If you assign the same comment to multiple cells then openpyxl will automatically create copies + +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.comments import Comment +>>> wb=Workbook() +>>> ws=wb.active +>>> comment = Comment("Text", "Author") +>>> ws["A1"].comment = comment +>>> ws["B2"].comment = comment +>>> ws["A1"].comment is comment +True +>>> ws["B2"].comment is comment +False + + +Loading and saving comments +---------------------------- + +Comments present in a workbook when loaded are stored in the comment +attribute of their respective cells automatically. Formatting information +such as font size, bold and italics are lost, as are the original dimensions +and position of the comment's container box. + +Comments remaining in a workbook when it is saved are automatically saved to +the workbook file. + +Comment dimensions can be specified for write-only. Comment dimension are +in pixels. + +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.comments import Comment +>>> from openpyxl.utils import units +>>> +>>> wb=Workbook() +>>> ws=wb.active +>>> +>>> comment = Comment("Text", "Author") +>>> comment.width = 300 +>>> comment.height = 50 +>>> +>>> ws["A1"].comment = comment +>>> +>>> wb.save('commented_book.xlsx') + + +If needed, ``openpyxl.utils.units`` contains helper functions for converting +from other measurements such as mm or points to pixels: + +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.comments import Comment +>>> from openpyxl.utils import units +>>> +>>> wb=Workbook() +>>> ws=wb.active +>>> +>>> comment = Comment("Text", "Author") +>>> comment.width = units.points_to_pixels(300) +>>> comment.height = units.points_to_pixels(50) +>>> +>>> ws["A1"].comment = comment diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..775ceb6 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +# +# openpyxl documentation build configuration file, created by +# sphinx-quickstart on Fri Sep 10 09:50:03 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +up = os.path.dirname +HERE = os.path.split(__file__)[0] +sys.path.insert(0, os.path.abspath(os.path.join(up(HERE), '.'))) + +import openpyxl + +def AliasProxyGet(self, instance, cls): + return getattr(cls, self.alias) + +from openpyxl.styles.numbers import NumberFormatDescriptor + +def NumberFormatGet(self, instance, cls): + return self + +from openpyxl.styles.styleable import StyleDescriptor + +def StyleDescriptorGet(self, instance, cls): + return self.key + +if os.environ.get("APIDOC") == "True": + from openpyxl.descriptors import Alias + Alias.__get__ = AliasProxyGet + NumberFormatDescriptor.__get__ = NumberFormatGet + + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', 'sphinx.ext.doctest', 'sphinx.ext.coverage'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'openpyxl' +from datetime import date + +copyright = u'2010 - {0}, {1}'.format(date.today().year, openpyxl.__author__) + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = openpyxl.__version__ +# The short X.Y version. +version = ".".join(release.split(".")[:-1]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if on_rtd: + html_theme = 'default' +else: + html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = 'logo.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'openpyxldoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'openpyxl.tex', u'openpyxl Documentation', + openpyxl.__author__, 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'openpyxl', u'openpyxl Documentation', + [openpyxl.__author__], 1) +] + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} + +doctest_global_setup = """ +import os, shutil +if not os.path.exists("tmp"): + os.mkdir("tmp") +shutil.copy("logo.png", "tmp") +os.chdir("tmp") +""" +doctest_global_cleanup = """ +import shutil +import os +os.chdir("..") +shutil.rmtree("tmp") +""" + +# Invoke Sphinx apidoc to generate api rst files + +def run_apidoc(_): + try: + from sphinx.ext.apidoc import main + except ImportError: + from sphinx.apidoc import main + + cur_dir = os.path.abspath(os.path.dirname(__file__)) + output_path = os.path.join(cur_dir, 'api') + modules = os.path.dirname(openpyxl.__file__) + exclusions = [ + '../openpyxl/cell/tests', + '../openpyxl/chart/tests', + '../openpyxl/chartsheet/tests', + '../openpyxl/comments/tests', + '../openpyxl/compat', + '../openpyxl/descriptors/tests', + '../openpyxl/descriptors/slots.py', + '../openpyxl/develop/', + '../openpyxl/drawing/tests', + '../openpyxl/formula/', + '../openpyxl/formatting/tests/', + '../openpyxl/worksheet/tests', + '../openpyxl/writer/tests/', + '../openpyxl/xml/tests', + '../openpyxl/conftest.py', + '../openpyxl/packaging/tests', + '../openpyxl/pivot/tests', + '../openpyxl/reader/tests', + '../openpyxl/styles/tests', + '../openpyxl/tests', + '../openpyxl/utils/tests', + '../openpyxl/utils/formulas.py', + '../openpyxl/workbook/tests', + '../openpyxl/workbook/external_link/tests', + '../openpyxl/writer/tests', + '../openpyxl/xml/tests', + ] + main(['-f', '-T', '-e', '-M', '-o', output_path, modules] + exclusions) + +def setup(app): + app.connect('builder-inited', run_apidoc) diff --git a/doc/defined_names.rst b/doc/defined_names.rst new file mode 100644 index 0000000..45abcdb --- /dev/null +++ b/doc/defined_names.rst @@ -0,0 +1,27 @@ +Defined Names +============= + + +The specification has the following to say about defined names: + + "Defined names are descriptive text that is used to represents a cell, range + of cells, formula, or constant value." + +This means they are very loosely defined. They might contain a constant, a +formula, a single cell reference, a range of cells or multiple ranges of +cells across different worksheets. Or all of the above. They are defined +globally for a workbook and accessed from there `defined_names` attribue. + +Sample use for ranges +--------------------- + +Accessing a range called "my_range":: + + my_range = wb.defined_names['my_range'] + # if this contains a range of cells then the destinations attribute is not None + dests = my_range.destinations # returns a generator of (worksheet title, cell range) tuples + + cells = [] + for title, coord in dests: + ws = wb[title] + cells.append(ws[coord]) diff --git a/doc/development.rst b/doc/development.rst new file mode 100644 index 0000000..643eb73 --- /dev/null +++ b/doc/development.rst @@ -0,0 +1,199 @@ +Development +=========== + +With the ongoing development of openpyxl, there is occasional information +useful to assist developers. + + +What is suppoprted +------------------ + +The primary aim of openpyxl is to support reading and writing Microsoft Excel +2010 files. Where possible support for files generated by other libraries or +programs is available but this is not guaranteed. + + +Supporting different Python versions +------------------------------------ + +We have a small library of utility functions to support development for +Python 2 and 3. This is openpyxl.compat for Python and openpyxl.xml for XML +functions. + + +Coding style +------------ + +Use PEP-8 except when implementing attributes for roundtripping but always +use Python data conventions (boolean, None, etc.) Note exceptions in +docstrings. + + +Getting the source +------------------ + +The source code is hosted on bitbucket.org. You can get it using a Mercurial +client and the following URL. + +.. parsed-literal:: + + $ hg clone \https://bitbucket.org/openpyxl/openpyxl + $ virtualenv openpyxl + $ hg up |version| + $ cd openpyxl + $ source bin/activate + $ pip install -U -r requirements.txt + $ python setup.py develop + + +Specification +------------- + +The file specification for OOXML was released jointly as `ECMA 376 +`_ and +`ISO 29500 `_. + +Testing +------- + +Contributions without tests will **not** be accepted. + +We use pytest as the test runner with pytest-cov for coverage information and +pytest-flakes for static code analysis. + + +Coverage +++++++++ + +The goal is 100 % coverage for unit tests - data types and utility functions. +Coverage information can be obtained using + + :code:`py.test --cov openpyxl` + + +Organisation +++++++++++++ + +Tests should be preferably at package / module level e.g openpyxl/cell. This +makes testing and getting statistics for code under development easier: + + :code:`py.test --cov openpyxl/cell openpyxl/cell` + + +Checking XML +++++++++++++ + +Use the :code:`openpyxl.tests.helper.compare_xml` function to compare +generated and expected fragments of XML. + + +Schema validation ++++++++++++++++++ + +When working on code to generate XML it is possible to validate that the +generated XML conforms to the published specification. Note, this won't +necessarily guarantee that everything is fine but is preferable to reverse +engineering! + + +Microsoft Tools ++++++++++++++++ + +Along with the SDK, Microsoft also has a `"Productivity Tool" +`_ for working +with Office OpenXML. + +This allows you to quickly inspect or compare whole Excel files. +Unfortunately, validation errors contain many false positives. The tool also +contain links to the specification and implementers' notes. + +Please see :doc:`windows-development` for additional information on setting up and testing on Windows. + + +Contributing +------------ + +Contributions in the form of pull requests are always welcome. Don't forget +to add yourself to the list of authors! + + +Branch naming convention +------------------------ + +We use a "major.minor.patch" numbering system, ie. |release|. Development +branches are named after "major.minor" releases. In general, API change will +only happen major releases but there will be exceptions. Always communicate +API changes to the mailing list before making them. If you are changing an +API try and an implement a fallback (with deprecation warning) for the old +behaviour. + +The "default branch" is used for releases and always has changes from a +development branch merged in. It should never be the target for a pull +request. + + +Pull Requests +------------- + +Pull requests should be submitted to the current, unreleased development +branch. Eg. if the current release is |release|, pull requests should be made +to the |version| branch. Exceptions are bug fixes to released versions which +should be made to the relevant release branch and merged upstream into +development. + +Please use tox to test code for different submissions **before** making a +pull request. This is especially important for picking up problems across +Python versions. + + +Documentation ++++++++++++++ + +Remember to update the documentation when adding or changing features. Check +that documentation is syntactically correct. + +:code:`tox -e doc` + + +Benchmarking +------------ + +Benchmarking and profiling are ongoing tasks. Contributions to these are very +welcome as we know there is a lot to do. + + +Memory Use +++++++++++ + +There is a tox profile for long-running memory benchmarks using the +`memory_utils` package. + +:code:`tox -e memory` + + +Pympler ++++++++ + +As openpyxl does not include any internal memory benchmarking tools, the +python *pympler* package was used during the testing of styles to profile the +memory usage in :code:`openpyxl.reader.excel.read_style_table()`:: + + # in openpyxl/reader/style.py + from pympler import muppy, summary + + def read_style_table(xml_source): + ... + if cell_xfs is not None: # ~ line 47 + initialState = summary.summarize(muppy.get_objects()) # Capture the initial state + for index, cell_xfs_node in enumerate(cell_xfs_nodes): + ... + table[index] = new_style + finalState = summary.summarize(muppy.get_objects()) # Capture the final state + diff = summary.get_diff(initialState, finalState) # Compare + summary.print_(diff) + + +:code:`pympler.summary.print_()` prints to the console a report of object +memory usage, allowing the comparison of different methods and examination of +memory usage. A useful future development would be to construct a +benchmarking package to measure the performance of different components. diff --git a/doc/example.py b/doc/example.py new file mode 100644 index 0000000..f98525c --- /dev/null +++ b/doc/example.py @@ -0,0 +1,18 @@ +from openpyxl import Workbook +wb = Workbook() + +# grab the active worksheet +ws = wb.active + +# Data can be assigned directly to cells +ws['A1'] = 42 + +# Rows can also be appended +ws.append([1, 2, 3]) + +# Python types will automatically be converted +import datetime +ws['A2'] = datetime.datetime.now() + +# Save the file +wb.save("sample.xlsx") diff --git a/doc/filters.png b/doc/filters.png new file mode 100644 index 0000000..153dfed Binary files /dev/null and b/doc/filters.png differ diff --git a/doc/filters.py b/doc/filters.py new file mode 100644 index 0000000..4691f10 --- /dev/null +++ b/doc/filters.py @@ -0,0 +1,31 @@ +from openpyxl import Workbook + +wb = Workbook() +ws = wb.active + +data = [ + ["Fruit", "Quantity"], + ["Kiwi", 3], + ["Grape", 15], + ["Apple", 3], + ["Peach", 3], + ["Pomegranate", 3], + ["Pear", 3], + ["Tangerine", 3], + ["Blueberry", 3], + ["Mango", 3], + ["Watermelon", 3], + ["Blackberry", 3], + ["Orange", 3], + ["Raspberry", 3], + ["Banana", 3] +] + +for r in data: + ws.append(r) + +ws.auto_filter.ref = "A1:B15" +ws.auto_filter.add_filter_column(0, ["Kiwi", "Apple", "Mango"]) +ws.auto_filter.add_sort_condition("B2:B15") + +wb.save("filtered.xlsx") diff --git a/doc/filters.rst b/doc/filters.rst new file mode 100644 index 0000000..0ec0949 --- /dev/null +++ b/doc/filters.rst @@ -0,0 +1,19 @@ +Using filters and sorts +======================= + + +It's possible to add a filter to a worksheet. + +.. note:: + + Filters and sorts can only be configured by openpyxl but will need to be applied in applications like Excel. This is because they actually rearranges or format cells or rows in the range. + +To add a filter you define a range and then add columns and sort conditions: + +.. literalinclude:: filters.py + + +This will add the relevant instructions to the file but will **neither actually filter nor sort**. + +.. image:: filters.png + :alt: "Filter and sort prepared but not executed for a range of cells" diff --git a/doc/format_merged_cells.py b/doc/format_merged_cells.py new file mode 100644 index 0000000..fa4c135 --- /dev/null +++ b/doc/format_merged_cells.py @@ -0,0 +1,59 @@ +from openpyxl.styles import Border, Side, PatternFill, Font, GradientFill, Alignment +from openpyxl import Workbook + + +def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment=None): + """ + Apply styles to a range of cells as if they were a single cell. + + :param ws: Excel worksheet instance + :param range: An excel range to style (e.g. A1:F20) + :param border: An openpyxl Border + :param fill: An openpyxl PatternFill or GradientFill + :param font: An openpyxl Font object + """ + + top = Border(top=border.top) + left = Border(left=border.left) + right = Border(right=border.right) + bottom = Border(bottom=border.bottom) + + first_cell = ws[cell_range.split(":")[0]] + if alignment: + ws.merge_cells(cell_range) + first_cell.alignment = alignment + + rows = ws[cell_range] + if font: + first_cell.font = font + + for cell in rows[0]: + cell.border = cell.border + top + for cell in rows[-1]: + cell.border = cell.border + bottom + + for row in rows: + l = row[0] + r = row[-1] + l.border = l.border + left + r.border = r.border + right + if fill: + for c in row: + c.fill = fill + +wb = Workbook() +ws = wb.active +my_cell = ws['B2'] +my_cell.value = "My Cell" +thin = Side(border_style="thin", color="000000") +double = Side(border_style="double", color="ff0000") + +border = Border(top=double, left=thin, right=thin, bottom=double) +fill = PatternFill("solid", fgColor="DDDDDD") +fill = GradientFill(stop=("000000", "FFFFFF")) +font = Font(b=True, color="FF0000") +al = Alignment(horizontal="center", vertical="center") + + +style_range(ws, 'B2:F4', border=border, fill=fill, font=font, alignment=al) +wb.save("styled.xlsx") diff --git a/doc/formatting.rst b/doc/formatting.rst new file mode 100644 index 0000000..e44b2fd --- /dev/null +++ b/doc/formatting.rst @@ -0,0 +1,189 @@ +Conditional Formatting +====================== + +Excel supports three different types of conditional formatting: builtins, standard and custom. Builtins combine specific rules with predefined styles. Standard conditional formats combine specific rules with custom formatting. In additional it is possible to define custom formulae for applying custom formats using differential styles. + +.. note:: + + The syntax for the different rules varies so much that it is not + possible for openpyxl to know whether a rule makes sense or not. + + +The basic syntax for creating a formatting rule is: + +.. doctest + +>>> from openpyxl.formatting import Rule +>>> from openpyxl.styles import Font, PatternFill, Border +>>> from openpyxl.styles.differential import DifferentialStyle +>>> dxf = DifferentialStyle(font=Font(bold=True), fill=PatternFill(start_color='EE1111', end_color='EE1111')) +>>> rule = Rule(type='cellIs', dxf=dxf, formula=["10"]) + +Because the signatures for some rules can be quite verbose there are also some convenience factories for creating them. + +Builtin formats +--------------- + +The builtins conditional formats are: + + * ColorScale + * IconSet + * DataBar + +Builtin formats contain a sequence of formatting settings which combine a type with an integer for comparison. Possible types are: `'num', 'percent', 'max', 'min', 'formula', 'percentile'`. + + +ColorScale +++++++++++ + +You can have color scales with 2 or 3 colors. 2 color scales produce a gradient from one color to another; 3 color scales use an additional color for 2 gradients. + +The full syntax for creating a ColorScale rule is: + +.. doctest + +>>> from openpyxl.formatting.rule import ColorScale, FormatObject +>>> from openpyxl.styles import Color +>>> first = FormatObject(type='min') +>>> last = FormatObject(type='max') +>>> # colors match the format objects: +>>> colors = [Color('AA0000'), Color('00AA00')] +>>> cs2 = ColorScale(cfvo=[first, last], color=colors) +>>> # a three color scale would extend the sequences +>>> mid = FormatObject(type='num', val=40) +>>> colors.insert(1, Color('00AA00')) +>>> cs3 = ColorScale(cfvo=[first, mid, last], color=colors) +>>> # create a rule with the color scale +>>> from openpyxl.formatting.rule import Rule +>>> rule = Rule(type='colorScale', colorScale=cs3) + +There is a convenience function for creating ColorScale rules + +.. doctest + +>>> from openpyxl.formatting.rule import ColorScaleRule +>>> rule = ColorScaleRule(start_type='percentile', start_value=10, start_color='FFAA0000', +... mid_type='percentile', mid_value=50, mid_color='FF0000AA', +... end_type='percentile', end_value=90, end_color='FF00AA00') + + +IconSet ++++++++ + +Choose from the following set of icons: `'3Arrows', '3ArrowsGray', '3Flags', '3TrafficLights1', '3TrafficLights2', '3Signs', '3Symbols', '3Symbols2', '4Arrows', '4ArrowsGray', '4RedToBlack', '4Rating', '4TrafficLights', '5Arrows', '5ArrowsGray', '5Rating', '5Quarters'` + +The full syntax for creating an IconSet rule is: + +.. doctest + +>>> from openpyxl.formatting.rule import IconSet, FormatObject +>>> first = FormatObject(type='percent', val=0) +>>> second = FormatObject(type='percent', val=33) +>>> third = FormatObject(type='percent', val=67) +>>> iconset = IconSet(iconSet='3TrafficLights1', cfvo=[first, second, third], showValue=None, percent=None, reverse=None) +>>> # assign the icon set to a rule +>>> from openpyxl.formatting.rule import Rule +>>> rule = Rule(type='iconSet', iconSet=iconset) + +There is a convenience function for creating IconSet rules: + +.. doctest + +>>> from openpyxl.formatting.rule import IconSetRule +>>> rule = IconSetRule('5Arrows', 'percent', [10, 20, 30, 40, 50], showValue=None, percent=None, reverse=None) + + +DataBar ++++++++ + +Currently, openpyxl supports the DataBars as defined in the original specification. Borders and directions were added in a later extension. + +The full syntax for creating a DataBar rule is: + +.. doctest + +>>> from openpyxl.formatting.rule import DataBar, FormatObject +>>> first = FormatObject(type='min') +>>> second = FormatObject(type='max') +>>> data_bar = DataBar(cfvo=[first, second], color="638EC6", showValue=None, minLength=None, maxLength=None) +>>> # assign the data bar to a rule +>>> from openpyxl.formatting.rule import Rule +>>> rule = Rule(type='dataBar', dataBar=data_bar) + +There is a convenience function for creating DataBar rules: + +.. doctest + +>>> from openpyxl.formatting.rule import DataBarRule +>>> rule = DataBarRule(start_type='percentile', start_value=10, end_type='percentile', end_value='90', +... color="FF638EC6", showValue="None", minLength=None, maxLength=None) + + +Standard conditional formats +---------------------------- + +The standard conditional formats are: + + * Average + * Percent + * Unique or duplicate + * Value + * Rank + +.. doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.styles import Color, PatternFill, Font, Border +>>> from openpyxl.styles.differential import DifferentialStyle +>>> from openpyxl.formatting.rule import ColorScaleRule, CellIsRule, FormulaRule +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> # Create fill +>>> redFill = PatternFill(start_color='EE1111', +... end_color='EE1111', +... fill_type='solid') +>>> +>>> # Add a two-color scale +>>> # Takes colors in excel 'RRGGBB' style. +>>> ws.conditional_formatting.add('A1:A10', +... ColorScaleRule(start_type='min', start_color='AA0000', +... end_type='max', end_color='00AA00') +... ) +>>> +>>> # Add a three-color scale +>>> ws.conditional_formatting.add('B1:B10', +... ColorScaleRule(start_type='percentile', start_value=10, start_color='AA0000', +... mid_type='percentile', mid_value=50, mid_color='0000AA', +... end_type='percentile', end_value=90, end_color='00AA00') +... ) +>>> +>>> # Add a conditional formatting based on a cell comparison +>>> # addCellIs(range_string, operator, formula, stopIfTrue, wb, font, border, fill) +>>> # Format if cell is less than 'formula' +>>> ws.conditional_formatting.add('C2:C10', +... CellIsRule(operator='lessThan', formula=['C$1'], stopIfTrue=True, fill=redFill)) +>>> +>>> # Format if cell is between 'formula' +>>> ws.conditional_formatting.add('D2:D10', +... CellIsRule(operator='between', formula=['1','5'], stopIfTrue=True, fill=redFill)) +>>> +>>> # Format using a formula +>>> ws.conditional_formatting.add('E1:E10', +... FormulaRule(formula=['ISBLANK(E1)'], stopIfTrue=True, fill=redFill)) +>>> +>>> # Aside from the 2-color and 3-color scales, format rules take fonts, borders and fills for styling: +>>> myFont = Font() +>>> myBorder = Border() +>>> ws.conditional_formatting.add('E1:E10', +... FormulaRule(formula=['E1=0'], font=myFont, border=myBorder, fill=redFill)) +>>> +>>> # Highlight cells that contain particular text by using a special formula +>>> red_text = Font(color="9C0006") +>>> red_fill = PatternFill(bgColor="FFC7CE") +>>> dxf = DifferentialStyle(font=red_text, fill=red_fill) +>>> rule = Rule(type="containsText", operator="containsText", text="highlight", dxf=dxf) +>>> rule.formula = ['NOT(ISERROR(SEARCH("highlight",A1)))'] +>>> ws.conditional_formatting.add('A1:F40', rule) +>>> wb.save("test.xlsx") diff --git a/doc/formula.rst b/doc/formula.rst new file mode 100644 index 0000000..e52a12d --- /dev/null +++ b/doc/formula.rst @@ -0,0 +1,91 @@ +Parsing Formulas +================ + +`openpyxl` supports limited parsing of formulas embedded in cells. The +`openpyxl.formula` package contains a `Tokenizer` class to break +formulas into their consitutuent tokens. Usage is as follows: + +.. doctest + +>>> from openpyxl.formula import Tokenizer +>>> tok = Tokenizer("""=IF($A$1,"then True",MAX(DEFAULT_VAL,'Sheet 2'!B1))""") +>>> print("\n".join("%12s%11s%9s" % (t.value, t.type, t.subtype) for t in tok.items)) + IF( FUNC OPEN + $A$1 OPERAND RANGE + , SEP ARG + "then True" OPERAND TEXT + , SEP ARG + MAX( FUNC OPEN + DEFAULT_VAL OPERAND RANGE + , SEP ARG +'Sheet 2'!B1 OPERAND RANGE + ) FUNC CLOSE + ) FUNC CLOSE + +As shown above, tokens have three attributes of interest: + +* ``.value``: The substring of the formula that produced this token + +* ``.type``: The type of token this represents. Can be one of + + - ``Token.LITERAL``: If the cell does not contain a formula, its + value is represented by a single ``LITERAL`` token. + + - ``Token.OPERAND``: A generic term for any value in the Excel + formula. (See ``.subtype`` below for more details). + + - ``Token.FUNC``: Function calls are broken up into tokens for the + opener (e.g., ``SUM(``), followed by the arguments, followed by + the closer (i.e., ``)``). The function name and opening + parenthesis together form one ``FUNC`` token, and the matching + parenthesis forms another ``FUNC`` token. + + - ``Token.ARRAY``: Array literals (enclosed between curly braces) + get two ``ARRAY`` tokens each, one for the opening ``{`` and one + for the closing ``}``. + + - ``Token.PAREN``: When used for grouping subexpressions (and not to + denote function calls), parentheses are tokenized as ``PAREN`` + tokens (one per character). + + - ``Token.SEP``: These tokens are created from either commas (``,``) + or semicolons (``;``). Commas create ``SEP`` tokens when they are + used to separate function arguments (e.g., ``SUM(a,b)``) or when + they are used to separate array elements (e.g., ``{a,b}``). (They + have another use as an infix operator for joining + ranges). Semicolons are always used to separate rows in an array + literal, so always create ``SEP`` tokens. + + - ``Token.OP_PRE``: Designates a prefix unary operator. Its value is + always ``+`` or ``-`` + + - ``Token.OP_IN``: Designates an infix binary operator. Possible + values are ``>=``, ``<=``, ``<>``, ``=``, ``>``, ``<``, ``*``, + ``/``, ``+``, ``-``, ``^``, or ``&``. + + - ``Token.OP_POST``: Designates a postfix unary operator. Its value + is always ``%``. + + - ``Token.WSPACE``: Created for any whitespace encountered. Its + value is always a single space, regardless of how much whitespace + is found. + +* ``.subtype``: Some of the token types above use the subtype to + provide additional information about the token. Possible subtypes + are: + + + ``Token.TEXT``, ``Token.NUMBER``, ``Token.LOGICAL``, + ``Token.ERROR``, ``Token.RANGE``: these subtypes describe the + various forms of ``OPERAND`` found in formulae. ``LOGICAL`` is + either ``TRUE`` or ``FALSE``, ``RANGE`` is either a named range or + a direct reference to another range. ``TEXT``, ``NUMBER``, and + ``ERROR`` all refer to literal values in the formula + + + ``Token.OPEN`` and ``Token.CLOSE``: these two subtypes are used by + ``PAREN``, ``FUNC``, and ``ARRAY``, to describe whether the token + is opening a new subexpression or closing it. + + + ``Token.ARG`` and ``Token.ROW``: are used by the ``SEP`` tokens, + to distinguish between the comma and semicolon. Commas produce + tokens of subtype ``ARG`` whereas semicolons produce tokens of + subtype ``ROW`` diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..3ae0f81 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,317 @@ +:mod:`openpyxl` - A Python library to read/write Excel 2010 xlsx/xlsm files +=========================================================================== + +.. module:: openpyxl +.. moduleauthor:: Eric Gazoni, Charlie Clark + +:Author: Eric Gazoni, Charlie Clark +:Source code: http://bitbucket.org/openpyxl/openpyxl/src +:Issues: http://bitbucket.org/openpyxl/openpyxl/issues +:Generated: |today| +:License: MIT/Expat +:Version: |release| + + +Introduction +------------ + +Openpyxl is a Python library for reading and writing Excel 2010 +xlsx/xlsm/xltx/xltm files. + +It was born from lack of existing library to read/write natively from Python +the Office Open XML format. + +All kudos to the PHPExcel team as openpyxl was initially based on `PHPExcel +`_. + + +Support ++++++++ + +This is an open source project, maintained by volunteers in their spare time. +This may well mean that particular features or functions that you would like +are missing. But things don't have to stay that way. You can contribute the +project :doc:`development` yourself or contract a developer for particular +features. + + +Professional support for openpyxl is available from +`Clark Consulting & Research `_ and +`Adimian `_. Donations to the project to support further +development and maintenance are welcome. + + +Bug reports and feature requests should be submitted using the `issue tracker +`_. Please provide a full +traceback of any error you see and if possible a sample file. If for reasons +of confidentiality you are unable to make a file publicly available then +contact of one the developers. + + +Sample code: +++++++++++++ + +.. literalinclude:: example.py + + +User List +--------- + +Official user list can be found on http://groups.google.com/group/openpyxl-users + + +How to Contribute Code +---------------------- + +Any help will be greatly appreciated, just follow those steps: + + 1. + Please start a new fork (https://bitbucket.org/openpyxl/openpyxl/fork) + for each independent feature, don't try to fix all problems at the same + time, it's easier for those who will review and merge your changes ;-) + + 2. + Hack hack hack + + 3. + Don't forget to add unit tests for your changes! (YES, even if it's a + one-liner, changes without tests will **not** be accepted.) There are plenty + of examples in the source if you lack know-how or inspiration. + + 4. + If you added a whole new feature, or just improved something, you can + be proud of it, so add yourself to the AUTHORS file :-) + + 5. + Let people know about the shiny thing you just implemented, update the + docs! + + 6. + When it's done, just issue a pull request (click on the large "pull + request" button on *your* repository) and wait for your code to be + reviewed, and, if you followed all theses steps, merged into the main + repository. + + +For further information see :doc:`development` + + +Other ways to help +------------------ + +There are several ways to contribute, even if you can't code (or can't code well): + + * triaging bugs on the bug tracker: closing bugs that have already been + closed, are not relevant, cannot be reproduced, ... + + * updating documentation in virtually every area: many large features have + been added (mainly about charts and images at the moment) but without any + documentation, it's pretty hard to do anything with it + + * proposing compatibility fixes for different versions of Python: we support + 2.7 to 3.5, so if it does not work on your environment, let us know :-) + + +Installation +------------ + +Install openpyxl using pip. It is advisable to do this in a Python virtualenv +without system packages:: + + $ pip install openpyxl + +.. note:: + + There is support for the popular `lxml`_ library which will be used if it + is installed. This is particular useful when creating large files. + +.. _lxml: http://lxml.de + +.. warning:: + + To be able to include images (jpeg, png, bmp,...) into an openpyxl file, + you will also need the "pillow" library that can be installed with:: + + $ pip install pillow + + or browse https://pypi.python.org/pypi/Pillow/, pick the latest version + and head to the bottom of the page for Windows binaries. + + +Working with a checkout +----------------------- + +Sometimes you might want to work with the checkout of a particular version. +This may be the case if bugs have been fixed but a release has not yet been +made. + +.. parsed-literal:: + $ pip install -e hg+https://bitbucket.org/openpyxl/openpyxl@\ |version|\ #egg=openpyxl + + +Usage examples +-------------- + +Tutorial +++++++++ + +.. toctree:: + + tutorial + +Cookbook +++++++++ + +.. toctree:: + + usage + + +Pandas and NumPy +++++++++++++++++ + +.. toctree:: + + pandas + + +Charts +++++++ + +.. toctree:: + + charts/introduction + + +Comments +++++++++ + +.. toctree:: + + comments + + +Read/write large files +++++++++++++++++++++++ + +.. toctree:: + + optimized + + +Working with styles ++++++++++++++++++++ + +.. toctree:: + + styles + worksheet_properties + + +Conditional Formatting +++++++++++++++++++++++ + +.. toctree:: + + formatting + + +Print Settings +++++++++++++++++++++++ + +.. toctree:: + + print_settings + + +Filtering and Sorting ++++++++++++++++++++++ + +.. toctree:: + + filters + + +Worksheet Tables +++++++++++++++++ + +.. toctree:: + + worksheet_tables.rst + + +Data Validation ++++++++++++++++ + +.. toctree:: + + validation + + +Defined Names & Ranges +++++++++++++++++++++++ + +.. toctree:: + + defined_names + + +Parsing Formulas +++++++++++++++++ + +.. toctree:: + + formula + + +Protection +++++++++++ + +.. toctree:: + + protection + + +Information for Developers +-------------------------- + +.. toctree:: + + development + windows-development + + +API Documentation +------------------ + +Key Classes ++++++++++++ + +* :class:`openpyxl.workbook.workbook.Workbook` +* :class:`openpyxl.worksheet.worksheet.Worksheet` +* :class:`openpyxl.cell.cell.Cell` + +Full API +++++++++ + +.. toctree:: + :maxdepth: 2 + + api/openpyxl + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + +Release Notes +============= + +.. toctree:: + :maxdepth: 1 + + changes diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000..e2998ba Binary files /dev/null and b/doc/logo.png differ diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..47406bc --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\openpyxl.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\openpyxl.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/doc/optimized.rst b/doc/optimized.rst new file mode 100644 index 0000000..5f5087c --- /dev/null +++ b/doc/optimized.rst @@ -0,0 +1,103 @@ +Read-only mode +============== + +Sometimes, you will need to open or write extremely large XLSX files, +and the common routines in openpyxl won't be able to handle that load. +Fortunately, there are two modes that enable you to read and write unlimited +amounts of data with (near) constant memory consumption. + +Introducing :class:`openpyxl.worksheet.read_only.ReadOnlyWorksheet`:: + + from openpyxl import load_workbook + wb = load_workbook(filename='large_file.xlsx', read_only=True) + ws = wb['big_data'] + + for row in ws.rows: + for cell in row: + print(cell.value) + +.. warning:: + + * :class:`openpyxl.worksheet.read_only.ReadOnlyWorksheet` is read-only + +Cells returned are not regular :class:`openpyxl.cell.cell.Cell` but +:class:`openpyxl.cell.read_only.ReadOnlyCell`. + + +Worksheet dimensions +-------------------- + +Read-only mode relies on applications and libraries that created the file +providing correct information about the worksheets, specifically the used +part of it, known as the dimensions. Some applications set this incorrectly. +You can check the apparent dimensions of a worksheet using +`ws.calculate_dimension()`. If this returns a range that you know is +incorrect, say `A1:A1` then simply resetting the max_row and max_column +attributes should allow you to work with the file:: + + ws.max_row = ws.max_column = None + + +Write-only mode +=============== + +Here again, the regular :class:`openpyxl.worksheet.worksheet.Worksheet` has been replaced +by a faster alternative, the :class:`openpyxl.writer.write_only.WriteOnlyWorksheet`. +When you want to dump large amounts of data make sure you have `lxml` installed. + +.. :: doctest + +>>> from openpyxl import Workbook +>>> wb = Workbook(write_only=True) +>>> ws = wb.create_sheet() +>>> +>>> # now we'll fill it with 100 rows x 200 columns +>>> +>>> for irow in range(100): +... ws.append(['%d' % i for i in range(200)]) +>>> # save the file +>>> wb.save('new_big_file.xlsx') # doctest: +SKIP + +If you want to have cells with styles or comments then use a :func:`openpyxl.worksheet.write_only.WriteOnlyCell` + +.. :: doctest + +>>> from openpyxl import Workbook +>>> wb = Workbook(write_only = True) +>>> ws = wb.create_sheet() +>>> from openpyxl.worksheet.write_only import WriteOnlyCell +>>> from openpyxl.comments import Comment +>>> from openpyxl.styles import Font +>>> cell = WriteOnlyCell(ws, value="hello world") +>>> cell.font = Font(name='Courier', size=36) +>>> cell.comment = Comment(text="A comment", author="Author's Name") +>>> ws.append([cell, 3.14, None]) +>>> wb.save('write_only_file.xlsx') + + +This will create a write-only workbook with a single sheet, and append +a row of 3 cells: one text cell with a custom font and a comment, a +floating-point number, and an empty cell (which will be discarded +anyway). + +.. warning:: + + * Unlike a normal workbook, a newly-created write-only workbook + does not contain any worksheets; a worksheet must be specifically + created with the :func:`create_sheet()` method. + + * In a write-only workbook, rows can only be added with + :func:`append()`. It is not possible to write (or read) cells at + arbitrary locations with :func:`cell()` or :func:`iter_rows()`. + + * It is able to export unlimited amount of data (even more than Excel can + handle actually), while keeping memory usage under 10Mb. + + * A write-only workbook can only be saved once. After + that, every attempt to save the workbook or append() to an existing + worksheet will raise an :class:`openpyxl.utils.exceptions.WorkbookAlreadySaved` + exception. + + * Everything that appears in the file before the actual cell data must be created + before cells are added because it must written to the file before then. + For example, `freeze_panes` should be set before cells are added. diff --git a/doc/pandas.rst b/doc/pandas.rst new file mode 100644 index 0000000..9897b3e --- /dev/null +++ b/doc/pandas.rst @@ -0,0 +1,93 @@ +Working with Pandas and NumPy +============================= + +openpyxl is able to work with the popular libraries `Pandas +`_ and `NumPy `_ + + +NumPy Support +------------- + +openpyxl has builtin support for the NumPy types float, integer and boolean. +DateTimes are supported using the Pandas' Timestamp type. + + +Working with Pandas Dataframes +------------------------------ + +The :func:`openpyxl.utils.dataframe.dataframe_to_rows` function provides a +simple way to work with Pandas Dataframes:: + + from openpyxl.utils.dataframe import dataframe_to_rows + wb = Workbook() + ws = wb.active + + for r in dataframe_to_rows(df, index=True, header=True): + ws.append(r) + + +While Pandas itself supports conversion to Excel, this gives client code +additional flexibility including the ability to stream dataframes straight to +files. + +To convert a dataframe into a worksheet highlighting the header and index:: + + wb = Workbook() + ws = wb.active + + for r in dataframe_to_rows(df, index=True, header=True): + ws.append(r) + + for cell in ws['A'] + ws[1]: + cell.style = 'Pandas' + + wb.save("pandas_openpyxl.xlsx") + +Alternatively, if you just want to convert the data you can use write-only mode:: + + from openpyxl.cell.cell import WriteOnlyCell + wb = Workbook(write_only=True) + ws = wb.create_sheet() + + cell = WriteOnlyCell(ws) + cell.style = 'Pandas' + + def format_first_row(row, cell): + + for c in row: + cell.value = c + yield cell + + rows = dataframe_to_rows(df) + first_row = format_first_row(next(rows), cell) + ws.append(first_row) + + for row in rows: + row = list(row) + cell.value = row[0] + row[0] = cell + ws.append(row) + + wb.save("openpyxl_stream.xlsx") + + +This code will work just as well with a standard workbook. + + +Converting a worksheet to a Dataframe +------------------------------------- + +To convert a worksheet to a Dataframe you can use the `values` property. This +is very easy if the worksheet has no headers or indices:: + + df = DataFrame(ws.values) + +If the worksheet does have headers or indices, such as one created by Pandas, +then a little more work is required:: + + data = ws.values + cols = next(data)[1:] + data = list(data) + idx = [r[0] for r in data] + data = (islice(r, 1, None) for r in data) + df = DataFrame(data, index=idx, columns=cols) diff --git a/doc/print_settings.rst b/doc/print_settings.rst new file mode 100644 index 0000000..668d06b --- /dev/null +++ b/doc/print_settings.rst @@ -0,0 +1,76 @@ +Print Settings +============== + +openpyxl provides reasonably full support for print settings. + + +Edit Print Options +------------------- +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.print_options.horizontalCentered = True +>>> ws.print_options.verticalCentered = True + + +Headers and Footers +------------------- + +Headers and footers use their own formatting language. This is fully +supported when writing them but, due to the complexity and the possibility of +nesting, only partially when reading them. There is support for the font, +size and color for a left, centre/center, or right element. Granular control +(highlighting individuals words) will require applying control codes +manually. + + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.oddHeader.left.text = "Page &[Page] of &N" +>>> ws.oddHeader.left.size = 14 +>>> ws.oddHeader.left.font = "Tahoma,Bold" +>>> ws.oddHeader.left.color = "CC3366" + + +Also supported are `evenHeader` and `evenFooter` as well as `firstHeader` and `firstFooter`. + + +Add Print Titles +---------------- + +You can print titles on every page to ensure that the data is properly +labelled. + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.print_title_cols = 'A:B' # the first two cols +>>> ws.print_title_rows = '1:1' # the first row + + +Add a Print Area +---------------- + +You can select a part of a worksheet as the only part that you want to print + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.print_area = 'A1:F10' diff --git a/doc/protection.rst b/doc/protection.rst new file mode 100644 index 0000000..b67722e --- /dev/null +++ b/doc/protection.rst @@ -0,0 +1,62 @@ +Protection +========== + +.. warning:: + + Password protecting a workbook or worksheet only provides a quite basic level of security. + The data is not encrypted, so can be modified by any number of freely available tools. In + fact the specification states: "Worksheet or workbook element protection should not be + confused with file security. It is meant to make your workbook safe from unintentional + modification, and cannot protect it from malicious modification." + +Openpyxl provides support for protecting a workbook and worksheet from modification. The Open XML +"Legacy Password Hash Algorithm" is used to generate hashed password values unless another +algorithm is explicitly configured. + +Workbook Protection +------------------- + +To prevent other users from viewing hidden worksheets, adding, moving, deleting, or hiding worksheets, and +renaming worksheets, you can protect the structure of your workbook with a password. The password can be +set using the :func:`openpyxl.workbook.protection.WorkbookProtection.workbookPassword` property :: + + >>> wb.security.workbookPassword = '...' + >>> wb.security.lockStructure = True + + +Similarly removing change tracking and change history from a shared workbook can be prevented by setting +another password. This password can be set using the +:func:`openpyxl.workbook.protection.WorkbookProtection.revisionsPassword` property :: + + >>> wb.security.revisionsPassword = '...' + +Other properties on the :class:`openpyxl.workbook.protection.WorkbookProtection` object control exactly what +restrictions are in place, but these will only be enforced if the appropriate password is set. + +Specific setter functions are provided if you need to set the raw password value without using the +default hashing algorithm - e.g. :: + + hashed_password = ... + wb.security.set_workbook_password(hashed_password, already_hashed=True) + + +Worksheet Protection +-------------------- + +Various aspects of a worksheet can also be locked by setting attributes on the +:class:`openpyxl.worksheet.protection.SheetProtection` object. Unlike workbook protection, sheet +protection may be enabled with or without using a password. Sheet protection is enabled using the +:attr:`openpxyl.worksheet.protection.SheetProtection.sheet` attribute or calling `enable()` or `disable()`:: + + >>> ws = wb.active + >>> wb.protection.sheet = True + >>> wb.protection.enable() + >>> wb.protection.disabe() + + +If no password is specified, users can disable configured sheet protection without specifying a password. +Otherwise they must supply a password to change configured protections. The password is set using +the :func:`openpxyl.worksheet.protection.SheetProtection.password` property :: + + >>> ws = wb.active + >>> ws.protection.password = '...' diff --git a/doc/styles.rst b/doc/styles.rst new file mode 100644 index 0000000..7205311 --- /dev/null +++ b/doc/styles.rst @@ -0,0 +1,301 @@ +Working with styles +=================== + +Introduction +------------ + +Styles are used to change the look of your data while displayed on screen. +They are also used to determine the formatting for numbers. + +Styles can be applied to the following aspects: + + * font to set font size, color, underlining, etc. + * fill to set a pattern or color gradient + * border to set borders on a cell + * cell alignment + * protection + +The following are the default values + +.. :: doctest + +>>> from openpyxl.styles import PatternFill, Border, Side, Alignment, Protection, Font +>>> font = Font(name='Calibri', +... size=11, +... bold=False, +... italic=False, +... vertAlign=None, +... underline='none', +... strike=False, +... color='FF000000') +>>> fill = PatternFill(fill_type=None, +... start_color='FFFFFFFF', +... end_color='FF000000') +>>> border = Border(left=Side(border_style=None, +... color='FF000000'), +... right=Side(border_style=None, +... color='FF000000'), +... top=Side(border_style=None, +... color='FF000000'), +... bottom=Side(border_style=None, +... color='FF000000'), +... diagonal=Side(border_style=None, +... color='FF000000'), +... diagonal_direction=0, +... outline=Side(border_style=None, +... color='FF000000'), +... vertical=Side(border_style=None, +... color='FF000000'), +... horizontal=Side(border_style=None, +... color='FF000000') +... ) +>>> alignment=Alignment(horizontal='general', +... vertical='bottom', +... text_rotation=0, +... wrap_text=False, +... shrink_to_fit=False, +... indent=0) +>>> number_format = 'General' +>>> protection = Protection(locked=True, +... hidden=False) +>>> + +Cell Styles and Named Styles +---------------------------- + +There are two types of styles: cell styles and named styles, also known as style templates. + +Cell Styles ++++++++++++ + +Cell styles are shared between objects and once they have been assigned they +cannot be changed. This stops unwanted side-effects such as changing the +style for lots of cells when instead of only one. + +.. :: doctest + +>>> from openpyxl.styles import colors +>>> from openpyxl.styles import Font, Color +>>> from openpyxl import Workbook +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> a1 = ws['A1'] +>>> d4 = ws['D4'] +>>> ft = Font(color=colors.RED) +>>> a1.font = ft +>>> d4.font = ft +>>> +>>> a1.font.italic = True # is not allowed # doctest: +SKIP +>>> +>>> # If you want to change the color of a Font, you need to reassign it:: +>>> +>>> a1.font = Font(color=colors.RED, italic=True) # the change only affects A1 + + +Copying styles +-------------- + +Styles can also be copied + +.. :: doctest + +>>> from openpyxl.styles import Font +>>> from copy import copy +>>> +>>> ft1 = Font(name='Arial', size=14) +>>> ft2 = copy(ft1) +>>> ft2.name = "Tahoma" +>>> ft1.name +'Arial' +>>> ft2.name +'Tahoma' +>>> ft2.size # copied from the +14.0 + + +Basic Font Colors +----------------- +Colors are usually RGB or aRGB hexvalues. The `colors` module contains some handy constants + +.. :: doctest + +>>> from openpyxl.styles import Font +>>> from openpyxl.styles.colors import RED +>>> font = Font(color=RED) +>>> font = Font(color="FFBB00") + +There is also support for legacy indexed colors as well as themes and tints + +>>> from openpyxl.styles.colors import Color +>>> c = Color(indexed=32) +>>> c = Color(theme=6, tint=0.5) + + +Applying Styles +--------------- +Styles are applied directly to cells + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> from openpyxl.styles import Font, Fill +>>> wb = Workbook() +>>> ws = wb.active +>>> c = ws['A1'] +>>> c.font = Font(size=12) + +Styles can also applied to columns and rows but note that this applies only +to cells created (in Excel) after the file is closed. If you want to apply +styles to entire rows and columns then you must apply the style to each cell +yourself. This is a restriction of the file format:: + +>>> col = ws.column_dimensions['A'] +>>> col.font = Font(bold=True) +>>> row = ws.row_dimensions[1] +>>> row.font = Font(underline="single") + +.. _styling-merged-cells: + +Styling Merged Cells +-------------------- + +Sometimes you want to format a range of cells as if they were a single +object. Excel pretends that this is possible by merging cells (deleting all +but the top-left cell) and then recreating them in order to apply +pseudo-styles. + +.. literalinclude:: format_merged_cells.py + + +Edit Page Setup +------------------- +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE +>>> ws.page_setup.paperSize = ws.PAPERSIZE_TABLOID +>>> ws.page_setup.fitToHeight = 0 +>>> ws.page_setup.fitToWidth = 1 + + +Named Styles +++++++++++++ + +In contrast to Cell Styles, Named Styles are mutable. They make sense when +you want to apply formatting to lots of different cells at once. NB. once you +have assigned a named style to a cell, additional changes to the style will +**not** affect the cell. + +Once a named style has been registered with a workbook, it can be referred to simply by name. + + +Creating a Named Style +---------------------- + +.. :: doctest + +>>> from openpyxl.styles import NamedStyle, Font, Border, Side +>>> highlight = NamedStyle(name="highlight") +>>> highlight.font = Font(bold=True, size=20) +>>> bd = Side(style='thick', color="000000") +>>> highlight.border = Border(left=bd, top=bd, right=bd, bottom=bd) + +Once a named style has been created, it can be registered with the workbook: + +>>> wb.add_named_style(highlight) + +But named styles will also be registered automatically the first time they are assigned to a cell: + +>>> ws['A1'].style = highlight + +Once registered assign the style using just the name: + +>>> ws['D5'].style = 'highlight' + + +Using builtin styles +-------------------- + +The specification includes some builtin styles which can also be used. +Unfortunately, the names for these styles are stored in their localised +forms. openpyxl will only recognise the English names and only exactly as +written here. These are as follows: + + +* 'Normal' # same as no style + +Number formats +++++++++++++++ + +* 'Comma' +* 'Comma [0]' +* 'Currency' +* 'Currency [0]' +* 'Percent' + +Informative ++++++++++++ + +* 'Calculation' +* 'Total' +* 'Note' +* 'Warning Text' +* 'Explanatory Text' + +Text styles ++++++++++++ + +* 'Title' +* 'Headline 1' +* 'Headline 2' +* 'Headline 3' +* 'Headline 4' +* 'Hyperlink' +* 'Followed Hyperlink' +* 'Linked Cell' + +Comparisons ++++++++++++ + +* 'Input' +* 'Output' +* 'Check Cell' +* 'Good' +* 'Bad' +* 'Neutral' + +Highlights +++++++++++ + +* 'Accent1' +* '20 % - Accent1' +* '40 % - Accent1' +* '60 % - Accent1' +* 'Accent2' +* '20 % - Accent2' +* '40 % - Accent2' +* '60 % - Accent2' +* 'Accent3' +* '20 % - Accent3' +* '40 % - Accent3' +* '60 % - Accent3' +* 'Accent4' +* '20 % - Accent4' +* '40 % - Accent4' +* '60 % - Accent4' +* 'Accent5' +* '20 % - Accent5' +* '40 % - Accent5' +* '60 % - Accent5' +* 'Accent6' +* '20 % - Accent6' +* '40 % - Accent6' +* '60 % - Accent6' +* 'Pandas' + +For more information about the builtin styles please refer to the :mod:`openpyxl.styles.builtins` diff --git a/doc/table.png b/doc/table.png new file mode 100644 index 0000000..54cb868 Binary files /dev/null and b/doc/table.png differ diff --git a/doc/table.py b/doc/table.py new file mode 100644 index 0000000..afc7091 --- /dev/null +++ b/doc/table.py @@ -0,0 +1,26 @@ +from openpyxl import Workbook +from openpyxl.worksheet.table import Table, TableStyleInfo + +wb = Workbook() +ws = wb.active + +data = [ + ['Apples', 10000, 5000, 8000, 6000], + ['Pears', 2000, 3000, 4000, 5000], + ['Bananas', 6000, 6000, 6500, 6000], + ['Oranges', 500, 300, 200, 700], +] + +# add column headings. NB. these must be strings +ws.append(["Fruit", "2011", "2012", "2013", "2014"]) +for row in data: + ws.append(row) + +tab = Table(displayName="Table1", ref="A1:E5") + +# Add a default style with striped rows and banded columns +style = TableStyleInfo(name="TableStyleMedium9", showFirstColumn=False, + showLastColumn=False, showRowStripes=True, showColumnStripes=True) +tab.tableStyleInfo = style +ws.add_table(tab) +wb.save("table.xlsx") diff --git a/doc/tutorial.rst b/doc/tutorial.rst new file mode 100644 index 0000000..7ad6566 --- /dev/null +++ b/doc/tutorial.rst @@ -0,0 +1,308 @@ +Manipulating a workbook in memory +================================= + +Create a workbook +----------------- + +There is no need to create a file on the filesystem to get started with openpyxl. +Just import the Workbook class and start using it :: + + >>> from openpyxl import Workbook + >>> wb = Workbook() + +A workbook is always created with at least one worksheet. You can get it by +using the :func:`openpyxl.workbook.Workbook.active` property :: + + >>> ws = wb.active + +.. note:: + + This function uses the `_active_sheet_index` property, set to 0 by default. + Unless you modify its value, you will always get the + first worksheet by using this method. + +You can also create new worksheets by using the +:func:`openpyxl.workbook.Workbook.create_sheet` method :: + + >>> ws1 = wb.create_sheet("Mysheet") # insert at the end (default) + # or + >>> ws2 = wb.create_sheet("Mysheet", 0) # insert at first position + +Sheets are given a name automatically when they are created. +They are numbered in sequence (Sheet, Sheet1, Sheet2, ...). +You can change this name at any time with the `title` property:: + + ws.title = "New Title" + +The background color of the tab holding this title is white by default. +You can change this providing an RRGGBB color code to the sheet_properties.tabColor property:: + + ws.sheet_properties.tabColor = "1072BA" + +Once you gave a worksheet a name, you can get it as a key of the workbook:: + + >>> ws3 = wb["New Title"] + +You can review the names of all worksheets of the workbook with the +:func:`openpyxl.workbook.Workbook.sheetnames` property :: + + >>> print(wb.sheetnames) + ['Sheet2', 'New Title', 'Sheet1'] + +You can loop through worksheets :: + + >>> for sheet in wb: + ... print(sheet.title) + +You can create copies of worksheets *within a single workbook*: + +:func:`openpyxl.workbook.Workbook.copy_worksheet` method:: + + >>> source = wb.active + >>> target = wb.copy_worksheet(source) + +.. note:: + + Only cells (including values, styles, hyperlinks and comments) and + certain worksheet attribues (including dimensions, format and + properties) are copied. All other workbook / worksheet attributes + are not copied - e.g. Images, Charts. + +.. note:: + + You cannot copy worksheets between workbooks. You also cannot copy + a worksheet if the workbook is open in `read-only` or `write-only` + mode. + + +Playing with data +------------------ + +Accessing one cell +++++++++++++++++++ + +Now we know how to access a worksheet, we can start modifying cells content. + +Cells can be accessed directly as keys of the worksheet :: + + >>> c = ws['A4'] + +This will return the cell at A4 or create one if it does not exist yet. +Values can be directly assigned :: + + >>> ws['A4'] = 4 + +There is also the :func:`openpyxl.worksheet.Worksheet.cell` method. + +This provides access to cells using row and column notation:: + + >>> d = ws.cell(row=4, column=2, value=10) + +.. note:: + + When a worksheet is created in memory, it contains no `cells`. They are + created when first accessed. + +.. warning:: + + Because of this feature, scrolling through cells instead of accessing them + directly will create them all in memory, even if you don't assign them a value. + + Something like :: + + >>> for i in range(1,101): + ... for j in range(1,101): + ... ws.cell(row=i, column=j) + + will create 100x100 cells in memory, for nothing. + + + +Accessing many cells +++++++++++++++++++++ + +Ranges of cells can be accessed using slicing :: + + >>> cell_range = ws['A1':'C2'] + + +Ranges of rows or columns can be obtained similarly:: + + >>> colC = ws['C'] + >>> col_range = ws['C:D'] + >>> row10 = ws[10] + >>> row_range = ws[5:10] + +You can also use the :func:`openpyxl.worksheet.Worksheet.iter_rows` method:: + + >>> for row in ws.iter_rows(min_row=1, max_col=3, max_row=2): + ... for cell in row: + ... print(cell) + + + + + + + +Likewise the :func:`openpyxl.worksheet.Worksheet.iter_cols` method will return columns:: + + >>> for col in ws.iter_cols(min_row=1, max_col=3, max_row=2): + ... for cell in col: + ... print(cell) + + + + + + + + +If you need to iterate through all the rows or columns of a file, you can instead use the +:func:`openpyxl.worksheet.Worksheet.rows` property:: + + >>> ws = wb.active + >>> ws['C9'] = 'hello world' + >>> tuple(ws.rows) + ((, , ), + (, , ), + (, , ), + (, , ), + (, , ), + (, , ), + (, , ), + (, , ), + (, , )) + +or the :func:`openpyxl.worksheet.Worksheet.columns` property:: + + >>> tuple(ws.columns) + ((, + , + , + , + , + , + ... + , + , + ), + (, + , + , + , + , + , + , + , + )) + + +Data storage +++++++++++++ + +Once we have a :class:`openpyxl.cell.Cell`, we can assign it a value:: + + >>> c.value = 'hello, world' + >>> print(c.value) + 'hello, world' + + >>> d.value = 3.14 + >>> print(d.value) + 3.14 + +You can also enable type and format inference:: + + >>> wb = Workbook(guess_types=True) + >>> c.value = '12%' + >>> print(c.value) + 0.12 + + >>> import datetime + >>> d.value = datetime.datetime.now() + >>> print d.value + datetime.datetime(2010, 9, 10, 22, 25, 18) + + >>> c.value = '31.50' + >>> print(c.value) + 31.5 + + +Saving to a file +================ + +The simplest and safest way to save a workbook is by using the +:func:`openpyxl.workbook.Workbook.save()` method of the +:class:`openpyxl.workbook.Workbook` object:: + + >>> wb = Workbook() + >>> wb.save('balances.xlsx') + +.. warning:: + + This operation will overwrite existing files without warning. + +.. note:: + + Extension is not forced to be xlsx or xlsm, although you might have + some trouble opening it directly with another application if you don't + use an official extension. + + As OOXML files are basically ZIP files, you can also end the filename + with .zip and open it with your favourite ZIP archive manager. + +You can specify the attribute `template=True`, to save a workbook +as a template:: + + >>> wb = load_workbook('document.xlsx') + >>> wb.template = True + >>> wb.save('document_template.xltx') + +or set this attribute to `False` (default), to save as a document:: + + >>> wb = load_workbook('document_template.xltx') + >>> wb.template = False + >>> wb.save('document.xlsx', as_template=False) + +.. warning:: + + You should monitor the data attributes and document extensions + for saving documents in the document templates and vice versa, + otherwise the result table engine can not open the document. + +.. note:: + + The following will fail:: + + >>> wb = load_workbook('document.xlsx') + >>> # Need to save with the extension *.xlsx + >>> wb.save('new_document.xlsm') + >>> # MS Excel can't open the document + >>> + >>> # or + >>> + >>> # Need specify attribute keep_vba=True + >>> wb = load_workbook('document.xlsm') + >>> wb.save('new_document.xlsm') + >>> # MS Excel will not open the document + >>> + >>> # or + >>> + >>> wb = load_workbook('document.xltm', keep_vba=True) + >>> # If we need a template document, then we must specify extension as *.xltm. + >>> wb.save('new_document.xlsm') + >>> # MS Excel will not open the document + + +Loading from a file +=================== + +The same way as writing, you can import :func:`openpyxl.load_workbook` to +open an existing workbook:: + + >>> from openpyxl import load_workbook + >>> wb2 = load_workbook('test.xlsx') + >>> print wb2.get_sheet_names() + ['Sheet2', 'New Title', 'Sheet1'] + +This ends the tutorial for now, you can proceed to the :doc:`usage` section diff --git a/doc/usage.rst b/doc/usage.rst new file mode 100644 index 0000000..33ce534 --- /dev/null +++ b/doc/usage.rst @@ -0,0 +1,164 @@ +Simple usage +============ + +Write a workbook +---------------- +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.compat import range +>>> from openpyxl.utils import get_column_letter +>>> +>>> wb = Workbook() +>>> +>>> dest_filename = 'empty_book.xlsx' +>>> +>>> ws1 = wb.active +>>> ws1.title = "range names" +>>> +>>> for row in range(1, 40): +... ws1.append(range(600)) +>>> +>>> ws2 = wb.create_sheet(title="Pi") +>>> +>>> ws2['F5'] = 3.14 +>>> +>>> ws3 = wb.create_sheet(title="Data") +>>> for row in range(10, 20): +... for col in range(27, 54): +... _ = ws3.cell(column=col, row=row, value="{0}".format(get_column_letter(col))) +>>> print(ws3['AA10'].value) +AA +>>> wb.save(filename = dest_filename) + + +Read an existing workbook +------------------------- +.. :: doctest + +>>> from openpyxl import load_workbook +>>> wb = load_workbook(filename = 'empty_book.xlsx') +>>> sheet_ranges = wb['range names'] +>>> print(sheet_ranges['D18'].value) +3 + + +.. note :: + + There are several flags that can be used in load_workbook. + + - `guess_types` will enable or disable (default) type inference when + reading cells. + + - `data_only` controls whether cells with formulae have either the + formula (default) or the value stored the last time Excel read the sheet. + + - `keep_vba` controls whether any Visual Basic elements are preserved or + not (default). If they are preserved they are still not editable. + + +.. warning :: + + openpyxl does currently not read all possible items in an Excel file so + images and charts will be lost from existing files if they are opened and + saved with the same name. + + +Using number formats +-------------------- +.. :: doctest + +>>> import datetime +>>> from openpyxl import Workbook +>>> wb = Workbook() +>>> ws = wb.active +>>> # set date using a Python datetime +>>> ws['A1'] = datetime.datetime(2010, 7, 21) +>>> +>>> ws['A1'].number_format +'yyyy-mm-dd h:mm:ss' +>>> # You can enable type inference on a case-by-case basis +>>> wb.guess_types = True +>>> # set percentage using a string followed by the percent sign +>>> ws['B1'] = '3.14%' +>>> wb.guess_types = False +>>> ws['B1'].value +0.031400000000000004 +>>> +>>> ws['B1'].number_format +'0%' + + +Using formulae +-------------- +.. :: doctest + +>>> from openpyxl import Workbook +>>> wb = Workbook() +>>> ws = wb.active +>>> # add a simple formula +>>> ws["A1"] = "=SUM(1, 1)" +>>> wb.save("formula.xlsx") + +.. warning:: + NB you must use the English name for a function and function arguments *must* be separated by commas and not other punctuation such as semi-colons. + +openpyxl never evaluates formula but it is possible to check the name of a formula: + +.. :: doctest + +>>> from openpyxl.utils import FORMULAE +>>> "HEX2DEC" in FORMULAE +True + +If you're trying to use a formula that isn't known this could be because you're using a formula that was not included in the initial specification. Such formulae must be prefixed with `_xlfn.` to work. + +Merge / Unmerge cells +--------------------- + +When you merge cells all cells but the top-left one are **removed** from the +worksheet. See :ref:`styling-merged-cells` for information on formatting merged cells. + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> ws.merge_cells('A2:D2') +>>> ws.unmerge_cells('A2:D2') +>>> +>>> # or equivalently +>>> ws.merge_cells(start_row=2, start_column=1, end_row=4, end_column=4) +>>> ws.unmerge_cells(start_row=2, start_column=1, end_row=4, end_column=4) + + +Inserting an image +------------------- +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.drawing.image import Image +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> ws['A1'] = 'You should see three logos below' + +>>> # create an image +>>> img = Image('logo.png') + +>>> # add to worksheet and anchor next to cells +>>> ws.add_image(img, 'A1') +>>> wb.save('logo.xlsx') + + +Fold columns (outline) +---------------------- +.. :: doctest + +>>> import openpyxl +>>> wb = openpyxl.Workbook() +>>> ws = wb.create_sheet() +>>> ws.column_dimensions.group('A','D', hidden=True) +>>> wb.save('group.xlsx') diff --git a/doc/validation.rst b/doc/validation.rst new file mode 100644 index 0000000..cf6d5cf --- /dev/null +++ b/doc/validation.rst @@ -0,0 +1,113 @@ +Validating cells +================ + +Data validators can be applied to ranges of cells but are not enforced or evaluated. Ranges do not have to be contiguous: eg. "A1 B2:B5" is contains A1 and the cells B2 to B5 but not A2 or B2. + + +Examples +-------- + +.. :: doctest + +>>> from openpyxl import Workbook +>>> from openpyxl.worksheet.datavalidation import DataValidation +>>> +>>> # Create the workbook and worksheet we'll be working with +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> # Create a data-validation object with list validation +>>> dv = DataValidation(type="list", formula1='"Dog,Cat,Bat"', allow_blank=True) +>>> +>>> # Optionally set a custom error message +>>> dv.error ='Your entry is not in the list' +>>> dv.errorTitle = 'Invalid Entry' +>>> +>>> # Optionally set a custom prompt message +>>> dv.prompt = 'Please select from the list' +>>> dv.promptTitle = 'List Selection' +>>> +>>> # Add the data-validation object to the worksheet +>>> ws.add_data_validation(dv) + +>>> # Create some cells, and add them to the data-validation object +>>> c1 = ws["A1"] +>>> c1.value = "Dog" +>>> dv.add(c1) +>>> c2 = ws["A2"] +>>> c2.value = "An invalid value" +>>> dv.add(c2) +>>> +>>> # Or, apply the validation to a range of cells +>>> dv.add('B1:B1048576') # This is the same as for the whole of column B +>>> +>>> # Check with a cell is in the validator +>>> "B4" in dv +True + + +.. note :: + + Validations without any cell ranges will be ignored when saving a workbook. + +Other validation examples +------------------------- + +Any whole number: +:: + + dv = DataValidation(type="whole") + +Any whole number above 100: +:: + + dv = DataValidation(type="whole", + operator="greaterThan", + formula1=100) + +Any decimal number: +:: + + dv = DataValidation(type="decimal") + +Any decimal number between 0 and 1: +:: + + dv = DataValidation(type="decimal", + operator="between", + formula1=0, + formula2=1) + +Any date: +:: + + dv = DataValidation(type="date") + +or time: +:: + + dv = DataValidation(type="time") + +Any string at most 15 characters: +:: + + dv = DataValidation(type="textLength", + operator="lessThanOrEqual"), + formula1=15) + +Cell range validation: +:: + + from openpyxl.utils import quote_sheetname + dv = DataValidation(type="list", + formula1="{0}!$B$1:$B$10".format(quote_sheetname(sheetname)) + ) + +Custom rule: +:: + + dv = DataValidation(type="custom", + formula1"=SOMEFORMULA") + +.. note:: + See http://www.contextures.com/xlDataVal07.html for custom rules diff --git a/doc/windows-development.rst b/doc/windows-development.rst new file mode 100644 index 0000000..2cda1cc --- /dev/null +++ b/doc/windows-development.rst @@ -0,0 +1,82 @@ +Testing on Windows +================== + + +Although openpyxl itself is pure Python and should run on any Python, we do use some libraries that require compiling for tests and documentation. The setup for testing on Windows is somewhat different. + + +Getting started +--------------- + +Once you have installed the versions of Python (2.6, 2.7, 3.3, 3.4) you should setup a development environment for testing so that you do not adversely affect the system install. + + +Setting up a development environment +------------------------------------ + +First of all you should checkout a copy of the repository. Atlassian provides a nice GUI client `SourceTree `_ that allows you to do this with a single-click from the browser. + +By default the repository will be installed under your user folder. eg. c:\Users\YOURUSER\openpyxl + +Switch to the branch you want to work on by double-clicking it. The default branch should never be used for development work. + +Creating a virtual environment +++++++++++++++++++++++++++++++ + +You will need to manually install virtualenv. This is best done by first installing pip. open a command line and download the script "get_pip.py" to your preferred Python folder:: + + bitsadmin /transfer pip http://bootstrap.pypa.io/get-pip.py c:\python27\get-pip.py # change the path as necessary + +Install pip (it needs to be at least pip 6.0):: + + python get_pip.py + +Now you can install virtualenv:: + + Scripts\pip install virtualenv + Scripts\virtualenv c:\Users\YOURUSER\openpyxl + + +lxml +---- + +openpyxl needs `lxml` in order to run the tests. Unfortunately, automatic installation of lxml on Windows is tricky as pip defaults to try and compile it. This can be avoided by using pre-compiled versions of the library. + +#. In the command line switch to your repository folder:: + + cd c:\Users\YOURUSER\openpyxl + +#. Activate the virtualenv:: + + Scripts\activate + +#. Install a development version of openpyxl:: + + pip install -e . + +#. Download all the relevant `lxml Windows wheels `_ + + Releases for legacy versions of Python: + + * `lxml 4.0.0 for Python 2.7 `_ + * `lxml 4.0.0 for Python 3.6 `_ + +#. Move all these files to a folder called "downloads" in your openpyxl checkout + +#. Install the project requirements:: + + pip download -r requirements.txt -d downloads + pip install --no-index --find-links downloads -r requirements.txt + +To run tests for the virtualenv:: + + py.test -xrf openpyxl # the flag will stop testing at the first error + + +tox +--- + +We use `tox` to run the tests on different Python versions and configurations. Using it is as simple as:: + + set PIP_FIND_LINKS=downloads + tox openpyxl diff --git a/doc/worksheet_properties.rst b/doc/worksheet_properties.rst new file mode 100644 index 0000000..c652212 --- /dev/null +++ b/doc/worksheet_properties.rst @@ -0,0 +1,57 @@ +Additional Worksheet Properties +=============================== + +These are advanced properties for particular behaviours, the most used ones +are the "fitTopage" page setup property and the tabColor that define the +background color of the worksheet tab. + +Available properties for worksheets +----------------------------------- + +* "enableFormatConditionsCalculation" +* "filterMode" +* "published" +* "syncHorizontal" +* "syncRef" +* "syncVertical" +* "transitionEvaluation" +* "transitionEntry" +* "tabColor" + +Available fields for page setup properties +------------------------------------------ + +"autoPageBreaks" +"fitToPage" + +Available fields for outlines +----------------------------- + +* "applyStyles" +* "summaryBelow" +* "summaryRight" +* "showOutlineSymbols" + +see http://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.sheetproperties%28v=office.14%29.aspx_ for details. + +.. note:: + By default, outline properties are intitialized so you can directly modify each of their 4 attributes, while page setup properties don't. + If you want modify the latter, you should first initialize a :class:`openpyxl.worksheet.properties.PageSetupProperties` object with the required parameters. + Once done, they can be directly modified by the routine later if needed. + + +.. :: doctest + +>>> from openpyxl.workbook import Workbook +>>> from openpyxl.worksheet.properties import WorksheetProperties, PageSetupProperties +>>> +>>> wb = Workbook() +>>> ws = wb.active +>>> +>>> wsprops = ws.sheet_properties +>>> wsprops.tabColor = "1072BA" +>>> wsprops.filterMode = False +>>> wsprops.pageSetUpPr = PageSetupProperties(fitToPage=True, autoPageBreaks=False) +>>> wsprops.outlinePr.summaryBelow = False +>>> wsprops.outlinePr.applyStyles = True +>>> wsprops.pageSetUpPr.autoPageBreaks = True diff --git a/doc/worksheet_tables.rst b/doc/worksheet_tables.rst new file mode 100644 index 0000000..c060f07 --- /dev/null +++ b/doc/worksheet_tables.rst @@ -0,0 +1,26 @@ +Worksheet Tables +================ + + +Worksheet tables are references to groups of cells. This makes +certain operations such as styling the cells in a table easier. + + +Creating a table +---------------- + +.. literalinclude:: table.py + + +By default tables are created with a header from the first row and filters for all the columns. + +Styles are managed using the the `TableStyleInfo` object. This allows you to +stripe rows or columns and apply the different colour schemes. + + +Important notes +--------------- + +Table names must be unique within a workbook and table headers and filter +ranges must always contain strings. If this is not the case then Excel may +consider the file invalid and remove the table. diff --git a/openpyxl/.constants.json b/openpyxl/.constants.json new file mode 100644 index 0000000..54cfbe5 --- /dev/null +++ b/openpyxl/.constants.json @@ -0,0 +1,8 @@ +{ + "__author__": "See AUTHORS", + "__author_email__": "charlie.clark@clark-consulting.eu", + "__license__": "MIT/Expat", + "__maintainer_email__": "openpyxl-users@googlegroups.com", + "__url__": "https://openpyxl.readthedocs.io", + "__version__": "2.5.3" +} diff --git a/openpyxl/__init__.py b/openpyxl/__init__.py new file mode 100644 index 0000000..2af840f --- /dev/null +++ b/openpyxl/__init__.py @@ -0,0 +1,30 @@ +# Copyright (c) 2010-2018 openpyxl + +# @license: http://www.opensource.org/licenses/mit-license.php +# @author: see AUTHORS file + + +import json +import os + +try: + here = os.path.abspath(os.path.dirname(__file__)) + src_file = os.path.join(here, ".constants.json") + with open(src_file) as src: + constants = json.load(src) + __author__ = constants['__author__'] + __author_email__ = constants["__author_email__"] + __license__ = constants["__license__"] + __maintainer_email__ = constants["__maintainer_email__"] + __url__ = constants["__url__"] + __version__ = constants["__version__"] +except IOError: + # packaged + pass + +"""Imports for the openpyxl package.""" +from openpyxl.compat.numbers import NUMPY, PANDAS +from openpyxl.xml import LXML + +from openpyxl.workbook import Workbook +from openpyxl.reader.excel import load_workbook diff --git a/openpyxl/cell/__init__.py b/openpyxl/cell/__init__.py new file mode 100644 index 0000000..25bb2b9 --- /dev/null +++ b/openpyxl/cell/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from .cell import Cell, WriteOnlyCell +from .read_only import ReadOnlyCell diff --git a/openpyxl/cell/cell.py b/openpyxl/cell/cell.py new file mode 100644 index 0000000..0367a21 --- /dev/null +++ b/openpyxl/cell/cell.py @@ -0,0 +1,382 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +"""Manage individual cells in a spreadsheet. + +The Cell class is required to know its value and type, display options, +and any other features of an Excel cell. Utilities for referencing +cells using Excel's 'A1' column/row nomenclature are also provided. + +""" + +__docformat__ = "restructuredtext en" + +# Python stdlib imports +from copy import copy +import datetime +import re + +from openpyxl.compat import ( + unicode, + basestring, + bytes, + NUMERIC_TYPES, + range, + deprecated, +) +from openpyxl.utils.units import ( + DEFAULT_ROW_HEIGHT, + DEFAULT_COLUMN_WIDTH +) +from openpyxl.utils.datetime import ( + to_excel, + time_to_days, + timedelta_to_days, + from_excel + ) +from openpyxl.utils.exceptions import ( + IllegalCharacterError +) +from openpyxl.utils.units import points_to_pixels +from openpyxl.utils import ( + get_column_letter, + column_index_from_string, +) +from openpyxl.styles import numbers, is_date_format +from openpyxl.styles.styleable import StyleableObject +from openpyxl.worksheet.hyperlink import Hyperlink + +# constants + + +TIME_TYPES = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) +STRING_TYPES = (basestring, unicode, bytes) +KNOWN_TYPES = NUMERIC_TYPES + TIME_TYPES + STRING_TYPES + (bool, type(None)) + +PERCENT_REGEX = re.compile(r'^(?P\-?[0-9]*\.?[0-9]*\s?)\%$') +TIME_REGEX = re.compile(r""" +^(?: # HH:MM and HH:MM:SS +(?P[0-1]{0,1}[0-9]{2}): +(?P[0-5][0-9]):? +(?P[0-5][0-9])?$) +| +^(?: # MM:SS. +([0-5][0-9]): +([0-5][0-9])?\. +(?P\d{1,6})) +""", re.VERBOSE) +NUMBER_REGEX = re.compile(r'^-?([\d]|[\d]+\.[\d]*|\.[\d]+|[1-9][\d]+\.?[\d]*)((E|e)[-+]?[\d]+)?$') +ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]') + +ERROR_CODES = ('#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', + '#N/A') + + +class Cell(StyleableObject): + """Describes cell associated properties. + + Properties of interest include style, type, value, and address. + + """ + __slots__ = ( + 'row', + 'col_idx', + '_value', + 'data_type', + 'parent', + '_hyperlink', + '_comment', + ) + + ERROR_CODES = ERROR_CODES + + TYPE_STRING = 's' + TYPE_FORMULA = 'f' + TYPE_NUMERIC = 'n' + TYPE_BOOL = 'b' + TYPE_NULL = 'n' + TYPE_INLINE = 'inlineStr' + TYPE_ERROR = 'e' + TYPE_FORMULA_CACHE_STRING = 'str' + + VALID_TYPES = (TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL, + TYPE_NULL, TYPE_INLINE, TYPE_ERROR, TYPE_FORMULA_CACHE_STRING) + + + def __init__(self, worksheet, column=None, row=None, value=None, col_idx=None, style_array=None): + super(Cell, self).__init__(worksheet, style_array) + self.row = row + """Row number of this cell (1-based)""" + # _value is the stored value, while value is the displayed value + self._value = None + self._hyperlink = None + self.data_type = 'n' + if value is not None: + self.value = value + self._comment = None + if column is not None: + col_idx = column_index_from_string(column) + self.col_idx = col_idx + """Column number of this cell (1-based)""" + + + @property + def coordinate(self): + """This cell's coordinate (ex. 'A5')""" + return '%s%d' % (self.column, self.row) + + @property + def column(self): + """The letter of this cell's column (ex. 'A')""" + return get_column_letter(self.col_idx) + + @property + def encoding(self): + return self.parent.encoding + + @property + def base_date(self): + return self.parent.parent.excel_base_date + + @property + def guess_types(self): + return getattr(self.parent.parent, 'guess_types', False) + + def __repr__(self): + return "".format(self.parent.title, self.coordinate) + + def check_string(self, value): + """Check string coding, length, and line break character""" + if value is None: + return + # convert to unicode string + if not isinstance(value, unicode): + value = unicode(value, self.encoding) + value = unicode(value) + # string must never be longer than 32,767 characters + # truncate if necessary + value = value[:32767] + if next(ILLEGAL_CHARACTERS_RE.finditer(value), None): + raise IllegalCharacterError + return value + + def check_error(self, value): + """Tries to convert Error" else N/A""" + try: + return unicode(value) + except UnicodeDecodeError: + return u'#N/A' + + def set_explicit_value(self, value=None, data_type=TYPE_STRING): + """Coerce values according to their explicit type""" + if data_type not in self.VALID_TYPES: + raise ValueError('Invalid data type: %s' % data_type) + if isinstance(value, STRING_TYPES): + value = self.check_string(value) + self._value = value + self.data_type = data_type + + + def _bind_value(self, value): + """Given a value, infer the correct data type""" + + self.data_type = "n" + + if value is True or value is False: + self.data_type = self.TYPE_BOOL + + elif isinstance(value, NUMERIC_TYPES): + pass + + elif isinstance(value, TIME_TYPES): + value = self._set_time_format(value) + self.data_type = "d" + + elif isinstance(value, STRING_TYPES): + value = self.check_string(value) + self.data_type = self.TYPE_STRING + if len(value) > 1 and value.startswith("="): + self.data_type = self.TYPE_FORMULA + elif value in self.ERROR_CODES: + self.data_type = self.TYPE_ERROR + elif self.guess_types: + value = self._infer_value(value) + + elif value is not None: + raise ValueError("Cannot convert {0!r} to Excel".format(value)) + + self._value = value + + + def _infer_value(self, value): + """Given a string, infer type and formatting options.""" + if not isinstance(value, unicode): + value = str(value) + + # number detection + v = self._cast_numeric(value) + if v is None: + # percentage detection + v = self._cast_percentage(value) + if v is None: + # time detection + v = self._cast_time(value) + if v is not None: + self.data_type = self.TYPE_NUMERIC + return v + + return value + + + def _cast_numeric(self, value): + """Explicity convert a string to a numeric value""" + if NUMBER_REGEX.match(value): + try: + return int(value) + except ValueError: + return float(value) + + def _cast_percentage(self, value): + """Explicitly convert a string to numeric value and format as a + percentage""" + match = PERCENT_REGEX.match(value) + if match: + self.number_format = numbers.FORMAT_PERCENTAGE + return float(match.group('number')) / 100 + + + def _cast_time(self, value): + """Explicitly convert a string to a number and format as datetime or + time""" + match = TIME_REGEX.match(value) + if match: + if match.group("microsecond") is not None: + value = value[:12] + pattern = "%M:%S.%f" + fmt = numbers.FORMAT_DATE_TIME5 + elif match.group('second') is None: + fmt = numbers.FORMAT_DATE_TIME3 + pattern = "%H:%M" + else: + pattern = "%H:%M:%S" + fmt = numbers.FORMAT_DATE_TIME6 + self.number_format = fmt + value = datetime.datetime.strptime(value, pattern) + return value.time() + + + def _set_time_format(self, value): + """Set number format for Python date or time""" + if isinstance(value, datetime.datetime): + #value = to_excel(value, self.base_date) + self.number_format = numbers.FORMAT_DATE_DATETIME + elif isinstance(value, datetime.date): + #value = to_excel(value, self.base_date) + self.number_format = numbers.FORMAT_DATE_YYYYMMDD2 + elif isinstance(value, datetime.time): + #value = time_to_days(value) + self.number_format = numbers.FORMAT_DATE_TIME6 + elif isinstance(value, datetime.timedelta): + #value = timedelta_to_days(value) + self.number_format = numbers.FORMAT_DATE_TIMEDELTA + return value + + @property + def value(self): + """Get or set the value held in the cell. + + :type: depends on the value (string, float, int or + :class:`datetime.datetime`) + """ + value = self._value + #if value is not None and self.is_date: + #value = from_excel(value, self.base_date) + return value + + @value.setter + def value(self, value): + """Set the value and infer type and display options.""" + self._bind_value(value) + + @property + def internal_value(self): + """Always returns the value for excel.""" + return self._value + + @property + def hyperlink(self): + """Return the hyperlink target or an empty string""" + return self._hyperlink + + + @hyperlink.setter + def hyperlink(self, val): + """Set value and display for hyperlinks in a cell. + Automatically sets the `value` of the cell with link text, + but you can modify it afterwards by setting the `value` + property, and the hyperlink will remain. + Hyperlink is removed if set to ``None``.""" + if val is None: + self._hyperlink = None + else: + if not isinstance(val, Hyperlink): + val = Hyperlink(ref="", target=val) + val.ref = self.coordinate + self._hyperlink = val + if self._value is None: + self.value = val.target or val.location + + + @property + def is_date(self): + """True if the value is formatted as a date + + :type: bool + """ + return self.data_type == 'd' or ( + self.data_type == 'n' and is_date_format(self.number_format) + ) + + + def offset(self, row=0, column=0): + """Returns a cell location relative to this cell. + + :param row: number of rows to offset + :type row: int + + :param column: number of columns to offset + :type column: int + + :rtype: :class:`openpyxl.cell.Cell` + """ + offset_column = self.col_idx + column + offset_row = self.row + row + return self.parent.cell(column=offset_column, row=offset_row) + + + @property + def comment(self): + """ Returns the comment associated with this cell + + :type: :class:`openpyxl.comments.Comment` + """ + return self._comment + + + @comment.setter + def comment(self, value): + """ + Assign a comment to a cell + """ + + if value is not None: + if value.parent: + value = copy(value) + value.bind(self) + elif value is None and self._comment: + self._comment.unbind() + self._comment = value + + +def WriteOnlyCell(ws=None, value=None): + return Cell(worksheet=ws, column='A', row=1, value=value) diff --git a/openpyxl/cell/interface.py b/openpyxl/cell/interface.py new file mode 100644 index 0000000..4c0c6e6 --- /dev/null +++ b/openpyxl/cell/interface.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.compat.abc import ABC +from abc import abstractmethod, abstractproperty + + +class AbstractCell(ABC): + + + def __init__(self, value=None): + self.value = value + + @abstractproperty + def encoding(self): + pass + + @abstractproperty + def coordinate(self): + pass + + @abstractproperty + def base_date(self): + pass + + @abstractproperty + def guess_types(self): + pass + + @abstractproperty + def value(self): + pass + + @abstractproperty + def internal_value(self): + pass + + @abstractmethod + def __repr__(self): + pass + + @abstractmethod + def offset(self, row=0, column=0): + pass + + @abstractproperty + def comment(self): + pass + + @abstractproperty + def style(self): + pass + + @abstractproperty + def number_format(self): + pass + + @abstractproperty + def is_date(self): + pass diff --git a/openpyxl/cell/read_only.py b/openpyxl/cell/read_only.py new file mode 100644 index 0000000..afc8654 --- /dev/null +++ b/openpyxl/cell/read_only.py @@ -0,0 +1,155 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import re + +from openpyxl.compat import unicode, long + +from openpyxl.cell import Cell +from openpyxl.utils import get_column_letter +from openpyxl.utils.datetime import from_excel +from openpyxl.styles import is_date_format +from openpyxl.styles.numbers import BUILTIN_FORMATS + + +FLOAT_REGEX = re.compile(r"\.|[E-e]") + + +def _cast_number(value): + "Convert numbers as string to an int or float" + m = FLOAT_REGEX.search(value) + if m is not None: + return float(value) + return long(value) + + +class ReadOnlyCell(object): + + __slots__ = ('parent', 'row', 'column', '_value', 'data_type', '_style_id') + + def __init__(self, sheet, row, column, value, data_type='n', style_id=0): + self.parent = sheet + self._value = None + self.row = row + self.column = column + self.data_type = data_type + self.value = value + self._style_id = style_id + + def __eq__(self, other): + for a in self.__slots__: + if getattr(self, a) != getattr(other, a): + return + return True + + def __ne__(self, other): + return not self.__eq__(other) + + + def __repr__(self): + return "".format(self.parent.title, self.coordinate) + + @property + def shared_strings(self): + return self.parent.shared_strings + + @property + def base_date(self): + return self.parent.base_date + + @property + def coordinate(self): + column = get_column_letter(self.column) + return "{1}{0}".format(self.row, column) + + @property + def style_array(self): + return self.parent.parent._cell_styles[self._style_id] + + @property + def number_format(self): + _id = self.style_array.numFmtId + if _id < 164: + return BUILTIN_FORMATS.get(_id, "General") + else: + return self.parent.parent._number_formats[_id - 164] + + @property + def font(self): + _id = self.style_array.fontId + return self.parent.parent._fonts[_id] + + @property + def fill(self): + _id = self.style_array.fillId + return self.parent.parent._fills[_id] + + @property + def border(self): + _id = self.style_array.borderId + return self.parent.parent._borders[_id] + + @property + def alignment(self): + _id = self.style_array.alignmentId + return self.parent.parent._alignments[_id] + + @property + def protection(self): + _id = self.style_array.protectionId + return self.parent.parent._protections[_id] + + @property + def is_date(self): + return self.data_type == 'n' and is_date_format(self.number_format) + + @property + def internal_value(self): + return self._value + + @property + def value(self): + if self._value is None: + return + if self.data_type == 'n': + if self.style_array: + if is_date_format(self.number_format): + return from_excel(self._value, self.base_date) + return self._value + if self.data_type == 'b': + return self._value == '1' + elif self.data_type in(Cell.TYPE_INLINE, Cell.TYPE_FORMULA_CACHE_STRING): + return unicode(self._value) + elif self.data_type == 's': + return unicode(self.shared_strings[int(self._value)]) + return self._value + + @value.setter + def value(self, value): + if self._value is not None: + raise AttributeError("Cell is read only") + if value is None: + self.data_type = 'n' + elif self.data_type == 'n': + value = _cast_number(value) + self._value = value + + +class EmptyCell(object): + + __slots__ = () + + value = None + is_date = False + font = None + border = None + fill = None + number_format = None + alignment = None + data_type = 'n' + + + def __repr__(self): + return "" + +EMPTY_CELL = EmptyCell() diff --git a/openpyxl/cell/tests/__init__.py b/openpyxl/cell/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openpyxl/cell/tests/test_cell.py b/openpyxl/cell/tests/test_cell.py new file mode 100644 index 0000000..f7b85dd --- /dev/null +++ b/openpyxl/cell/tests/test_cell.py @@ -0,0 +1,418 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + + +# Python stdlib imports +from datetime import ( + time, + datetime, + date, +) + +# 3rd party imports +import pytest + +# package imports + +from openpyxl.comments import Comment +from openpyxl.cell.cell import ERROR_CODES + + +@pytest.fixture +def DummyWorksheet(): + from openpyxl.utils.indexed_list import IndexedList + from openpyxl.utils.datetime import CALENDAR_WINDOWS_1900 + from openpyxl.cell import Cell + + class Wb(object): + excel_base_date = CALENDAR_WINDOWS_1900 + _fonts = IndexedList() + _fills = IndexedList() + _borders = IndexedList() + _protections = IndexedList() + _alignments = IndexedList() + _number_formats = IndexedList() + _cell_styles = IndexedList() + + + class Ws(object): + + encoding = 'utf-8' + parent = Wb() + title = "Dummy Worksheet" + _comment_count = 0 + + def cell(self, column, row): + return Cell(self, row=row, col_idx=column) + + return Ws() + + +@pytest.fixture +def Cell(): + from ..cell import Cell + return Cell + + +@pytest.fixture +def dummy_cell(DummyWorksheet, Cell): + ws = DummyWorksheet + cell = Cell(ws, column="A", row=1) + return cell + + +@pytest.fixture(params=[True, False]) +def guess_types(request): + return request.param + + +@pytest.mark.parametrize("value, expected", + [ + ('4.2', 4.2), + ('-42.000', -42), + ( '0', 0), + ('0.9999', 0.9999), + ('99E-02', 0.99), + ('4', 4), + ('-1E3', -1000), + ('2e+2', 200), + ] + ) +def test_cast_numeric(dummy_cell, value, expected): + cell = dummy_cell + result = cell._cast_numeric(value) + assert result == expected + + +@pytest.mark.parametrize("value, expected", + [ + ('-3.1%', -0.031), + ('3.1%', 0.031), + ('4.5 %', 0.045), + ] + ) +def test_cast_percent(dummy_cell, value, expected): + cell = dummy_cell + result = cell._cast_percentage(value) + assert result == expected + + +@pytest.mark.parametrize("value, expected", + [ + ('03:40:16', time(3, 40, 16)), + ('03:40', time(3, 40)), + ('30:33.865633336', time(0, 30, 33, 865633)), + ] + ) +def test_infer_datetime(dummy_cell, value, expected): + cell = dummy_cell + result = cell._cast_time(value) + assert result == expected + + +def test_ctor(dummy_cell): + cell = dummy_cell + assert cell.data_type == 'n' + assert cell.column == 'A' + assert cell.row == 1 + assert cell.coordinate == "A1" + assert cell.value is None + assert cell.comment is None + + +@pytest.mark.parametrize("datatype", ['n', 'd', 's', 'b', 'f', 'e']) +def test_null(dummy_cell, datatype): + cell = dummy_cell + cell.data_type = datatype + assert cell.data_type == datatype + cell.value = None + assert cell.data_type == 'n' + + +@pytest.mark.parametrize("value", ['hello', ".", '0800']) +def test_string(dummy_cell, value): + cell = dummy_cell + cell.value = 'hello' + assert cell.data_type == 's' + + +@pytest.mark.parametrize("value", ['=42', '=if(A1<4;-1;1)']) +def test_formula(dummy_cell, value): + cell = dummy_cell + cell.value = value + assert cell.data_type == 'f' + + +def test_not_formula(dummy_cell): + dummy_cell.value = "=" + assert dummy_cell.data_type == 's' + assert dummy_cell.value == "=" + + +@pytest.mark.parametrize("value", [True, False]) +def test_boolean(dummy_cell, value): + cell = dummy_cell + cell.value = value + assert cell.data_type == 'b' + + +@pytest.mark.parametrize("error_string", ERROR_CODES) +def test_error_codes(dummy_cell, error_string): + cell = dummy_cell + cell.value = error_string + assert cell.data_type == 'e' + + +@pytest.mark.parametrize("value, number_format", + [ + ( + datetime(2010, 7, 13, 6, 37, 41), + "yyyy-mm-dd h:mm:ss" + ), + ( + date(2010, 7, 13), + "yyyy-mm-dd" + ), + ( + time(1, 3), + "h:mm:ss", + ) + ] + ) +def test_insert_date(dummy_cell, value, number_format): + cell = dummy_cell + cell.value = value + assert cell.data_type == 'd' + assert cell.is_date + assert cell.number_format == number_format + + +@pytest.mark.parametrize("value, is_date", + [ + (None, True,), + ("testme", False), + (True, False), + ] + ) +def test_cell_formatted_as_date(dummy_cell, value, is_date): + cell = dummy_cell + cell.value = datetime.today() + cell.value = value + assert cell.is_date == is_date + assert cell.value == value + + +def test_set_bad_type(dummy_cell): + cell = dummy_cell + with pytest.raises(ValueError): + cell.set_explicit_value(1, 'q') + + +def test_illegal_characters(dummy_cell): + from openpyxl.utils.exceptions import IllegalCharacterError + from openpyxl.compat import range + from itertools import chain + cell = dummy_cell + + # The bytes 0x00 through 0x1F inclusive must be manually escaped in values. + + illegal_chrs = chain(range(9), range(11, 13), range(14, 32)) + for i in illegal_chrs: + with pytest.raises(IllegalCharacterError): + cell.value = chr(i) + + with pytest.raises(IllegalCharacterError): + cell.value = "A {0} B".format(chr(i)) + + cell.value = chr(33) + cell.value = chr(9) # Tab + cell.value = chr(10) # Newline + cell.value = chr(13) # Carriage return + cell.value = " Leading and trailing spaces are legal " + + +values = ( + ('30:33.865633336', [('', '', '', '30', '33', '865633')]), + ('03:40:16', [('03', '40', '16', '', '', '')]), + ('03:40', [('03', '40', '', '', '', '')]), + ('55:72:12', []), + ) +@pytest.mark.parametrize("value, expected", + values) +def test_time_regex(value, expected): + from openpyxl.cell.cell import TIME_REGEX + m = TIME_REGEX.findall(value) + assert m == expected + + +#def test_timedelta(dummy_cell): + #cell = dummy_cell + #cell.value = timedelta(days=1, hours=3) + #assert cell.value == 1.125 + #assert cell.data_type == 'n' + #assert cell.is_date is False + #assert cell.number_format == "[hh]:mm:ss" + + +def test_repr(dummy_cell): + cell = dummy_cell + assert repr(cell) == "" + + +def test_repr_object(dummy_cell): + + class Dummy: + + def __str__(self): + return "something" + + cell = dummy_cell + try: + cell._bind_value(Dummy()) + except ValueError as err: + assert "something" not in str(err) + + +def test_comment_assignment(dummy_cell): + assert dummy_cell.comment is None + comm = Comment("text", "author") + dummy_cell.comment = comm + assert dummy_cell.comment == comm + + +def test_only_one_cell_per_comment(dummy_cell): + ws = dummy_cell.parent + comm = Comment('text', 'author') + dummy_cell.comment = comm + + c2 = ws.cell(column=1, row=2) + c2.comment = comm + assert c2.comment.parent is c2 + + +def test_remove_comment(dummy_cell): + comm = Comment('text', 'author') + dummy_cell.comment = comm + dummy_cell.comment = None + assert dummy_cell.comment is None + + +def test_cell_offset(dummy_cell): + cell = dummy_cell + assert cell.offset(2, 1).coordinate == 'B3' + + +class TestEncoding: + + try: + # Python 2 + pound = unichr(163) + except NameError: + # Python 3 + pound = chr(163) + test_string = ('Compound Value (' + pound + ')').encode('latin1') + + def test_bad_encoding(self): + from openpyxl import Workbook + + wb = Workbook() + ws = wb.active + cell = ws['A1'] + with pytest.raises(UnicodeDecodeError): + cell.check_string(self.test_string) + with pytest.raises(UnicodeDecodeError): + cell.value = self.test_string + + def test_good_encoding(self): + from openpyxl import Workbook + + wb = Workbook() + wb.encoding = 'latin1' + ws = wb.active + cell = ws['A1'] + cell.value = self.test_string + + +def test_font(DummyWorksheet, Cell): + from openpyxl.styles import Font + font = Font(bold=True) + ws = DummyWorksheet + ws.parent._fonts.add(font) + + cell = Cell(ws, column='A', row=1) + assert cell.font == font + + +def test_fill(DummyWorksheet, Cell): + from openpyxl.styles import PatternFill + fill = PatternFill(patternType="solid", fgColor="FF0000") + ws = DummyWorksheet + ws.parent._fills.add(fill) + + cell = Cell(ws, column='A', row=1) + assert cell.fill == fill + + +def test_border(DummyWorksheet, Cell): + from openpyxl.styles import Border + border = Border() + ws = DummyWorksheet + ws.parent._borders.add(border) + + cell = Cell(ws, column='A', row=1) + assert cell.border == border + + +def test_number_format(DummyWorksheet, Cell): + ws = DummyWorksheet + ws.parent._number_formats.add("dd--hh--mm") + + cell = Cell(ws, column="A", row=1) + cell.number_format = "dd--hh--mm" + assert cell.number_format == "dd--hh--mm" + + +def test_alignment(DummyWorksheet, Cell): + from openpyxl.styles import Alignment + align = Alignment(wrapText=True) + ws = DummyWorksheet + ws.parent._alignments.add(align) + + cell = Cell(ws, column="A", row=1) + assert cell.alignment == align + + +def test_protection(DummyWorksheet, Cell): + from openpyxl.styles import Protection + prot = Protection(locked=False) + ws = DummyWorksheet + ws.parent._protections.add(prot) + + cell = Cell(ws, column="A", row=1) + assert cell.protection == prot + + +def test_pivot_button(DummyWorksheet, Cell): + ws = DummyWorksheet + + cell = Cell(ws, column="A", row=1) + cell.style_id + cell._style.pivotButton = 1 + assert cell.pivotButton is True + + +def test_quote_prefix(DummyWorksheet, Cell): + ws = DummyWorksheet + + cell = Cell(ws, column="A", row=1) + cell.style_id + cell._style.quotePrefix = 1 + assert cell.quotePrefix is True + + +def test_remove_hyperlink(dummy_cell): + """Remove a cell hyperlink""" + cell = dummy_cell + cell.hyperlink = "http://test.com" + cell.hyperlink = None + assert cell.hyperlink is None diff --git a/openpyxl/cell/tests/test_read_only.py b/openpyxl/cell/tests/test_read_only.py new file mode 100644 index 0000000..8f04064 --- /dev/null +++ b/openpyxl/cell/tests/test_read_only.py @@ -0,0 +1,158 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import datetime +import pytest + +from openpyxl.cell.read_only import ReadOnlyCell +from openpyxl.utils.indexed_list import IndexedList +from openpyxl.styles.styleable import StyleArray + + +@pytest.fixture(scope='module') +def dummy_sheet(): + class DummyWorkbook(object): + shared_styles = IndexedList() + style = StyleArray() + shared_styles.add(style) # Workbooks always have a default style + _cell_styles = IndexedList() + _cell_styles.add(style) + _number_formats = IndexedList() + _fonts = IndexedList() + _fonts.add(None) + + + class DummySheet(object): + base_date = 2415018.5 + style_table = {} + shared_strings = ['Hello world'] + parent = DummyWorkbook() + return DummySheet() + + +def test_ctor(dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, '10', 'n') + assert cell.value == 10 + + +def test_empty_cell(dummy_sheet): + from openpyxl.cell.read_only import EMPTY_CELL + assert EMPTY_CELL.value is None + assert EMPTY_CELL.data_type == 'n' + + +def test_base_date(dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, '10', 'n') + assert cell.base_date == 2415018.5 + + +def test_string_table(dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, '0', 's') + assert cell.shared_strings == ['Hello world'] + assert cell.value == 'Hello world' + + +def test_coordinate(dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, 1, 1, 10, None) + assert cell.coordinate == "A1" + + +@pytest.mark.parametrize("value, expected", + [ + ('1', True), + ('0', False), + ]) +def test_bool(dummy_sheet, value, expected): + cell = ReadOnlyCell(dummy_sheet, None, None, value, 'b') + assert cell.value is expected + + +def test_inline_String(dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, "Hello World!", 'inlineStr') + assert cell.value == "Hello World!" + + +@pytest.mark.parametrize("value, expected", + [ + ("24555", 24555), + ("1.5", 1.5), + (None, None), + ]) +def test_numeric(dummy_sheet, value, expected): + cell = ReadOnlyCell(dummy_sheet, None, None, value, 'n') + v = cell.value + assert v == expected + assert hasattr(v, 'is_integer') == hasattr(expected, 'is_integer'),\ + "Expected {0}, {1}".format(type(expected), type(v)) + + +@pytest.fixture(scope="class") +def DummyCell(dummy_sheet): + + dummy_sheet.parent._number_formats.add('d-mmm-yy') + style = StyleArray([0,0,0,164,0,0,0,0,0]) + dummy_sheet.parent._cell_styles.add(style) + cell = ReadOnlyCell(dummy_sheet, None, None, "23596", 'n', 1) + return cell + + +class TestDateTime: + + def test_number_format(self, DummyCell): + assert DummyCell.number_format == 'd-mmm-yy' + + def test_is_date(self, DummyCell): + assert DummyCell.is_date is True + + def test_conversion(self, DummyCell): + assert DummyCell.value == datetime.datetime(1964, 8, 7, 0, 0, 0) + + def test_interal_value(self, DummyCell): + assert DummyCell.internal_value == 23596 + + +class TestStyle: + + def test_style_array(self, dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, None) + assert cell.style_array == StyleArray() + + def test_font(self, dummy_sheet): + cell = ReadOnlyCell(dummy_sheet, None, None, None) + assert cell.font == None + + +def test_read_only(dummy_sheet): + cell = ReadOnlyCell(sheet=dummy_sheet, row=None, column=None, value='1') + assert cell.value == 1 + with pytest.raises(AttributeError): + cell.value = 10 + with pytest.raises(AttributeError): + cell.style = 1 + + +def test_equality(): + c1 = ReadOnlyCell(None, None, 10, None) + c2 = ReadOnlyCell(None, None, 10, None) + assert c1 is not c2 + assert c1 == c2 + c3 = ReadOnlyCell(None, None, 5, None) + assert c3 != c1 + + +@pytest.mark.parametrize("value, expected", + [ + ('4.2', 4.2), + ('-42.000', -42), + ('0', 0), + ('0.9999', 0.9999), + ('99E-02', 0.99), + ('4', 4), + ('-1E3', -1000), + ('1E-3', 0.001), + ('2e+2', 200.0), + ] + ) +def test_number_convesion(value, expected): + from .. read_only import _cast_number + assert _cast_number(value) == expected diff --git a/openpyxl/cell/tests/test_text.py b/openpyxl/cell/tests/test_text.py new file mode 100644 index 0000000..68b1ae5 --- /dev/null +++ b/openpyxl/cell/tests/test_text.py @@ -0,0 +1,184 @@ +from __future__ import absolute_import +# coding=utf8 +# Copyright (c) 2010-2018 openpyxl +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def InlineFont(): + from ..text import InlineFont + return InlineFont + + +class TestInlineFont: + + def test_ctor(self, InlineFont): + font = InlineFont() + xml = tostring(font.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, InlineFont): + src = """ + + """ + node = fromstring(src) + font = InlineFont.from_tree(node) + assert font == InlineFont() + + +@pytest.fixture +def RichText(): + from ..text import RichText + return RichText + + +class TestRichText: + + def test_ctor(self, RichText): + text = RichText() + xml = tostring(text.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, RichText): + src = """ + + """ + node = fromstring(src) + text = RichText.from_tree(node) + assert text == RichText() + + +@pytest.fixture +def Text(): + from ..text import Text + return Text + + +class TestText: + + def test_ctor(self, Text): + text = Text() + text.plain = "comment" + xml = tostring(text.to_tree()) + expected = """ + + comment + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + @pytest.mark.parametrize("src, expected", + [ + ("""ID""", "ID"), + (""" + + + + 11 de September de 2014 + + + """, + "11 de September de 2014" + ), + ] + ) + def test_from_xml(self, Text, src, expected): + node = fromstring(src) + text = Text.from_tree(node) + assert text.content == expected + + + def test_empty_element(self, Text): + src = """ + + + Replaced Data + + + + + + + + + + + + + """ + node = fromstring(src) + text = Text.from_tree(node) + assert text.content == "Replaced Data" + + +@pytest.fixture +def PhoneticText(): + from ..text import PhoneticText + return PhoneticText + + +class TestPhoneticText: + + def test_ctor(self, PhoneticText): + text = PhoneticText(sb=9, eb=10, t=u'\u3088') + xml = tostring(text.to_tree()) + expected = b""" + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PhoneticText): + src = b""" + + + + """ + node = fromstring(src) + text = PhoneticText.from_tree(node) + assert text == PhoneticText(sb=9, eb=10, t=u'\u3088') + + +@pytest.fixture +def PhoneticProperties(): + from ..text import PhoneticProperties + return PhoneticProperties + + +class TestPhoneticProperties: + + def test_ctor(self, PhoneticProperties): + props = PhoneticProperties(fontId=0, type="Hiragana") + xml = tostring(props.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PhoneticProperties): + src = """ + + """ + node = fromstring(src) + props = PhoneticProperties.from_tree(node) + assert props == PhoneticProperties(fontId=0, type="noConversion") diff --git a/openpyxl/cell/text.py b/openpyxl/cell/text.py new file mode 100644 index 0000000..cbc94d8 --- /dev/null +++ b/openpyxl/cell/text.py @@ -0,0 +1,186 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +""" +Richtext definition +""" +from openpyxl.compat import unicode + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Alias, + Typed, + Integer, + Set, + NoneSet, + Bool, + String, + Sequence, +) +from openpyxl.descriptors.nested import ( + NestedBool, + NestedInteger, + NestedString, + NestedText, +) +from openpyxl.styles.fonts import Font + + +class PhoneticProperties(Serialisable): + + tagname = "phoneticPr" + + fontId = Integer() + type = NoneSet(values=(['halfwidthKatakana', 'fullwidthKatakana', + 'Hiragana', 'noConversion'])) + alignment = NoneSet(values=(['noControl', 'left', 'center', 'distributed'])) + + def __init__(self, + fontId=None, + type=None, + alignment=None, + ): + self.fontId = fontId + self.type = type + self.alignment = alignment + + +class PhoneticText(Serialisable): + + tagname = "rPh" + + sb = Integer() + eb = Integer() + t = NestedText(expected_type=unicode) + text = Alias('t') + + def __init__(self, + sb=None, + eb=None, + t=None, + ): + self.sb = sb + self.eb = eb + self.t = t + + +class InlineFont(Font): + + """ + Font for inline text because, yes what you need are different objects with the same elements but different constraints. + """ + + tagname = "RPrElt" + + rFont = NestedString(allow_none=True) + charset = Font.charset + family = Font.family + b =Font.b + i = Font.i + strike = Font.strike + outline = Font.outline + shadow = Font.shadow + condense = Font.condense + extend = Font.extend + color = Font.color + sz = Font.sz + u = Font.u + vertAlign = Font.vertAlign + scheme = Font.scheme + + __elements__ = ('rFont', 'charset', 'family', 'b', 'i', 'strike', + 'outline', 'shadow', 'condense', 'extend', 'color', 'sz', 'u', + 'vertAlign', 'scheme') + + def __init__(self, + rFont=None, + charset=None, + family=None, + b=None, + i=None, + strike=None, + outline=None, + shadow=None, + condense=None, + extend=None, + color=None, + sz=None, + u=None, + vertAlign=None, + scheme=None, + ): + self.rFont = rFont + self.charset = charset + self.family = family + self.b = b + self.i = i + self.strike = strike + self.outline = outline + self.shadow = shadow + self.condense = condense + self.extend = extend + self.color = color + self.sz = sz + self.u = u + self.vertAlign = vertAlign + self.scheme = scheme + + +class RichText(Serialisable): + + tagname = "RElt" + + rPr = Typed(expected_type=InlineFont, allow_none=True) + font = Alias("rPr") + t = NestedText(expected_type=unicode, allow_none=True) + text = Alias("t") + + __elements__ = ('rPr', 't') + + def __init__(self, + rPr=None, + t=None, + ): + self.rPr = rPr + self.t = t + + +class Text(Serialisable): + + tagname = "text" + + t = NestedText(allow_none=True, expected_type=unicode) + plain = Alias("t") + r = Sequence(expected_type=RichText, allow_none=True) + formatted = Alias("r") + rPh = Sequence(expected_type=PhoneticText, allow_none=True) + phonetic = Alias("rPh") + phoneticPr = Typed(expected_type=PhoneticProperties, allow_none=True) + PhoneticProperties = Alias("phoneticPr") + + __elements__ = ('t', 'r', 'rPh', 'phoneticPr') + + def __init__(self, + t=None, + r=(), + rPh=(), + phoneticPr=None, + ): + self.t = t + self.r = r + self.rPh = rPh + self.phoneticPr = phoneticPr + + + @property + def content(self): + """ + Text stripped of all formatting + """ + snippets = [] + if self.plain is not None: + snippets.append(self.plain) + for block in self.formatted: + if block.t is not None: + snippets.append(block.t) + return u"".join(snippets) diff --git a/openpyxl/chart/_3d.py b/openpyxl/chart/_3d.py new file mode 100644 index 0000000..4ad6c0d --- /dev/null +++ b/openpyxl/chart/_3d.py @@ -0,0 +1,103 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors import Typed, Alias +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors.nested import ( + NestedBool, + NestedInteger, + NestedMinMax, +) +from openpyxl.descriptors.excel import ExtensionList +from .marker import PictureOptions +from .shapes import GraphicalProperties + + +class View3D(Serialisable): + + tagname = "view3D" + + rotX = NestedMinMax(min=-90, max=90, allow_none=True) + x_rotation = Alias('rotX') + hPercent = NestedMinMax(min=5, max=500, allow_none=True) + height_percent = Alias('hPercent') + rotY = NestedInteger(min=-90, max=90, allow_none=True) + y_rotation = Alias('rotY') + depthPercent = NestedInteger(allow_none=True) + rAngAx = NestedBool(allow_none=True) + right_angle_axes = Alias('rAngAx') + perspective = NestedInteger(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('rotX', 'hPercent', 'rotY', 'depthPercent', 'rAngAx', + 'perspective',) + + def __init__(self, + rotX=15, + hPercent=None, + rotY=20, + depthPercent=None, + rAngAx=True, + perspective=None, + extLst=None, + ): + self.rotX = rotX + self.hPercent = hPercent + self.rotY = rotY + self.depthPercent = depthPercent + self.rAngAx = rAngAx + self.perspective = perspective + + +class Surface(Serialisable): + + tagname = "surface" + + thickness = NestedInteger(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + pictureOptions = Typed(expected_type=PictureOptions, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('thickness', 'spPr', 'pictureOptions',) + + def __init__(self, + thickness=None, + spPr=None, + pictureOptions=None, + extLst=None, + ): + self.thickness = thickness + self.spPr = spPr + self.pictureOptions = pictureOptions + + +class _3DBase(Serialisable): + + """ + Base class for 3D charts + """ + + view3D = Typed(expected_type=View3D, allow_none=True) + floor = Typed(expected_type=Surface, allow_none=True) + sideWall = Typed(expected_type=Surface, allow_none=True) + backWall = Typed(expected_type=Surface, allow_none=True) + + def __init__(self, + view3D=None, + floor=None, + sideWall=None, + backWall=None + ): + if view3D is None: + view3D = View3D() + self.view3D = view3D + if floor is None: + floor = Surface() + self.floor = floor + if sideWall is None: + sideWall = Surface() + self.sideWall = sideWall + if backWall is None: + backWall = Surface() + self.backWall = backWall diff --git a/openpyxl/chart/__init__.py b/openpyxl/chart/__init__.py new file mode 100644 index 0000000..9d446cd --- /dev/null +++ b/openpyxl/chart/__init__.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from .area_chart import AreaChart, AreaChart3D +from .bar_chart import BarChart, BarChart3D +from .bubble_chart import BubbleChart +from .line_chart import LineChart, LineChart3D +from .pie_chart import ( + PieChart, + PieChart3D, + DoughnutChart, + ProjectedPieChart +) +from .radar_chart import RadarChart +from .scatter_chart import ScatterChart +from .stock_chart import StockChart +from .surface_chart import SurfaceChart, SurfaceChart3D + +from .series_factory import SeriesFactory as Series +from .reference import Reference diff --git a/openpyxl/chart/_chart.py b/openpyxl/chart/_chart.py new file mode 100644 index 0000000..0919524 --- /dev/null +++ b/openpyxl/chart/_chart.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from collections import OrderedDict + +from openpyxl.compat import basestring + +from openpyxl.descriptors import ( + Typed, + Integer, + Alias, + MinMax, + Bool, +) +from openpyxl.descriptors.nested import Nested +from openpyxl.descriptors.sequence import NestedSequence, ValueSequence +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.xml.constants import PACKAGE_CHARTS + +from ._3d import _3DBase +from .data_source import AxDataSource, NumRef +from .layout import Layout +from .legend import Legend +from .reference import Reference +from .series_factory import SeriesFactory +from .series import attribute_mapping +from .shapes import GraphicalProperties +from .title import TitleDescriptor + +class AxId(Serialisable): + + val = Integer() + + def __init__(self, val): + self.val = val + + +def PlotArea(): + from .chartspace import PlotArea + return PlotArea() + + +class ChartBase(Serialisable): + + """ + Base class for all charts + """ + + legend = Typed(expected_type=Legend, allow_none=True) + layout = Typed(expected_type=Layout, allow_none=True) + roundedCorners = Bool(allow_none=True) + axId = ValueSequence(expected_type=int) + visible_cells_only = Bool() + + _series_type = "" + ser = () + series = Alias('ser') + title = TitleDescriptor() + anchor = "E15" # default anchor position + width = 15 # in cm, approx 5 rows + height = 7.5 # in cm, approx 14 rows + _id = 1 + _path = "/xl/charts/chart{0}.xml" + style = MinMax(allow_none=True, min=1, max=48) + mime_type = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + graphical_properties = Typed(expected_type=GraphicalProperties, allow_none=True) + + __elements__ = () + + + def __init__(self, axId=(), **kw): + self._charts = [self] + self.title = None + self.layout = None + self.roundedCorners = None + self.legend = Legend() + self.graphical_properties = None + self.style = None + self.plot_area = PlotArea() + self.axId = axId + super(ChartBase, self).__init__(**kw) + + def __hash__(self): + """ + Just need to check for identity + """ + return id(self) + + def __iadd__(self, other): + """ + Combine the chart with another one + """ + if not isinstance(other, ChartBase): + raise TypeError("Only other charts can be added") + self._charts.append(other) + return self + + + def to_tree(self, namespace=None, tagname=None, idx=None): + self.axId = [id for id in self._axes] + if self.ser is not None: + for s in self.ser: + s.__elements__ = attribute_mapping[self._series_type] + return super(ChartBase, self).to_tree(tagname, idx) + + + def _write(self): + from .chartspace import ChartSpace, ChartContainer + self.plot_area.layout = self.layout + + idx_base = 0 + for chart in self._charts: + if chart not in self.plot_area._charts: + chart.idx_base = idx_base + idx_base += len(chart.series) + self.plot_area._charts = self._charts + + container = ChartContainer(plotArea=self.plot_area, legend=self.legend, title=self.title) + if isinstance(chart, _3DBase): + container.view3D = chart.view3D + container.floor = chart.floor + container.sideWall = chart.sideWall + container.backWall = chart.backWall + container.plotVisOnly = self.visible_cells_only + cs = ChartSpace(chart=container) + cs.style = self.style + cs.roundedCorners = self.roundedCorners + return cs.to_tree() + + + @property + def _axes(self): + x = getattr(self, "x_axis", None) + y = getattr(self, "y_axis", None) + z = getattr(self, "z_axis", None) + return OrderedDict([(axis.axId, axis) for axis in (x, y, z) if axis]) + + + def set_categories(self, labels): + """ + Set the categories / x-axis values + """ + if not isinstance(labels, Reference): + labels = Reference(range_string=labels) + for s in self.ser: + s.cat = AxDataSource(numRef=NumRef(f=labels)) + + + def add_data(self, data, from_rows=False, titles_from_data=False): + """ + Add a range of data in a single pass. + The default is to treat each column as a data series. + """ + if not isinstance(data, Reference): + data = Reference(range_string=data) + + if from_rows: + values = data.rows + + else: + values = data.cols + + for v in values: + range_string = u"{0}!{1}:{2}".format(data.sheetname, v[0], v[-1]) + series = SeriesFactory(range_string, title_from_data=titles_from_data) + self.ser.append(series) + + + def append(self, value): + """Append a data series to the chart""" + l = self.series[:] + l.append(value) + self.series = l + + + @property + def path(self): + return self._path.format(self._id) diff --git a/openpyxl/chart/area_chart.py b/openpyxl/chart/area_chart.py new file mode 100644 index 0000000..7f23199 --- /dev/null +++ b/openpyxl/chart/area_chart.py @@ -0,0 +1,107 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Set, + Bool, + Integer, + Sequence, + Alias, +) + +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedMinMax, + NestedSet, + NestedBool, +) + +from ._chart import ChartBase +from .descriptors import NestedGapAmount +from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines +from .label import DataLabelList +from .series import Series + + +class _AreaChartBase(ChartBase): + + grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked'])) + varyColors = NestedBool(nested=True, allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + dropLines = Typed(expected_type=ChartLines, allow_none=True) + + _series_type = "area" + + __elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines') + + def __init__(self, + grouping="standard", + varyColors=None, + ser=(), + dLbls=None, + dropLines=None, + ): + self.grouping = grouping + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + self.dropLines = dropLines + super(_AreaChartBase, self).__init__() + + +class AreaChart(_AreaChartBase): + + tagname = "areaChart" + + grouping = _AreaChartBase.grouping + varyColors = _AreaChartBase.varyColors + ser = _AreaChartBase.ser + dLbls = _AreaChartBase.dLbls + dropLines = _AreaChartBase.dropLines + + # chart properties actually used by containing classes + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _AreaChartBase.__elements__ + ('axId',) + + def __init__(self, + axId=None, + extLst=None, + **kw + ): + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + super(AreaChart, self).__init__(**kw) + + +class AreaChart3D(AreaChart): + + tagname = "area3DChart" + + grouping = _AreaChartBase.grouping + varyColors = _AreaChartBase.varyColors + ser = _AreaChartBase.ser + dLbls = _AreaChartBase.dLbls + dropLines = _AreaChartBase.dropLines + + gapDepth = NestedGapAmount() + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + z_axis = Typed(expected_type=SeriesAxis, allow_none=True) + + __elements__ = AreaChart.__elements__ + ('gapDepth', ) + + def __init__(self, gapDepth=None, **kw): + self.gapDepth = gapDepth + super(AreaChart3D, self).__init__(**kw) + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + self.z_axis = SeriesAxis() diff --git a/openpyxl/chart/axis.py b/openpyxl/chart/axis.py new file mode 100644 index 0000000..dfd7880 --- /dev/null +++ b/openpyxl/chart/axis.py @@ -0,0 +1,402 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Float, + NoneSet, + Bool, + Integer, + MinMax, + NoneSet, + Set, + String, + Alias, +) + +from openpyxl.descriptors.excel import ( + ExtensionList, + Percentage, + _explicit_none, +) +from openpyxl.descriptors.nested import ( + NestedValue, + NestedSet, + NestedBool, + NestedNoneSet, + NestedFloat, + NestedInteger, + NestedMinMax, +) +from openpyxl.xml.constants import CHART_NS + +from .descriptors import NumberFormatDescriptor +from .layout import Layout +from .text import Text, RichText +from .shapes import GraphicalProperties +from .title import Title, TitleDescriptor + + +class ChartLines(Serialisable): + + tagname = "chartLines" + + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + + def __init__(self, spPr=None): + self.spPr = spPr + + +class Scaling(Serialisable): + + tagname = "scaling" + + logBase = NestedFloat(allow_none=True) + orientation = NestedSet(values=(['maxMin', 'minMax'])) + max = NestedFloat(allow_none=True) + min = NestedFloat(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('logBase', 'orientation', 'max', 'min',) + + def __init__(self, + logBase=None, + orientation="minMax", + max=None, + min=None, + extLst=None, + ): + self.logBase = logBase + self.orientation = orientation + self.max = max + self.min = min + + +class _BaseAxis(Serialisable): + + axId = NestedInteger(expected_type=int) + scaling = Typed(expected_type=Scaling) + delete = NestedBool(allow_none=True) + axPos = NestedSet(values=(['b', 'l', 'r', 't'])) + majorGridlines = Typed(expected_type=ChartLines, allow_none=True) + minorGridlines = Typed(expected_type=ChartLines, allow_none=True) + title = TitleDescriptor() + numFmt = NumberFormatDescriptor() + number_format = Alias("numFmt") + majorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none) + minorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none) + tickLblPos = NestedNoneSet(values=(['high', 'low', 'nextTo'])) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + txPr = Typed(expected_type=RichText, allow_none=True) + textProperties = Alias('txPr') + crossAx = NestedInteger(expected_type=int) # references other axis + crosses = NestedNoneSet(values=(['autoZero', 'max', 'min'])) + crossesAt = NestedFloat(allow_none=True) + + # crosses & crossesAt are mutually exclusive + + __elements__ = ('axId', 'scaling', 'delete', 'axPos', 'majorGridlines', + 'minorGridlines', 'title', 'numFmt', 'majorTickMark', 'minorTickMark', + 'tickLblPos', 'spPr', 'txPr', 'crossAx', 'crosses', 'crossesAt') + + def __init__(self, + axId=None, + scaling=None, + delete=None, + axPos='l', + majorGridlines=None, + minorGridlines=None, + title=None, + numFmt=None, + majorTickMark=None, + minorTickMark=None, + tickLblPos=None, + spPr=None, + txPr= None, + crossAx=None, + crosses=None, + crossesAt=None, + ): + self.axId = axId + if scaling is None: + scaling = Scaling() + self.scaling = scaling + self.delete = delete + self.axPos = axPos + self.majorGridlines = majorGridlines + self.minorGridlines = minorGridlines + self.title = title + self.numFmt = numFmt + self.majorTickMark = majorTickMark + self.minorTickMark = minorTickMark + self.tickLblPos = tickLblPos + self.spPr = spPr + self.txPr = txPr + self.crossAx = crossAx + self.crosses = crosses + self.crossesAt = None + + +class DisplayUnitsLabel(Serialisable): + + tagname = "dispUnitsLbl" + + layout = Typed(expected_type=Layout, allow_none=True) + tx = Typed(expected_type=Text, allow_none=True) + text = Alias("tx") + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + txPr = Typed(expected_type=RichText, allow_none=True) + textPropertes = Alias("txPr") + + __elements__ = ('layout', 'tx', 'spPr', 'txPr') + + def __init__(self, + layout=None, + tx=None, + spPr=None, + txPr=None, + ): + self.layout = layout + self.tx = tx + self.spPr = spPr + self.txPr = txPr + + +class DisplayUnitsLabelList(Serialisable): + + tagname = "dispUnits" + + custUnit = NestedFloat(allow_none=True) + builtInUnit = NestedNoneSet(values=(['hundreds', 'thousands', + 'tenThousands', 'hundredThousands', 'millions', 'tenMillions', + 'hundredMillions', 'billions', 'trillions'])) + dispUnitsLbl = Typed(expected_type=DisplayUnitsLabel, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('custUnit', 'builtInUnit', 'dispUnitsLbl',) + + def __init__(self, + custUnit=None, + builtInUnit=None, + dispUnitsLbl=None, + extLst=None, + ): + self.custUnit = custUnit + self.builtInUnit = builtInUnit + self.dispUnitsLbl = dispUnitsLbl + + +class NumericAxis(_BaseAxis): + + tagname = "valAx" + + axId = _BaseAxis.axId + scaling = _BaseAxis.scaling + delete = _BaseAxis.delete + axPos = _BaseAxis.axPos + majorGridlines = _BaseAxis.majorGridlines + minorGridlines = _BaseAxis.minorGridlines + title = _BaseAxis.title + numFmt = _BaseAxis.numFmt + majorTickMark = _BaseAxis.majorTickMark + minorTickMark = _BaseAxis.minorTickMark + tickLblPos = _BaseAxis.tickLblPos + spPr = _BaseAxis.spPr + txPr = _BaseAxis.txPr + crossAx = _BaseAxis.crossAx + crosses = _BaseAxis.crosses + crossesAt = _BaseAxis.crossesAt + + crossBetween = NestedNoneSet(values=(['between', 'midCat'])) + majorUnit = NestedFloat(allow_none=True) + minorUnit = NestedFloat(allow_none=True) + dispUnits = Typed(expected_type=DisplayUnitsLabelList, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _BaseAxis.__elements__ + ('crossBetween', 'majorUnit', + 'minorUnit', 'dispUnits',) + + + def __init__(self, + crossBetween=None, + majorUnit=None, + minorUnit=None, + dispUnits=None, + extLst=None, + **kw + ): + self.crossBetween = crossBetween + self.majorUnit = majorUnit + self.minorUnit = minorUnit + self.dispUnits = dispUnits + kw.setdefault('majorGridlines', ChartLines()) + kw.setdefault('axId', 100) + kw.setdefault('crossAx', 10) + super(NumericAxis, self).__init__(**kw) + + + @classmethod + def from_tree(cls, node): + """ + Special case value axes with no gridlines + """ + self = super(NumericAxis, cls).from_tree(node) + gridlines = node.find("{%s}majorGridlines" % CHART_NS) + if gridlines is None: + self.majorGridlines = None + return self + + + +class TextAxis(_BaseAxis): + + tagname = "catAx" + + axId = _BaseAxis.axId + scaling = _BaseAxis.scaling + delete = _BaseAxis.delete + axPos = _BaseAxis.axPos + majorGridlines = _BaseAxis.majorGridlines + minorGridlines = _BaseAxis.minorGridlines + title = _BaseAxis.title + numFmt = _BaseAxis.numFmt + majorTickMark = _BaseAxis.majorTickMark + minorTickMark = _BaseAxis.minorTickMark + tickLblPos = _BaseAxis.tickLblPos + spPr = _BaseAxis.spPr + txPr = _BaseAxis.txPr + crossAx = _BaseAxis.crossAx + crosses = _BaseAxis.crosses + crossesAt = _BaseAxis.crossesAt + + auto = NestedBool(allow_none=True) + lblAlgn = NestedNoneSet(values=(['ctr', 'l', 'r'])) + lblOffset = NestedMinMax(min=0, max=1000) + tickLblSkip = NestedInteger(allow_none=True) + tickMarkSkip = NestedInteger(allow_none=True) + noMultiLvlLbl = NestedBool(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _BaseAxis.__elements__ + ('auto', 'lblAlgn', 'lblOffset', + 'tickLblSkip', 'tickMarkSkip', 'noMultiLvlLbl') + + def __init__(self, + auto=None, + lblAlgn=None, + lblOffset=100, + tickLblSkip=None, + tickMarkSkip=None, + noMultiLvlLbl=None, + extLst=None, + **kw + ): + self.auto = auto + self.lblAlgn = lblAlgn + self.lblOffset = lblOffset + self.tickLblSkip = tickLblSkip + self.tickMarkSkip = tickMarkSkip + self.noMultiLvlLbl = noMultiLvlLbl + kw.setdefault('axId', 10) + kw.setdefault('crossAx', 100) + super(TextAxis, self).__init__(**kw) + + +class DateAxis(TextAxis): + + tagname = "dateAx" + + axId = _BaseAxis.axId + scaling = _BaseAxis.scaling + delete = _BaseAxis.delete + axPos = _BaseAxis.axPos + majorGridlines = _BaseAxis.majorGridlines + minorGridlines = _BaseAxis.minorGridlines + title = _BaseAxis.title + numFmt = _BaseAxis.numFmt + majorTickMark = _BaseAxis.majorTickMark + minorTickMark = _BaseAxis.minorTickMark + tickLblPos = _BaseAxis.tickLblPos + spPr = _BaseAxis.spPr + txPr = _BaseAxis.txPr + crossAx = _BaseAxis.crossAx + crosses = _BaseAxis.crosses + crossesAt = _BaseAxis.crossesAt + + auto = NestedBool(allow_none=True) + lblOffset = NestedInteger(allow_none=True) + baseTimeUnit = NestedNoneSet(values=(['days', 'months', 'years'])) + majorUnit = NestedFloat(allow_none=True) + majorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years'])) + minorUnit = NestedFloat(allow_none=True) + minorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years'])) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _BaseAxis.__elements__ + ('auto', 'lblOffset', + 'baseTimeUnit', 'majorUnit', 'majorTimeUnit', 'minorUnit', + 'minorTimeUnit') + + def __init__(self, + auto=None, + lblOffset=None, + baseTimeUnit=None, + majorUnit=None, + majorTimeUnit=None, + minorUnit=None, + minorTimeUnit=None, + extLst=None, + **kw + ): + self.auto = auto + self.lblOffset = lblOffset + self.baseTimeUnit = baseTimeUnit + self.majorUnit = majorUnit + self.majorTimeUnit = majorTimeUnit + self.minorUnit = minorUnit + self.minorTimeUnit = minorTimeUnit + kw.setdefault('axId', 500) + kw.setdefault('lblOffset', lblOffset) + super(DateAxis, self).__init__(**kw) + + +class SeriesAxis(_BaseAxis): + + tagname = "serAx" + + axId = _BaseAxis.axId + scaling = _BaseAxis.scaling + delete = _BaseAxis.delete + axPos = _BaseAxis.axPos + majorGridlines = _BaseAxis.majorGridlines + minorGridlines = _BaseAxis.minorGridlines + title = _BaseAxis.title + numFmt = _BaseAxis.numFmt + majorTickMark = _BaseAxis.majorTickMark + minorTickMark = _BaseAxis.minorTickMark + tickLblPos = _BaseAxis.tickLblPos + spPr = _BaseAxis.spPr + txPr = _BaseAxis.txPr + crossAx = _BaseAxis.crossAx + crosses = _BaseAxis.crosses + crossesAt = _BaseAxis.crossesAt + + tickLblSkip = NestedInteger(allow_none=True) + tickMarkSkip = NestedInteger(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _BaseAxis.__elements__ + ('tickLblSkip', 'tickMarkSkip') + + def __init__(self, + tickLblSkip=None, + tickMarkSkip=None, + extLst=None, + **kw + ): + self.tickLblSkip = tickLblSkip + self.tickMarkSkip = tickMarkSkip + kw.setdefault('axId', 1000) + kw.setdefault('crossAx', 10) + super(SeriesAxis, self).__init__(**kw) diff --git a/openpyxl/chart/bar_chart.py b/openpyxl/chart/bar_chart.py new file mode 100644 index 0000000..7e88fad --- /dev/null +++ b/openpyxl/chart/bar_chart.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Bool, + Integer, + Sequence, + Alias, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedSet, + NestedBool, + NestedInteger, + NestedMinMax, +) + +from .descriptors import ( + NestedGapAmount, + NestedOverlap, +) +from ._chart import ChartBase +from ._3d import _3DBase +from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines +from .shapes import GraphicalProperties +from .series import Series +from .legend import Legend +from .label import DataLabelList + + +class _BarChartBase(ChartBase): + + barDir = NestedSet(values=(['bar', 'col'])) + type = Alias("barDir") + grouping = NestedSet(values=(['percentStacked', 'clustered', 'standard', + 'stacked'])) + varyColors = NestedBool(nested=True, allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + + __elements__ = ('barDir', 'grouping', 'varyColors', 'ser', 'dLbls') + + _series_type = "bar" + + def __init__(self, + barDir="col", + grouping="clustered", + varyColors=None, + ser=(), + dLbls=None, + **kw + ): + self.barDir = barDir + self.grouping = grouping + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + super(_BarChartBase, self).__init__(**kw) + + +class BarChart(_BarChartBase): + + tagname = "barChart" + + barDir = _BarChartBase.barDir + grouping = _BarChartBase.grouping + varyColors = _BarChartBase.varyColors + ser = _BarChartBase.ser + dLbls = _BarChartBase.dLbls + + gapWidth = NestedGapAmount() + overlap = NestedOverlap() + serLines = Typed(expected_type=ChartLines, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + # chart properties actually used by containing classes + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + + __elements__ = _BarChartBase.__elements__ + ('gapWidth', 'overlap', 'serLines', 'axId') + + def __init__(self, + gapWidth=150, + overlap=None, + serLines=None, + extLst=None, + **kw + ): + self.gapWidth = gapWidth + self.overlap = overlap + self.serLines = serLines + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + self.legend = Legend() + super(BarChart, self).__init__(**kw) + + +class BarChart3D(_BarChartBase, _3DBase): + + tagname = "bar3DChart" + + barDir = _BarChartBase.barDir + grouping = _BarChartBase.grouping + varyColors = _BarChartBase.varyColors + ser = _BarChartBase.ser + dLbls = _BarChartBase.dLbls + + view3D = _3DBase.view3D + floor = _3DBase.floor + sideWall = _3DBase.sideWall + backWall = _3DBase.backWall + + gapWidth = NestedGapAmount() + gapDepth = NestedGapAmount() + shape = NestedNoneSet(values=(['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax'])) + serLines = Typed(expected_type=ChartLines, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + z_axis = Typed(expected_type=SeriesAxis, allow_none=True) + + __elements__ = _BarChartBase.__elements__ + ('gapWidth', 'gapDepth', 'shape', 'serLines', 'axId') + + def __init__(self, + gapWidth=150, + gapDepth=150, + shape=None, + serLines=None, + extLst=None, + **kw + ): + self.gapWidth = gapWidth + self.gapDepth = gapDepth + self.shape = shape + self.serLines = serLines + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + self.z_axis = SeriesAxis() + + super(BarChart3D, self).__init__(**kw) diff --git a/openpyxl/chart/bubble_chart.py b/openpyxl/chart/bubble_chart.py new file mode 100644 index 0000000..e07143d --- /dev/null +++ b/openpyxl/chart/bubble_chart.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import +#Autogenerated schema +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Set, + MinMax, + Bool, + Integer, + Alias, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedMinMax, + NestedBool, +) + +from ._chart import ChartBase +from .axis import TextAxis, NumericAxis +from .series import XYSeries +from .label import DataLabelList + + +class BubbleChart(ChartBase): + + tagname = "bubbleChart" + + varyColors = NestedBool(allow_none=True) + ser = Sequence(expected_type=XYSeries, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + bubble3D = NestedBool(allow_none=True) + bubbleScale = NestedMinMax(min=0, max=300, allow_none=True) + showNegBubbles = NestedBool(allow_none=True) + sizeRepresents = NestedNoneSet(values=(['area', 'w'])) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=NumericAxis) + y_axis = Typed(expected_type=NumericAxis) + + _series_type = "bubble" + + __elements__ = ('varyColors', 'ser', 'dLbls', 'bubble3D', 'bubbleScale', + 'showNegBubbles', 'sizeRepresents', 'axId') + + def __init__(self, + varyColors=None, + ser=(), + dLbls=None, + bubble3D=None, + bubbleScale=None, + showNegBubbles=None, + sizeRepresents=None, + extLst=None, + **kw + ): + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + self.bubble3D = bubble3D + self.bubbleScale = bubbleScale + self.showNegBubbles = showNegBubbles + self.sizeRepresents = sizeRepresents + self.x_axis = NumericAxis(axId=10, crossAx=20) + self.y_axis = NumericAxis(axId=20, crossAx=10) + super(BubbleChart, self).__init__(**kw) diff --git a/openpyxl/chart/chartspace.py b/openpyxl/chart/chartspace.py new file mode 100644 index 0000000..6643899 --- /dev/null +++ b/openpyxl/chart/chartspace.py @@ -0,0 +1,268 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +""" +Enclosing chart object. The various chart types are actually child objects. +Will probably need to call this indirectly +""" + +from openpyxl.compat import unicode + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Bool, + Float, + Typed, + MinMax, + Integer, + NoneSet, + String, + Alias, + Sequence, + Typed, +) +from openpyxl.descriptors.excel import ( + Percentage, + ExtensionList, + Relation +) +from openpyxl.descriptors.nested import ( + NestedBool, + NestedNoneSet, + NestedInteger, + NestedString, + NestedMinMax, + NestedText, +) +from openpyxl.xml.constants import CHART_NS + +from openpyxl.drawing.colors import ColorMapping +from .text import Text, RichText +from .shapes import GraphicalProperties +from .legend import Legend +from .marker import PictureOptions, Marker +from .label import DataLabel +from ._3d import _3DBase, View3D +from .plotarea import PlotArea +from .title import Title +from .print_settings import PrintSettings + + +class PivotFormat(Serialisable): + + tagname = "pivotFmt" + + idx = NestedInteger(nested=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + txPr = Typed(expected_type=RichText, allow_none=True) + TextBody = Alias("txPr") + marker = Typed(expected_type=Marker, allow_none=True) + dLbl = Typed(expected_type=DataLabel, allow_none=True) + DataLabel = Alias("dLbl") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('idx', 'spPr', 'txPr', 'marker', 'dLbl') + + def __init__(self, + idx=0, + spPr=None, + txPr=None, + marker=None, + dLbl=None, + extLst=None, + ): + self.idx = idx + self.spPr = spPr + self.txPr = txPr + self.marker = marker + self.dLbl = dLbl + + +class PivotFormatList(Serialisable): + + tagname = "pivotFmts" + + pivotFmt = Sequence(expected_type=PivotFormat, allow_none=True) + + __elements__ = ('pivotFmt',) + + def __init__(self, + pivotFmt=(), + ): + self.pivotFmt = pivotFmt + + +class ChartContainer(Serialisable): + + tagname = "chart" + + title = Typed(expected_type=Title, allow_none=True) + autoTitleDeleted = NestedBool(allow_none=True) + pivotFmts = Typed(expected_type=PivotFormatList, allow_none=True) + view3D = _3DBase.view3D + floor = _3DBase.floor + sideWall = _3DBase.sideWall + backWall = _3DBase.backWall + plotArea = Typed(expected_type=PlotArea, ) + legend = Typed(expected_type=Legend, allow_none=True) + plotVisOnly = NestedBool() + dispBlanksAs = NestedNoneSet(values=(['span', 'gap', 'zero'])) + showDLblsOverMax = NestedBool(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('title', 'autoTitleDeleted', 'pivotFmts', 'view3D', + 'floor', 'sideWall', 'backWall', 'plotArea', 'legend', 'plotVisOnly', + 'dispBlanksAs', 'showDLblsOverMax') + + def __init__(self, + title=None, + autoTitleDeleted=None, + pivotFmts=None, + view3D=None, + floor=None, + sideWall=None, + backWall=None, + plotArea=None, + legend=None, + plotVisOnly=True, + dispBlanksAs="gap", + showDLblsOverMax=None, + extLst=None, + ): + self.title = title + self.autoTitleDeleted = autoTitleDeleted + self.pivotFmts = pivotFmts + self.view3D = view3D + self.floor = floor + self.sideWall = sideWall + self.backWall = backWall + if plotArea is None: + plotArea = PlotArea() + self.plotArea = plotArea + self.legend = legend + self.plotVisOnly = plotVisOnly + self.dispBlanksAs = dispBlanksAs + self.showDLblsOverMax = showDLblsOverMax + + +class Protection(Serialisable): + + tagname = "protection" + + chartObject = NestedBool(allow_none=True) + data = NestedBool(allow_none=True) + formatting = NestedBool(allow_none=True) + selection = NestedBool(allow_none=True) + userInterface = NestedBool(allow_none=True) + + __elements__ = ("chartObject", "data", "formatting", "selection", "userInterface") + + def __init__(self, + chartObject=None, + data=None, + formatting=None, + selection=None, + userInterface=None, + ): + self.chartObject = chartObject + self.data = data + self.formatting = formatting + self.selection = selection + self.userInterface = userInterface + + +class PivotSource(Serialisable): + + tagname = "pivotSource" + + name = NestedText(expected_type=unicode) + fmtId = NestedInteger(expected_type=int) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('name', 'fmtId') + + def __init__(self, + name=None, + fmtId=None, + extLst=None, + ): + self.name = name + self.fmtId = fmtId + + +class ExternalData(Serialisable): + + tagname = "externalData" + + autoUpdate = NestedBool(allow_none=True) + id = String() # Needs namespace + + def __init__(self, + autoUpdate=None, + id=None + ): + self.autoUpdate = autoUpdate + self.id = id + + +class ChartSpace(Serialisable): + + tagname = "chartSpace" + + date1904 = NestedBool(allow_none=True) + lang = NestedString(allow_none=True) + roundedCorners = NestedBool(allow_none=True) + style = NestedMinMax(allow_none=True, min=1, max=48) + clrMapOvr = Typed(expected_type=ColorMapping, allow_none=True) + pivotSource = Typed(expected_type=PivotSource, allow_none=True) + protection = Typed(expected_type=Protection, allow_none=True) + chart = Typed(expected_type=ChartContainer) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + txPr = Typed(expected_type=RichText, allow_none=True) + textProperties = Alias("txPr") + externalData = Typed(expected_type=ExternalData, allow_none=True) + printSettings = Typed(expected_type=PrintSettings, allow_none=True) + userShapes = Relation() + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('date1904', 'lang', 'roundedCorners', 'style', + 'clrMapOvr', 'pivotSource', 'protection', 'chart', 'spPr', 'txPr', + 'externalData', 'printSettings', 'userShapes') + + def __init__(self, + date1904=None, + lang=None, + roundedCorners=None, + style=None, + clrMapOvr=None, + pivotSource=None, + protection=None, + chart=None, + spPr=None, + txPr=None, + externalData=None, + printSettings=None, + userShapes=None, + extLst=None, + ): + self.date1904 = date1904 + self.lang = lang + self.roundedCorners = roundedCorners + self.style = style + self.clrMapOvr = clrMapOvr + self.pivotSource = pivotSource + self.protection = protection + self.chart = chart + self.spPr = spPr + self.txPr = txPr + self.externalData = externalData + self.printSettings = printSettings + self.userShapes = userShapes + + + def to_tree(self, tagname=None, idx=None, namespace=None): + tree = super(ChartSpace, self).to_tree() + tree.set("xmlns", CHART_NS) + return tree diff --git a/openpyxl/chart/data_source.py b/openpyxl/chart/data_source.py new file mode 100644 index 0000000..67cd8c0 --- /dev/null +++ b/openpyxl/chart/data_source.py @@ -0,0 +1,188 @@ +""" +Collection of utility primitives for charts. +""" + +from openpyxl.compat import unicode +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Bool, + Typed, + Alias, + String, + Integer, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedString, + NestedText, + NestedInteger, +) + + +class NumFmt(Serialisable): + + formatCode = String() + sourceLinked = Bool() + + def __init__(self, + formatCode=None, + sourceLinked=False + ): + self.formatCode = formatCode + self.sourceLinked = sourceLinked + + +class NumberValueDescriptor(NestedText): + """ + Data should be numerical but isn't always :-/ + """ + + allow_none = True + + def __set__(self, instance, value): + if value == "#N/A": + self.expected_type = unicode + else: + self.expected_type = float + super(NumberValueDescriptor, self).__set__(instance, value) + + +class NumVal(Serialisable): + + idx = Integer() + formatCode = NestedText(allow_none=True, expected_type=unicode) + v = NumberValueDescriptor() + + def __init__(self, + idx=None, + formatCode=None, + v=None, + ): + self.idx = idx + self.formatCode = formatCode + self.v = v + + +class NumData(Serialisable): + + formatCode = NestedText(expected_type=unicode, allow_none=True) + ptCount = NestedInteger(allow_none=True) + pt = Sequence(expected_type=NumVal) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('formatCode', 'ptCount', 'pt') + + def __init__(self, + formatCode=None, + ptCount=None, + pt=(), + extLst=None, + ): + self.formatCode = formatCode + self.ptCount = ptCount + self.pt = pt + + +class NumRef(Serialisable): + + f = NestedText(expected_type=unicode) + ref = Alias('f') + numCache = Typed(expected_type=NumData, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('f', 'numCache') + + def __init__(self, + f=None, + numCache=None, + extLst=None, + ): + self.f = f + self.numCache = numCache + + +class StrVal(Serialisable): + + tagname = "strVal" + + idx = Integer() + v = NestedText(expected_type=unicode) + + def __init__(self, + idx=0, + v=None, + ): + self.idx = idx + self.v = v + + +class StrData(Serialisable): + + tagname = "strData" + + ptCount = NestedInteger(allow_none=True) + pt = Sequence(expected_type=StrVal) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('ptCount', 'pt') + + def __init__(self, + ptCount=None, + pt=(), + extLst=None, + ): + self.ptCount = ptCount + self.pt = pt + + +class StrRef(Serialisable): + + tagname = "strRef" + + f = NestedText(expected_type=unicode, allow_none=True) + strCache = Typed(expected_type=StrData, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('f', 'strCache') + + def __init__(self, + f=None, + strCache=None, + extLst=None, + ): + self.f = f + self.strCache = strCache + + +class NumDataSource(Serialisable): + + numRef = Typed(expected_type=NumRef, allow_none=True) + numLit = Typed(expected_type=NumData, allow_none=True) + + + def __init__(self, + numRef=None, + numLit=None, + ): + self.numRef = numRef + self.numLit = numLit + + +class AxDataSource(Serialisable): + + numRef = Typed(expected_type=NumRef, allow_none=True) + numLit = Typed(expected_type=NumData, allow_none=True) + strRef = Typed(expected_type=StrRef, allow_none=True) + strLit = Typed(expected_type=StrData, allow_none=True) + + def __init__(self, + numRef=None, + numLit=None, + strRef=None, + strLit=None, + ): + self.numRef = numRef + self.numLit = numLit + self.strRef = strRef + self.strLit = strLit diff --git a/openpyxl/chart/descriptors.py b/openpyxl/chart/descriptors.py new file mode 100644 index 0000000..c512004 --- /dev/null +++ b/openpyxl/chart/descriptors.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.compat import basestring + +from openpyxl.descriptors.nested import ( + NestedMinMax + ) + +from openpyxl.descriptors import Typed + +from .data_source import NumFmt + +""" +Utility descriptors for the chart module. +For convenience but also clarity. +""" + +class NestedGapAmount(NestedMinMax): + + allow_none = True + min = 0 + max = 500 + + +class NestedOverlap(NestedMinMax): + + allow_none = True + min = -100 + max = 100 + + +class NumberFormatDescriptor(Typed): + """ + Allow direct assignment of format code + """ + + expected_type = NumFmt + allow_none = True + + def __set__(self, instance, value): + if isinstance(value, basestring): + value = NumFmt(value) + super(NumberFormatDescriptor, self).__set__(instance, value) diff --git a/openpyxl/chart/error_bar.py b/openpyxl/chart/error_bar.py new file mode 100644 index 0000000..7818561 --- /dev/null +++ b/openpyxl/chart/error_bar.py @@ -0,0 +1,60 @@ +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Float, + Set, + Alias +) + +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedSet, + NestedBool, + NestedFloat, +) + +from .data_source import NumDataSource +from .shapes import GraphicalProperties + + +class ErrorBars(Serialisable): + + tagname = "errBars" + + errDir = NestedNoneSet(values=(['x', 'y'])) + direction = Alias("errDir") + errBarType = NestedSet(values=(['both', 'minus', 'plus'])) + style = Alias("errBarType") + errValType = NestedSet(values=(['cust', 'fixedVal', 'percentage', 'stdDev', 'stdErr'])) + size = Alias("errValType") + noEndCap = NestedBool(nested=True, allow_none=True) + plus = Typed(expected_type=NumDataSource, allow_none=True) + minus = Typed(expected_type=NumDataSource, allow_none=True) + val = NestedFloat(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('errDir','errBarType', 'errValType', 'noEndCap','minus', 'plus', 'val', 'spPr') + + + def __init__(self, + errDir=None, + errBarType="both", + errValType="fixedVal", + noEndCap=None, + plus=None, + minus=None, + val=None, + spPr=None, + extLst=None, + ): + self.errDir = errDir + self.errBarType = errBarType + self.errValType = errValType + self.noEndCap = noEndCap + self.plus = plus + self.minus = minus + self.val = val + self.spPr = spPr diff --git a/openpyxl/chart/label.py b/openpyxl/chart/label.py new file mode 100644 index 0000000..5a27c97 --- /dev/null +++ b/openpyxl/chart/label.py @@ -0,0 +1,130 @@ +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + String, + Integer, + Bool, + Set, + Float, + Sequence, + Alias +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedBool, + NestedString, + NestedInteger, + ) + +from .shapes import GraphicalProperties +from .text import RichText + + +class _DataLabelBase(Serialisable): + + numFmt = NestedString(allow_none=True, attribute="formatCode") + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + txPr = Typed(expected_type=RichText, allow_none=True) + textProperties = Alias('txPr') + dLblPos = NestedNoneSet(values=['bestFit', 'b', 'ctr', 'inBase', 'inEnd', + 'l', 'outEnd', 'r', 't']) + position = Alias('dLblPos') + showLegendKey = NestedBool(allow_none=True) + showVal = NestedBool(allow_none=True) + showCatName = NestedBool(allow_none=True) + showSerName = NestedBool(allow_none=True) + showPercent = NestedBool(allow_none=True) + showBubbleSize = NestedBool(allow_none=True) + showLeaderLines = NestedBool(allow_none=True) + separator = NestedString(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ("numFmt", "spPr", "txPr", "dLblPos", "showLegendKey", + "showVal", "showCatName", "showSerName", "showPercent", "showBubbleSize", + "showLeaderLines", "separator") + + def __init__(self, + numFmt=None, + spPr=None, + txPr=None, + dLblPos=None, + showLegendKey=None, + showVal=None, + showCatName=None, + showSerName=None, + showPercent=None, + showBubbleSize=None, + showLeaderLines=None, + separator=None, + extLst=None, + ): + self.numFmt = numFmt + self.spPr = spPr + self.txPr = txPr + self.dLblPos = dLblPos + self.showLegendKey = showLegendKey + self.showVal = showVal + self.showCatName = showCatName + self.showSerName = showSerName + self.showPercent = showPercent + self.showBubbleSize = showBubbleSize + self.showLeaderLines = showLeaderLines + self.separator = separator + + +class DataLabel(_DataLabelBase): + + tagname = "dLbl" + + idx = NestedInteger() + + numFmt = _DataLabelBase.numFmt + spPr = _DataLabelBase.spPr + txPr = _DataLabelBase.txPr + dLblPos = _DataLabelBase.dLblPos + showLegendKey = _DataLabelBase.showLegendKey + showVal = _DataLabelBase.showVal + showCatName = _DataLabelBase.showCatName + showSerName = _DataLabelBase.showSerName + showPercent = _DataLabelBase.showPercent + showBubbleSize = _DataLabelBase.showBubbleSize + showLeaderLines = _DataLabelBase.showLeaderLines + separator = _DataLabelBase.separator + extLst = _DataLabelBase.extLst + + __elements__ = ("idx",) + _DataLabelBase.__elements__ + + def __init__(self, idx=0, **kw ): + self.idx = idx + super(DataLabel, self).__init__(**kw) + + +class DataLabelList(_DataLabelBase): + + tagname = "dLbls" + + dLbl = Sequence(expected_type=DataLabel, allow_none=True) + + delete = NestedBool(allow_none=True) + numFmt = _DataLabelBase.numFmt + spPr = _DataLabelBase.spPr + txPr = _DataLabelBase.txPr + dLblPos = _DataLabelBase.dLblPos + showLegendKey = _DataLabelBase.showLegendKey + showVal = _DataLabelBase.showVal + showCatName = _DataLabelBase.showCatName + showSerName = _DataLabelBase.showSerName + showPercent = _DataLabelBase.showPercent + showBubbleSize = _DataLabelBase.showBubbleSize + showLeaderLines = _DataLabelBase.showLeaderLines + separator = _DataLabelBase.separator + extLst = _DataLabelBase.extLst + + __elements__ = ("delete", "dLbl",) + _DataLabelBase.__elements__ + + def __init__(self, dLbl=(), delete=None, **kw): + self.dLbl = dLbl + self.delete = delete + super(DataLabelList, self).__init__(**kw) diff --git a/openpyxl/chart/layout.py b/openpyxl/chart/layout.py new file mode 100644 index 0000000..0fa12e0 --- /dev/null +++ b/openpyxl/chart/layout.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + NoneSet, + Float, + Typed, + Alias, +) + +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedFloat + +) + +class ManualLayout(Serialisable): + + tagname = "manualLayout" + + layoutTarget = NestedNoneSet(values=(['inner', 'outer'])) + xMode = NestedNoneSet(values=(['edge', 'factor'])) + yMode = NestedNoneSet(values=(['edge', 'factor'])) + wMode = NestedNoneSet(values=(['edge', 'factor'])) + hMode = NestedNoneSet(values=(['edge', 'factor'])) + x = NestedFloat(allow_none=True) + y = NestedFloat(allow_none=True) + w = NestedFloat(allow_none=True) + width = Alias('w') + h = NestedFloat(allow_none=True) + height = Alias('h') + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('layoutTarget', 'xMode', 'yMode', 'wMode', 'hMode', 'x', + 'y', 'w', 'h') + + def __init__(self, + layoutTarget=None, + xMode=None, + yMode=None, + wMode=None, + hMode=None, + x=None, + y=None, + w=None, + h=None, + extLst=None, + ): + self.layoutTarget = layoutTarget + self.xMode = xMode + self.yMode = yMode + self.wMode = wMode + self.hMode = hMode + self.x = x + self.y = y + self.w = w + self.h = h + + +class Layout(Serialisable): + + tagname = "layout" + + manualLayout = Typed(expected_type=ManualLayout, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('manualLayout',) + + def __init__(self, + manualLayout=None, + extLst=None, + ): + self.manualLayout = manualLayout diff --git a/openpyxl/chart/legend.py b/openpyxl/chart/legend.py new file mode 100644 index 0000000..8a0060d --- /dev/null +++ b/openpyxl/chart/legend.py @@ -0,0 +1,73 @@ +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Integer, + Alias, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedBool, + NestedSet, + NestedInteger +) + +from .layout import Layout +from .shapes import GraphicalProperties +from .text import RichText + + +class LegendEntry(Serialisable): + + tagname = "legendEntry" + + idx = NestedInteger() + delete = NestedBool() + txPr = Typed(expected_type=RichText, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('idx', 'delete', 'txPr') + + def __init__(self, + idx=0, + delete=False, + txPr=None, + extLst=None, + ): + self.idx = idx + self.delete = delete + self.txPr = txPr + + +class Legend(Serialisable): + + tagname = "legend" + + legendPos = NestedSet(values=(['b', 'tr', 'l', 'r', 't'])) + position = Alias('legendPos') + legendEntry = Sequence(expected_type=LegendEntry) + layout = Typed(expected_type=Layout, allow_none=True) + overlay = NestedBool(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + txPr = Typed(expected_type=RichText, allow_none=True) + textProperties = Alias('txPr') + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('legendPos', 'legendEntry', 'layout', 'overlay', 'spPr', 'txPr',) + + def __init__(self, + legendPos="r", + legendEntry=(), + layout=None, + overlay=None, + spPr=None, + txPr=None, + extLst=None, + ): + self.legendPos = legendPos + self.legendEntry = legendEntry + self.layout = layout + self.overlay = overlay + self.spPr = spPr + self.txPr = txPr diff --git a/openpyxl/chart/line_chart.py b/openpyxl/chart/line_chart.py new file mode 100644 index 0000000..169bd67 --- /dev/null +++ b/openpyxl/chart/line_chart.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import +#Autogenerated schema +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Sequence, + Alias, + ) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedSet, + NestedBool, + NestedMinMax, +) + +from ._chart import ChartBase +from .updown_bars import UpDownBars +from .descriptors import NestedGapAmount +from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines, _BaseAxis +from .label import DataLabelList +from .series import Series + + +class _LineChartBase(ChartBase): + + grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked'])) + varyColors = NestedBool(allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + dropLines = Typed(expected_type=ChartLines, allow_none=True) + + _series_type = "line" + + __elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines') + + def __init__(self, + grouping="standard", + varyColors=None, + ser=(), + dLbls=None, + dropLines=None, + **kw + ): + self.grouping = grouping + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + self.dropLines = dropLines + super(_LineChartBase, self).__init__(**kw) + + +class LineChart(_LineChartBase): + + tagname = "lineChart" + + grouping = _LineChartBase.grouping + varyColors = _LineChartBase.varyColors + ser = _LineChartBase.ser + dLbls = _LineChartBase.dLbls + dropLines =_LineChartBase.dropLines + + hiLowLines = Typed(expected_type=ChartLines, allow_none=True) + upDownBars = Typed(expected_type=UpDownBars, allow_none=True) + marker = NestedBool(allow_none=True) + smooth = NestedBool(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=_BaseAxis) + y_axis = Typed(expected_type=NumericAxis) + + __elements__ = _LineChartBase.__elements__ + ('hiLowLines', 'upDownBars', 'marker', 'smooth', 'axId') + + def __init__(self, + hiLowLines=None, + upDownBars=None, + marker=None, + smooth=None, + extLst=None, + **kw + ): + self.hiLowLines = hiLowLines + self.upDownBars = upDownBars + self.marker = marker + self.smooth = smooth + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + + super(LineChart, self).__init__(**kw) + + +class LineChart3D(_LineChartBase): + + tagname = "line3DChart" + + grouping = _LineChartBase.grouping + varyColors = _LineChartBase.varyColors + ser = _LineChartBase.ser + dLbls = _LineChartBase.dLbls + dropLines =_LineChartBase.dropLines + + gapDepth = NestedGapAmount() + hiLowLines = Typed(expected_type=ChartLines, allow_none=True) + upDownBars = Typed(expected_type=UpDownBars, allow_none=True) + marker = NestedBool(allow_none=True) + smooth = NestedBool(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + z_axis = Typed(expected_type=SeriesAxis) + + __elements__ = _LineChartBase.__elements__ + ('gapDepth', 'hiLowLines', + 'upDownBars', 'marker', 'smooth', 'axId') + + def __init__(self, + gapDepth=None, + hiLowLines=None, + upDownBars=None, + marker=None, + smooth=None, + **kw + ): + self.gapDepth = gapDepth + self.hiLowLines = hiLowLines + self.upDownBars = upDownBars + self.marker = marker + self.smooth = smooth + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + self.z_axis = SeriesAxis() + super(LineChart3D, self).__init__(**kw) diff --git a/openpyxl/chart/marker.py b/openpyxl/chart/marker.py new file mode 100644 index 0000000..82f7613 --- /dev/null +++ b/openpyxl/chart/marker.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Alias, +) + +from openpyxl.descriptors.excel import( + ExtensionList, + _explicit_none, +) + +from openpyxl.descriptors.nested import ( + NestedBool, + NestedInteger, + NestedMinMax, + NestedNoneSet, +) + +from .layout import Layout +from .picture import PictureOptions +from .shapes import * +from .text import * +from .error_bar import * + + +class Marker(Serialisable): + + tagname = "marker" + + symbol = NestedNoneSet(values=(['circle', 'dash', 'diamond', 'dot', 'picture', + 'plus', 'square', 'star', 'triangle', 'x', 'auto']), + to_tree=_explicit_none) + size = NestedMinMax(min=2, max=72, allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('symbol', 'size', 'spPr') + + def __init__(self, + symbol=None, + size=None, + spPr=None, + extLst=None, + ): + self.symbol = symbol + self.size = size + if spPr is None: + spPr = GraphicalProperties() + self.spPr = spPr + + +class DataPoint(Serialisable): + + tagname = "dPt" + + idx = NestedInteger() + invertIfNegative = NestedBool(allow_none=True) + marker = Typed(expected_type=Marker, allow_none=True) + bubble3D = NestedBool(allow_none=True) + explosion = NestedInteger(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + pictureOptions = Typed(expected_type=PictureOptions, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('idx', 'invertIfNegative', 'marker', 'bubble3D', + 'explosion', 'spPr', 'pictureOptions') + + def __init__(self, + idx=None, + invertIfNegative=None, + marker=None, + bubble3D=None, + explosion=None, + spPr=None, + pictureOptions=None, + extLst=None, + ): + self.idx = idx + self.invertIfNegative = invertIfNegative + self.marker = marker + self.bubble3D = bubble3D + self.explosion = explosion + if spPr is None: + spPr = GraphicalProperties() + self.spPr = spPr + self.pictureOptions = pictureOptions diff --git a/openpyxl/chart/packaging.rst b/openpyxl/chart/packaging.rst new file mode 100644 index 0000000..eff4a2c --- /dev/null +++ b/openpyxl/chart/packaging.rst @@ -0,0 +1,8 @@ +Notes on packaging charts +========================= + +In Excel charts are global objects even though they must be assigned to either a worksheet or a chart sheet. Assignment is further complicated because it is indirect: charts are referred to from drawings (also global objects), which are referred to from worksheets. Drawings can contain other objects. Relations are managed indirectly between worksheets and drawings, and drawings and charts. + +The naive approach too packaging is to collect as groups (charts, images, etc.) and, thus calculate indices. This involves looping over sheets multiple times and is tightly coupled, involving the passing of relation ids around objects. + +A better approach would be to serialise dependent objects sheet by sheet in reverse order of dependency: serialise charts (in /charts) and add the relation to the worksheet drawing relationship mapper; then serialise drawings (not sure if there can be more than one per worksheet, charts are normally bundled) and their relationships and add the relation to the drawing to the worksheet. All dependent objects must be either shadowed or referenced to both in the worksheet and in the workbook so that duplicate filenames can be easily avoided. \ No newline at end of file diff --git a/openpyxl/chart/picture.py b/openpyxl/chart/picture.py new file mode 100644 index 0000000..2fe1604 --- /dev/null +++ b/openpyxl/chart/picture.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable + +from openpyxl.descriptors.nested import ( + NestedBool, + NestedFloat, + NestedMinMax, + NestedNoneSet, +) + +class PictureOptions(Serialisable): + + tagname = "pictureOptions" + + applyToFront = NestedBool(allow_none=True, nested=True) + applyToSides = NestedBool(allow_none=True, nested=True) + applyToEnd = NestedBool(allow_none=True, nested=True) + pictureFormat = NestedNoneSet(values=(['stretch', 'stack', 'stackScale']), nested=True) + pictureStackUnit = NestedFloat(allow_none=True, nested=True) + + __elements__ = ('applyToFront', 'applyToSides', 'applyToEnd', 'pictureFormat', 'pictureStackUnit') + + def __init__(self, + applyToFront=None, + applyToSides=None, + applyToEnd=None, + pictureFormat=None, + pictureStackUnit=None, + ): + self.applyToFront = applyToFront + self.applyToSides = applyToSides + self.applyToEnd = applyToEnd + self.pictureFormat = pictureFormat + self.pictureStackUnit = pictureStackUnit diff --git a/openpyxl/chart/pie_chart.py b/openpyxl/chart/pie_chart.py new file mode 100644 index 0000000..5f215d0 --- /dev/null +++ b/openpyxl/chart/pie_chart.py @@ -0,0 +1,177 @@ +#Autogenerated schema +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Bool, + MinMax, + Integer, + NoneSet, + Float, + Alias, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList, Percentage +from openpyxl.descriptors.nested import ( + NestedBool, + NestedMinMax, + NestedInteger, + NestedFloat, + NestedNoneSet, + NestedSet, +) +from openpyxl.descriptors.sequence import ValueSequence + +from ._chart import ChartBase +from .axis import ChartLines +from .descriptors import NestedGapAmount +from .series import Series +from .label import DataLabelList + + +class _PieChartBase(ChartBase): + + varyColors = NestedBool(allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + + _series_type = "pie" + + __elements__ = ('varyColors', 'ser', 'dLbls') + + def __init__(self, + varyColors=True, + ser=(), + dLbls=None, + ): + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + super(_PieChartBase, self).__init__() + + + +class PieChart(_PieChartBase): + + tagname = "pieChart" + + varyColors = _PieChartBase.varyColors + ser = _PieChartBase.ser + dLbls = _PieChartBase.dLbls + + firstSliceAng = NestedMinMax(min=0, max=360) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _PieChartBase.__elements__ + ('firstSliceAng', ) + + def __init__(self, + firstSliceAng=0, + extLst=None, + **kw + ): + self.firstSliceAng = firstSliceAng + super(PieChart, self).__init__(**kw) + + +class PieChart3D(_PieChartBase): + + tagname = "pie3DChart" + + varyColors = _PieChartBase.varyColors + ser = _PieChartBase.ser + dLbls = _PieChartBase.dLbls + + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _PieChartBase.__elements__ + + +class DoughnutChart(_PieChartBase): + + tagname = "doughnutChart" + + varyColors = _PieChartBase.varyColors + ser = _PieChartBase.ser + dLbls = _PieChartBase.dLbls + + firstSliceAng = NestedMinMax(min=0, max=360) + holeSize = NestedMinMax(min=10, max=90, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _PieChartBase.__elements__ + ('firstSliceAng', 'holeSize') + + def __init__(self, + firstSliceAng=0, + holeSize=10, + extLst=None, + **kw + ): + self.firstSliceAng = firstSliceAng + self.holeSize = holeSize + super(DoughnutChart, self).__init__(**kw) + + +class CustomSplit(Serialisable): + + tagname = "custSplit" + + secondPiePt = ValueSequence(expected_type=int) + + __elements__ = ('secondPiePt',) + + def __init__(self, + secondPiePt=(), + ): + self.secondPiePt = secondPiePt + + +class ProjectedPieChart(_PieChartBase): + + """ + From the spec 21.2.2.126 + + This element contains the pie of pie or bar of pie series on this + chart. Only the first series shall be displayed. The splitType element + shall determine whether the splitPos and custSplit elements apply. + """ + + tagname = "ofPieChart" + + varyColors = _PieChartBase.varyColors + ser = _PieChartBase.ser + dLbls = _PieChartBase.dLbls + + ofPieType = NestedSet(values=(['pie', 'bar'])) + type = Alias('ofPieType') + gapWidth = NestedGapAmount() + splitType = NestedNoneSet(values=(['auto', 'cust', 'percent', 'pos', 'val'])) + splitPos = NestedFloat(allow_none=True) + custSplit = Typed(expected_type=CustomSplit, allow_none=True) + secondPieSize = NestedMinMax(min=5, max=200, allow_none=True) + serLines = Typed(expected_type=ChartLines, allow_none=True) + join_lines = Alias('serLines') + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = _PieChartBase.__elements__ + ('ofPieType', 'gapWidth', + 'splitType', 'splitPos', 'custSplit', 'secondPieSize', 'serLines') + + def __init__(self, + ofPieType="pie", + gapWidth=None, + splitType="auto", + splitPos=None, + custSplit=None, + secondPieSize=75, + serLines=None, + extLst=None, + **kw + ): + self.ofPieType = ofPieType + self.gapWidth = gapWidth + self.splitType = splitType + self.splitPos = splitPos + self.custSplit = custSplit + self.secondPieSize = secondPieSize + if serLines is None: + self.serLines = ChartLines() + super(ProjectedPieChart, self).__init__(**kw) diff --git a/openpyxl/chart/plotarea.py b/openpyxl/chart/plotarea.py new file mode 100644 index 0000000..9aa96b4 --- /dev/null +++ b/openpyxl/chart/plotarea.py @@ -0,0 +1,169 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Sequence, + Typed, + Alias, +) +from openpyxl.descriptors.excel import ( + ExtensionList, +) +from openpyxl.descriptors.sequence import ( + MultiSequence, + MultiSequencePart, +) +from openpyxl.descriptors.nested import ( + NestedBool, + NestedNoneSet, + NestedInteger, + NestedString, + NestedMinMax, + NestedText, +) + +from .area_chart import AreaChart, AreaChart3D +from .bar_chart import BarChart, BarChart3D +from .bubble_chart import BubbleChart +from .line_chart import LineChart, LineChart3D +from .pie_chart import PieChart, PieChart3D, ProjectedPieChart, DoughnutChart +from .radar_chart import RadarChart +from .scatter_chart import ScatterChart +from .stock_chart import StockChart +from .surface_chart import SurfaceChart, SurfaceChart3D +from .layout import Layout +from .shapes import GraphicalProperties +from .text import RichText + +from .axis import ( + NumericAxis, + TextAxis, + SeriesAxis, + DateAxis, +) + + +class DataTable(Serialisable): + + tagname = "dTable" + + showHorzBorder = NestedBool(allow_none=True) + showVertBorder = NestedBool(allow_none=True) + showOutline = NestedBool(allow_none=True) + showKeys = NestedBool(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + txPr = Typed(expected_type=RichText, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('showHorzBorder', 'showVertBorder', 'showOutline', + 'showKeys', 'spPr', 'txPr') + + def __init__(self, + showHorzBorder=None, + showVertBorder=None, + showOutline=None, + showKeys=None, + spPr=None, + txPr=None, + extLst=None, + ): + self.showHorzBorder = showHorzBorder + self.showVertBorder = showVertBorder + self.showOutline = showOutline + self.showKeys = showKeys + self.spPr = spPr + self.txPr = txPr + + +class PlotArea(Serialisable): + + tagname = "plotArea" + + layout = Typed(expected_type=Layout, allow_none=True) + dTable = Typed(expected_type=DataTable, allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + # at least one chart + _charts = MultiSequence() + areaChart = MultiSequencePart(expected_type=AreaChart, store="_charts") + area3DChart = MultiSequencePart(expected_type=AreaChart3D, store="_charts") + lineChart = MultiSequencePart(expected_type=LineChart, store="_charts") + line3DChart = MultiSequencePart(expected_type=LineChart3D, store="_charts") + stockChart = MultiSequencePart(expected_type=StockChart, store="_charts") + radarChart = MultiSequencePart(expected_type=RadarChart, store="_charts") + scatterChart = MultiSequencePart(expected_type=ScatterChart, store="_charts") + pieChart = MultiSequencePart(expected_type=PieChart, store="_charts") + pie3DChart = MultiSequencePart(expected_type=PieChart3D, store="_charts") + doughnutChart = MultiSequencePart(expected_type=DoughnutChart, store="_charts") + barChart = MultiSequencePart(expected_type=BarChart, store="_charts") + bar3DChart = MultiSequencePart(expected_type=BarChart3D, store="_charts") + ofPieChart = MultiSequencePart(expected_type=ProjectedPieChart, store="_charts") + surfaceChart = MultiSequencePart(expected_type=SurfaceChart, store="_charts") + surface3DChart = MultiSequencePart(expected_type=SurfaceChart3D, store="_charts") + bubbleChart = MultiSequencePart(expected_type=BubbleChart, store="_charts") + + # axes + _axes = MultiSequence() + valAx = MultiSequencePart(expected_type=NumericAxis, store="_axes") + catAx = MultiSequencePart(expected_type=TextAxis, store="_axes") + dateAx = MultiSequencePart(expected_type=DateAxis, store="_axes") + serAx = MultiSequencePart(expected_type=SeriesAxis, store="_axes") + + __elements__ = ('layout', '_charts', '_axes', 'dTable', 'spPr') + + def __init__(self, + layout=None, + dTable=None, + spPr=None, + _charts=(), + _axes=(), + extLst=None, + ): + self.layout = layout + self.dTable = dTable + self.spPr = spPr + self._charts = _charts + self._axes = _axes + + + def to_tree(self, tagname=None, idx=None, namespace=None): + axIds = set((ax.axId for ax in self._axes)) + for chart in self._charts: + for id, axis in chart._axes.items(): + if id not in axIds: + setattr(self, axis.tagname, axis) + axIds.add(id) + + return super(PlotArea, self).to_tree(tagname) + + + @classmethod + def from_tree(cls, node): + self = super(PlotArea, cls).from_tree(node) + axes = dict((axis.axId, axis) for axis in self._axes) + for chart in self._charts: + if isinstance(chart, ScatterChart): + x, y = (axes[axId] for axId in chart.axId) + chart.x_axis = x + chart.y_axis = y + continue + + if isinstance(chart, (BarChart3D, LineChart3D, SurfaceChart3D)): + chart.z_axis = None + + for axId in chart.axId: + if not axId: + continue + axis = axes[axId] + if axis.tagname in ("catAx", "dateAx"): + chart.x_axis = axis + elif axis.tagname == "valAx": + chart.y_axis = axis + elif axis.tagname == "serAx": + chart.z_axis = axis + + return self diff --git a/openpyxl/chart/print_settings.py b/openpyxl/chart/print_settings.py new file mode 100644 index 0000000..824e33b --- /dev/null +++ b/openpyxl/chart/print_settings.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Float, + Typed, + Alias, +) + +from openpyxl.worksheet.page import PrintPageSetup +from openpyxl.worksheet.header_footer import HeaderFooter + + +class PageMargins(Serialisable): + """ + Identical to openpyxl.worksheet.page.Pagemargins but element names are different :-/ + """ + tagname = "pageMargins" + + l = Float() + left = Alias('l') + r = Float() + right = Alias('r') + t = Float() + top = Alias('t') + b = Float() + bottom = Alias('b') + header = Float() + footer = Float() + + def __init__(self, l=0.75, r=0.75, t=1, b=1, header=0.5, footer=0.5): + self.l = l + self.r = r + self.t = t + self.b = b + self.header = header + self.footer = footer + + +class PrintSettings(Serialisable): + + tagname = "printSettings" + + headerFooter = Typed(expected_type=HeaderFooter, allow_none=True) + pageMargins = Typed(expected_type=PageMargins, allow_none=True) + pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True) + + __elements__ = ("headerFooter", "pageMargins", "pageMargins") + + def __init__(self, + headerFooter=None, + pageMargins=None, + pageSetup=None, + ): + self.headerFooter = headerFooter + self.pageMargins = pageMargins + self.pageSetup = pageSetup diff --git a/openpyxl/chart/radar_chart.py b/openpyxl/chart/radar_chart.py new file mode 100644 index 0000000..778c82f --- /dev/null +++ b/openpyxl/chart/radar_chart.py @@ -0,0 +1,53 @@ +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Sequence, + Typed, + Alias, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedBool, + NestedInteger, + NestedSet +) + +from ._chart import ChartBase +from .axis import TextAxis, NumericAxis +from .series import Series +from .label import DataLabelList + + +class RadarChart(ChartBase): + + tagname = "radarChart" + + radarStyle = NestedSet(values=(['standard', 'marker', 'filled'])) + type = Alias("radarStyle") + varyColors = NestedBool(nested=True, allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + _series_type = "radar" + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + + __elements__ = ('radarStyle', 'varyColors', 'ser', 'dLbls', 'axId') + + def __init__(self, + radarStyle="standard", + varyColors=None, + ser=(), + dLbls=None, + extLst=None, + **kw + ): + self.radarStyle = radarStyle + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + super(RadarChart, self).__init__(**kw) diff --git a/openpyxl/chart/reader.py b/openpyxl/chart/reader.py new file mode 100644 index 0000000..f7799a6 --- /dev/null +++ b/openpyxl/chart/reader.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2017 openpyxl +# Copyright (c) 2018 qyou.casia@gmail.com + +""" +Read a chart +""" + +from .chartspace import ChartSpace, PlotArea +from openpyxl.xml.functions import fromstring + +from openpyxl.packaging.relationship import get_rel, get_rels_path, get_dependents +from openpyxl.drawing.spreadsheet_drawing import SpreadsheetDrawing +from openpyxl.drawing.image import Image +from io import BytesIO + +_types = ('areaChart', 'area3DChart', 'lineChart', 'line3DChart', + 'stockChart', 'radarChart', 'scatterChart', 'pieChart', 'pie3DChart', + 'doughnutChart', 'barChart', 'bar3DChart', 'ofPieChart', 'surfaceChart', + 'surface3DChart', 'bubbleChart',) + +_axes = ('valAx', 'catAx', 'dateAx', 'serAx',) + + +def read_chart(chartspace): + cs = chartspace + plot = cs.chart.plotArea + + chart = plot._charts[0] + chart._charts = plot._charts + + chart.title = cs.chart.title + chart.layout = plot.layout + chart.legend = cs.chart.legend + + return chart + + +def find_charts(archive, path): + """ + Given the path to a drawing file extract anchors with charts + """ + + src = archive.read(path) + tree = fromstring(src) + drawing = SpreadsheetDrawing.from_tree(tree) + + rels_path = get_rels_path(path) + deps = [] + if rels_path in archive.namelist(): + deps = get_dependents(archive, rels_path) + + charts = [] + for rel in drawing._chart_rels: + cs = get_rel(archive, deps, rel.id, ChartSpace) + chart = read_chart(cs) + chart.anchor = rel.anchor + charts.append(chart) + + return charts + + +def find_images(archive, path): + src = archive.read(path) + tree = fromstring(src) + drawing = SpreadsheetDrawing.from_tree(tree) + + rels_path = get_rels_path(path) + deps = [] + if rels_path in archive.namelist(): + deps = get_dependents(archive, rels_path) + + images = [] + for rel in drawing._image_rels: + id = rel.embed + path = deps[id].target + image = Image(BytesIO(archive.read(path))) + image.anchor = rel.anchor + images.append(image) + return images diff --git a/openpyxl/chart/reference.py b/openpyxl/chart/reference.py new file mode 100644 index 0000000..2abc74d --- /dev/null +++ b/openpyxl/chart/reference.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from itertools import chain + +from openpyxl.compat import unicode +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + MinMax, + Typed, + String, + Strict, +) +from openpyxl.worksheet import Worksheet +from openpyxl.utils import ( + get_column_letter, + range_to_tuple, + quote_sheetname +) + + +class DummyWorksheet: + + + def __init__(self, title): + self.title = title + + +class Reference(Strict): + + """ + Normalise cell range references + """ + + min_row = MinMax(min=1, max=1000000, expected_type=int) + max_row = MinMax(min=1, max=1000000, expected_type=int) + min_col = MinMax(min=1, max=16384, expected_type=int) + max_col = MinMax(min=1, max=16384, expected_type=int) + range_string = String(allow_none=True) + + def __init__(self, + worksheet=None, + min_col=None, + min_row=None, + max_col=None, + max_row=None, + range_string=None + ): + if range_string is not None: + sheetname, boundaries = range_to_tuple(range_string) + min_col, min_row, max_col, max_row = boundaries + worksheet = DummyWorksheet(sheetname) + + self.worksheet = worksheet + self.min_col = min_col + self.min_row = min_row + if max_col is None: + max_col = min_col + self.max_col = max_col + if max_row is None: + max_row = min_row + self.max_row = max_row + + + def __repr__(self): + return unicode(self) + + + def __str__(self): + fmt = u"{0}!${1}${2}:${3}${4}" + if (self.min_col == self.max_col + and self.min_row == self.max_row): + fmt = u"{0}!${1}${2}" + return fmt.format(self.sheetname, + get_column_letter(self.min_col), self.min_row, + get_column_letter(self.max_col), self.max_row + ) + + + __unicode__ = __str__ + + + + def __len__(self): + if self.min_row == self.max_row: + return 1 + self.max_col - self.min_col + return 1 + self.max_row - self.min_row + + + @property + def rows(self): + """ + Return all cells in range by column + """ + for row in range(self.min_row, self.max_row+1): + yield tuple('%s%d' % (get_column_letter(col), row) + for col in range(self.min_col, self.max_col+1)) + + + @property + def cols(self): + """ + Return all cells in range by row + """ + for col in range(self.min_col, self.max_col+1): + yield tuple('%s%d' % (get_column_letter(col), row) + for row in range(self.min_row, self.max_row+1)) + + + @property + def cells(self): + """ + Return a flattened list of all cells (by column) + """ + return chain.from_iterable(self.cols) + + + def pop(self): + """ + Return and remove the first cell + """ + cell = next(self.cells) + if self.min_row == self.max_row: + self.min_col += 1 + else: + self.min_row += 1 + return cell + + + @property + def sheetname(self): + return quote_sheetname(self.worksheet.title) diff --git a/openpyxl/chart/scatter_chart.py b/openpyxl/chart/scatter_chart.py new file mode 100644 index 0000000..ce8dcc6 --- /dev/null +++ b/openpyxl/chart/scatter_chart.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Sequence, + Alias +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedNoneSet, + NestedBool, +) + +from ._chart import ChartBase +from .axis import NumericAxis +from .series import XYSeries +from .label import DataLabelList + + +class ScatterChart(ChartBase): + + tagname = "scatterChart" + + scatterStyle = NestedNoneSet(values=(['line', 'lineMarker', 'marker', 'smooth', 'smoothMarker'])) + varyColors = NestedBool(allow_none=True) + ser = Sequence(expected_type=XYSeries, allow_none=True) + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias("dLbls") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=NumericAxis) + y_axis = Typed(expected_type=NumericAxis) + + _series_type = "scatter" + + __elements__ = ('scatterStyle', 'varyColors', 'ser', 'dLbls', 'axId',) + + def __init__(self, + scatterStyle=None, + varyColors=None, + ser=(), + dLbls=None, + extLst=None, + **kw + ): + self.scatterStyle = scatterStyle + self.varyColors = varyColors + self.ser = ser + self.dLbls = dLbls + self.x_axis = NumericAxis(axId=10, crossAx=20) + self.y_axis = NumericAxis(axId=20, crossAx=10) + super(ScatterChart, self).__init__(**kw) diff --git a/openpyxl/chart/series.py b/openpyxl/chart/series.py new file mode 100644 index 0000000..ec92f7b --- /dev/null +++ b/openpyxl/chart/series.py @@ -0,0 +1,197 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.compat import unicode + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + String, + Integer, + Bool, + Alias, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedInteger, + NestedBool, + NestedNoneSet, + NestedText, +) + +from .shapes import GraphicalProperties +from .data_source import ( + AxDataSource, + NumDataSource, + NumRef, + StrRef, +) +from .error_bar import ErrorBars +from .label import DataLabelList +from .marker import DataPoint, PictureOptions, Marker +from .trendline import Trendline + +attribute_mapping = { + 'area': ('idx', 'order', 'tx', 'spPr', 'pictureOptions', 'dPt', 'dLbls', 'errBars', + 'trendline', 'cat', 'val',), + 'bar':('idx', 'order','tx', 'spPr', 'invertIfNegative', 'pictureOptions', 'dPt', + 'dLbls', 'trendline', 'errBars', 'cat', 'val', 'shape'), + 'bubble':('idx','order', 'tx', 'spPr', 'invertIfNegative', 'dPt', 'dLbls', + 'trendline', 'errBars', 'xVal', 'yVal', 'bubbleSize', 'bubble3D'), + 'line':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'trendline', + 'errBars', 'cat', 'val', 'smooth'), + 'pie':('idx', 'order', 'tx', 'spPr', 'explosion', 'dPt', 'dLbls', 'cat', 'val'), + 'radar':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'cat', 'val'), + 'scatter':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'trendline', + 'errBars', 'xVal', 'yVal', 'smooth'), + 'surface':('idx', 'order', 'tx', 'spPr', 'cat', 'val'), + } + + +class SeriesLabel(Serialisable): + + tagname = "tx" + + strRef = Typed(expected_type=StrRef, allow_none=True) + v = NestedText(expected_type=unicode, allow_none=True) + value = Alias('v') + + __elements__ = ('strRef', 'v') + + def __init__(self, + strRef=None, + v=None): + self.strRef = strRef + self.v = v + + +class Series(Serialisable): + + """ + Generic series object. Should not be instantiated directly. + User the chart.Series factory instead. + """ + + tagname = "ser" + + idx = NestedInteger() + order = NestedInteger() + tx = Typed(expected_type=SeriesLabel, allow_none=True) + title = Alias('tx') + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + + # area chart + pictureOptions = Typed(expected_type=PictureOptions, allow_none=True) + dPt = Sequence(expected_type=DataPoint, allow_none=True) + data_points = Alias("dPt") + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + labels = Alias("dLbls") + trendline = Typed(expected_type=Trendline, allow_none=True) + errBars = Typed(expected_type=ErrorBars, allow_none=True) + cat = Typed(expected_type=AxDataSource, allow_none=True) + identifiers = Alias("cat") + val = Typed(expected_type=NumDataSource, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + #bar chart + invertIfNegative = NestedBool(allow_none=True) + shape = NestedNoneSet(values=(['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax'])) + + #bubble chart + xVal = Typed(expected_type=AxDataSource, allow_none=True) + yVal = Typed(expected_type=NumDataSource, allow_none=True) + bubbleSize = Typed(expected_type=NumDataSource, allow_none=True) + zVal = Alias("bubbleSize") + bubble3D = NestedBool(allow_none=True) + + #line chart + marker = Typed(expected_type=Marker, allow_none=True) + smooth = NestedBool(allow_none=True) + + #pie chart + explosion = NestedInteger(allow_none=True) + + __elements__ = () + + + def __init__(self, + idx=0, + order=0, + tx=None, + spPr=None, + pictureOptions=None, + dPt=(), + dLbls=None, + trendline=None, + errBars=None, + cat=None, + val=None, + invertIfNegative=None, + shape=None, + xVal=None, + yVal=None, + bubbleSize=None, + bubble3D=None, + marker=None, + smooth=None, + explosion=None, + extLst=None, + ): + self.idx = idx + self.order = order + self.tx = tx + if spPr is None: + spPr = GraphicalProperties() + self.spPr = spPr + self.pictureOptions = pictureOptions + self.dPt = dPt + self.dLbls = dLbls + self.trendline = trendline + self.errBars = errBars + self.cat = cat + self.val = val + self.invertIfNegative = invertIfNegative + self.shape = shape + self.xVal = xVal + self.yVal = yVal + self.bubbleSize = bubbleSize + self.bubble3D = bubble3D + if marker is None: + marker = Marker() + self.marker = marker + self.smooth = smooth + self.explosion = explosion + + def to_tree(self, tagname=None, idx=None): + if idx is not None: + if self.order == self.idx: + self.order = idx + self.idx = idx + return super(Series, self).to_tree(tagname) + + +class XYSeries(Series): + + """Dedicated series for charts that have x and y series""" + + idx = Series.idx + order = Series.order + tx = Series.tx + spPr = Series.spPr + + dPt = Series.dPt + dLbls = Series.dLbls + trendline = Series.trendline + errBars = Series.errBars + xVal = Series.xVal + yVal = Series.yVal + + invertIfNegative = Series.invertIfNegative + + bubbleSize = Series.bubbleSize + bubble3D = Series.bubble3D + + marker = Series.marker + smooth = Series.smooth diff --git a/openpyxl/chart/series_factory.py b/openpyxl/chart/series_factory.py new file mode 100644 index 0000000..31a1674 --- /dev/null +++ b/openpyxl/chart/series_factory.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from .data_source import NumDataSource, NumRef, AxDataSource +from .reference import Reference +from .series import Series, XYSeries, SeriesLabel, StrRef +from openpyxl.utils import rows_from_range, quote_sheetname + + +def SeriesFactory(values, xvalues=None, zvalues=None, title=None, title_from_data=False): + """ + Convenience Factory for creating chart data series. + """ + + if not isinstance(values, Reference): + values = Reference(range_string=values) + + if title_from_data: + cell = values.pop() + title = u"{0}!{1}".format(values.sheetname, cell) + title = SeriesLabel(strRef=StrRef(title)) + elif title is not None: + title = SeriesLabel(v=title) + + source = NumDataSource(numRef=NumRef(f=values)) + if xvalues is not None: + if not isinstance(xvalues, Reference): + xvalues = Reference(range_string=xvalues) + series = XYSeries() + series.yVal = source + series.xVal = AxDataSource(numRef=NumRef(f=xvalues)) + if zvalues is not None: + if not isinstance(zvalues, Reference): + zvalues = Reference(range_string=zvalues) + series.zVal = NumDataSource(NumRef(f=zvalues)) + else: + series = Series() + series.val = source + + if title is not None: + series.title = title + return series diff --git a/openpyxl/chart/shapes.py b/openpyxl/chart/shapes.py new file mode 100644 index 0000000..e81e517 --- /dev/null +++ b/openpyxl/chart/shapes.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Alias +) +from openpyxl.descriptors.nested import ( + EmptyTag +) +from openpyxl.drawing.colors import ColorChoiceDescriptor +from openpyxl.drawing.fill import * +from openpyxl.drawing.line import LineProperties +from openpyxl.drawing.shapes import ( + Shape3D, + Scene3D, + Transform2D, + CustomGeometry2D, + PresetGeometry2D, +) + + +class GraphicalProperties(Serialisable): + + """ + Somewhat vaguely 21.2.2.197 says this: + + This element specifies the formatting for the parent chart element. The + custGeom, prstGeom, scene3d, and xfrm elements are not supported. The + bwMode attribute is not supported. + + This doesn't leave much. And the element is used in different places. + """ + + tagname = "spPr" + + bwMode = NoneSet(values=(['clr', 'auto', 'gray', 'ltGray', 'invGray', + 'grayWhite', 'blackGray', 'blackWhite', 'black', 'white', 'hidden'] + ) + ) + + xfrm = Typed(expected_type=Transform2D, allow_none=True) + transform = Alias('xfrm') + custGeom = Typed(expected_type=CustomGeometry2D, allow_none=True) # either or + prstGeom = Typed(expected_type=PresetGeometry2D, allow_none=True) + + # fills one of + noFill = EmptyTag(namespace=DRAWING_NS) + solidFill = ColorChoiceDescriptor() + gradFill = Typed(expected_type=GradientFillProperties, allow_none=True) + pattFill = Typed(expected_type=PatternFillProperties, allow_none=True) + + ln = Typed(expected_type=LineProperties, allow_none=True) + line = Alias('ln') + scene3d = Typed(expected_type=Scene3D, allow_none=True) + sp3d = Typed(expected_type=Shape3D, allow_none=True) + shape3D = Alias('sp3d') + extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True) + + __elements__ = ('xfrm', 'prstGeom', 'noFill', 'solidFill', 'gradFill', 'pattFill', + 'ln', 'scene3d', 'sp3d') + + def __init__(self, + bwMode=None, + xfrm=None, + noFill=None, + solidFill=None, + gradFill=None, + pattFill=None, + ln=None, + scene3d=None, + custGeom=None, + prstGeom=None, + sp3d=None, + extLst=None, + ): + self.bwMode = bwMode + self.xfrm = xfrm + self.noFill = noFill + self.solidFill = solidFill + self.gradFill = gradFill + self.pattFill = pattFill + if ln is None: + ln = LineProperties() + self.ln = ln + self.custGeom = custGeom + self.prstGeom = prstGeom + self.scene3d = scene3d + self.sp3d = sp3d diff --git a/openpyxl/chart/stock_chart.py b/openpyxl/chart/stock_chart.py new file mode 100644 index 0000000..0b040e2 --- /dev/null +++ b/openpyxl/chart/stock_chart.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Sequence, + Alias, +) +from openpyxl.descriptors.excel import ExtensionList + +from ._chart import ChartBase +from .axis import TextAxis, NumericAxis, ChartLines +from .updown_bars import UpDownBars +from .label import DataLabelList +from .series import Series + + +class StockChart(ChartBase): + + tagname = "stockChart" + + ser = Sequence(expected_type=Series) #min 3, max4 + dLbls = Typed(expected_type=DataLabelList, allow_none=True) + dataLabels = Alias('dLbls') + dropLines = Typed(expected_type=ChartLines, allow_none=True) + hiLowLines = Typed(expected_type=ChartLines, allow_none=True) + upDownBars = Typed(expected_type=UpDownBars, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + + _series_type = "line" + + __elements__ = ('ser', 'dLbls', 'dropLines', 'hiLowLines', 'upDownBars', + 'axId') + + def __init__(self, + ser=(), + dLbls=None, + dropLines=None, + hiLowLines=None, + upDownBars=None, + extLst=None, + **kw + ): + self.ser = ser + self.dLbls = dLbls + self.dropLines = dropLines + self.hiLowLines = hiLowLines + self.upDownBars = upDownBars + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + super(StockChart, self).__init__(**kw) diff --git a/openpyxl/chart/surface_chart.py b/openpyxl/chart/surface_chart.py new file mode 100644 index 0000000..09c7f8a --- /dev/null +++ b/openpyxl/chart/surface_chart.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Integer, + Bool, + Alias, + Sequence, +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedInteger, + NestedBool, +) + +from ._chart import ChartBase +from ._3d import _3DBase +from .axis import TextAxis, NumericAxis, SeriesAxis +from .shapes import GraphicalProperties +from .series import Series + + +class BandFormat(Serialisable): + + tagname = "bandFmt" + + idx = NestedInteger() + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + + __elements__ = ('idx', 'spPr') + + def __init__(self, + idx=0, + spPr=None, + ): + self.idx = idx + self.spPr = spPr + + +class BandFormatList(Serialisable): + + tagname = "bandFmts" + + bandFmt = Sequence(expected_type=BandFormat, allow_none=True) + + __elements__ = ('bandFmt',) + + def __init__(self, + bandFmt=(), + ): + self.bandFmt = bandFmt + + +class _SurfaceChartBase(ChartBase): + + wireframe = NestedBool(allow_none=True) + ser = Sequence(expected_type=Series, allow_none=True) + bandFmts = Typed(expected_type=BandFormatList, allow_none=True) + + _series_type = "surface" + + __elements__ = ('wireframe', 'ser', 'bandFmts') + + def __init__(self, + wireframe=None, + ser=(), + bandFmts=None, + **kw + ): + self.wireframe = wireframe + self.ser = ser + self.bandFmts = bandFmts + super(_SurfaceChartBase, self).__init__(**kw) + + +class SurfaceChart3D(_SurfaceChartBase, _3DBase): + + tagname = "surface3DChart" + + wireframe = _SurfaceChartBase.wireframe + ser = _SurfaceChartBase.ser + bandFmts = _SurfaceChartBase.bandFmts + + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + x_axis = Typed(expected_type=TextAxis) + y_axis = Typed(expected_type=NumericAxis) + z_axis = Typed(expected_type=SeriesAxis) + + __elements__ = _SurfaceChartBase.__elements__ + ('axId',) + + def __init__(self, **kw): + self.x_axis = TextAxis() + self.y_axis = NumericAxis() + self.z_axis = SeriesAxis() + super(SurfaceChart3D, self).__init__(**kw) + + +class SurfaceChart(SurfaceChart3D): + + tagname = "surfaceChart" + + wireframe = _SurfaceChartBase.wireframe + ser = _SurfaceChartBase.ser + bandFmts = _SurfaceChartBase.bandFmts + + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = SurfaceChart3D.__elements__ + + def __init__(self, **kw): + super(SurfaceChart, self).__init__(**kw) + self.y_axis.delete = True + self.view3D.x_rotation = 90 + self.view3D.y_rotation = 0 + self.view3D.perspective = False + self.view3D.right_angle_axes = False diff --git a/openpyxl/chart/tests/__init__.py b/openpyxl/chart/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openpyxl/chart/tests/conftest.py b/openpyxl/chart/tests/conftest.py new file mode 100644 index 0000000..a7e0f7b --- /dev/null +++ b/openpyxl/chart/tests/conftest.py @@ -0,0 +1,12 @@ +# Fixtures (pre-configured objects) for tests +import pytest + + +@pytest.fixture +def datadir(): + """DATADIR as a LocalPath""" + import os + from py._path.local import LocalPath + here = os.path.split(__file__)[0] + DATADIR = os.path.join(here, "data") + return LocalPath(DATADIR) diff --git a/openpyxl/chart/tests/data/chart1.xml b/openpyxl/chart/tests/data/chart1.xml new file mode 100644 index 0000000..abced86 --- /dev/null +++ b/openpyxl/chart/tests/data/chart1.xml @@ -0,0 +1,1198 @@ + + + + + + + + + + + + + + + + + + + + + + + Website Performance + + + + + + + + + + + + + + + + + + + onLoad!$A$2 + + + + Bob + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$2:$L$2 + + General + + + 9436.0 + + + 14389.0 + + + 12502.0 + + + 13691.0 + + + 9337.0 + + + 12471.0 + + + 13671.0 + + + 14200.0 + + + 12755.0 + + + 20353.0 + + + 12489.0 + + + + + + + + + + + + onLoad!$A$3 + + + + Alice + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$3:$L$3 + + General + + + 4382.0 + + + 4628.0 + + + 5439.0 + + + 6077.0 + + + 6849.0 + + + 5164.0 + + + 5611.0 + + + 10239.0 + + + 5391.0 + + + 4200.0 + + + 6908.0 + + + + + + + + + + + + onLoad!$A$4 + + + + Eve + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$4:$L$4 + + General + + + 9404.0 + + + 9277.0 + + + 8842.0 + + + 9193.0 + + + 12849.0 + + + 9408.0 + + + 6985.0 + + + 8376.0 + + + 6589.0 + + + 5326.0 + + + 9380.0 + + + + + + + + + + + + onLoad!$A$5 + + + + Charles + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$5:$L$5 + + General + + + 8669.0 + + + 12775.0 + + + 18881.0 + + + 13008.0 + + + 11672.0 + + + 9092.0 + + + 9427.0 + + + 11328.0 + + + 8308.0 + + + 11606.0 + + + 8685.0 + + + + + + + + + + + + onLoad!$A$6 + + + + Sam + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$6:$L$6 + + General + + + 3126.0 + + + 3451.0 + + + 2007.0 + + + 6498.0 + + + 4327.0 + + + 3840.0 + + + 3420.0 + + + 4437.0 + + + 3700.0 + + + 4167.0 + + + 4065.0 + + + + + + + + + + + + onLoad!$A$7 + + + + June + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$7:$L$7 + + General + + + 12337.0 + + + 13909.0 + + + 12156.0 + + + 11869.0 + + + 11810.0 + + + 14288.0 + + + 12644.0 + + + 12912.0 + + + 12048.0 + + + 13620.0 + + + 14090.0 + + + + + + + + + + + + onLoad!$A$8 + + + + Roger + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$8:$L$8 + + General + + + 10492.0 + + + 7541.0 + + + 6503.0 + + + 57700.0 + + + 9235.0 + + + 10934.0 + + + 12223.0 + + + 9639.0 + + + 42414.0 + + + 11192.0 + + + + + + + + + + + + onLoad!$A$9 + + + + Helen + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$9:$L$9 + + General + + + 19456.0 + + + 16247.0 + + + 18118.0 + + + 17689.0 + + + 15583.0 + + + 16092.0 + + + 16477.0 + + + 18264.0 + + + 16444.0 + + + 16336.0 + + + 15730.0 + + + + + + + + + + + + onLoad!$A$10 + + + + Dave + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$10:$L$10 + + General + + + 8456.0 + + + 9263.0 + + + 9469.0 + + + 7756.0 + + + 8322.0 + + + 10372.0 + + + 8305.0 + + + 10749.0 + + + 10780.0 + + + 16264.0 + + + 11250.0 + + + + + + + + + + + + onLoad!$A$11 + + + + Charlotte + + + + + + + + + + + + + + + onLoad!$B$1:$M$1 + + d\-mmm\-yy + + + 42186.0 + + + 42200.0 + + + 42217.0 + + + 42231.0 + + + 42248.0 + + + 42262.0 + + + 42292.0 + + + 42309.0 + + + 42323.0 + + + 42339.0 + + + 42353.0 + + + + + + + onLoad!$B$11:$L$11 + + General + + + 10411.0 + + + 9473.0 + + + 9610.0 + + + 12045.0 + + + 7571.0 + + + 9756.0 + + + 9004.0 + + + 9871.0 + + + 15782.0 + + + 20767.0 + + + 8120.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Time in seconds + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openpyxl/chart/tests/data/plotarea.xml b/openpyxl/chart/tests/data/plotarea.xml new file mode 100644 index 0000000..4ca9b19 --- /dev/null +++ b/openpyxl/chart/tests/data/plotarea.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + OXM!$F$10 + + + + + + + + OXM!$B$11:$B$29 + + + + + OXM!$F$11:$F$29 + + + + + + + + + + + + + + + + + + + + + + + + + + OXM!$G$10 + + + + + + + + OXM!$B$11:$B$29 + + + + + OXM!$G$11:$G$29 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <tx> + <rich> + <a:bodyPr rot="-5400000" vert="horz"/> + <a:lstStyle/> + <a:p> + <a:pPr> + <a:defRPr/> + </a:pPr> + <a:r> + <a:rPr lang="en-US"/> + <a:t>Panel Sales</a:t> + </a:r> + </a:p> + </rich> + </tx> + <layout/> + <overlay val="0"/> + + + + + + + + + + + + + + + + + + <tx> + <rich> + <a:bodyPr rot="-5400000" vert="horz"/> + <a:lstStyle/> + <a:p> + <a:pPr> + <a:defRPr/> + </a:pPr> + <a:r> + <a:rPr lang="en-US"/> + <a:t>Revenue ($MM)</a:t> + </a:r> + </a:p> + </rich> + </tx> + <layout/> + <overlay val="0"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openpyxl/chart/tests/data/sample.xlsx b/openpyxl/chart/tests/data/sample.xlsx new file mode 100644 index 0000000..f4d4b3a Binary files /dev/null and b/openpyxl/chart/tests/data/sample.xlsx differ diff --git a/openpyxl/chart/tests/data/scatterchart_plot_area.xml b/openpyxl/chart/tests/data/scatterchart_plot_area.xml new file mode 100644 index 0000000..4e7f6ec --- /dev/null +++ b/openpyxl/chart/tests/data/scatterchart_plot_area.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + C_File_Calculations!$K$56 + + + + + + + + + + + + + + + + + + + + + + + + + C_File_Calculations!$C$58:$C$64 + + + + + C_File_Calculations!$K$58:$K$64 + + + + + + + + + + + + + + + + + + + + + + + + <tx> + <rich> + <a:bodyPr/> + <a:lstStyle/> + <a:p> + <a:pPr> + <a:defRPr/> + </a:pPr> + <a:r> + <a:rPr lang="en-CA"/> + <a:t>Concentration (mg/L)</a:t> + </a:r> + </a:p> + </rich> + </tx> + <layout/> + <overlay val="0"/> + + + + + + + + + + + + + + + + + + + <tx> + <rich> + <a:bodyPr rot="-5400000" vert="horz"/> + <a:lstStyle/> + <a:p> + <a:pPr> + <a:defRPr/> + </a:pPr> + <a:r> + <a:rPr lang="en-CA" baseline="0"/> + <a:t> Absorbance (AU)</a:t> + </a:r> + <a:endParaRPr lang="en-CA"/> + </a:p> + </rich> + </tx> + <layout/> + <overlay val="0"/> + + + + + + + + + + + + + + + + + + + diff --git a/openpyxl/chart/tests/test_area_chart.py b/openpyxl/chart/tests/test_area_chart.py new file mode 100644 index 0000000..2385043 --- /dev/null +++ b/openpyxl/chart/tests/test_area_chart.py @@ -0,0 +1,158 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml +from .. import Series + + +@pytest.fixture +def AreaChart(): + from ..area_chart import AreaChart + return AreaChart + + +class TestAreaChart: + + def test_ctor(self, AreaChart): + chart = AreaChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, AreaChart): + src = """ + + + + + + + """ + node = fromstring(src) + chart = AreaChart.from_tree(node) + assert chart == AreaChart(grouping="percentStacked", varyColors=True) + + + def test_write(self, AreaChart): + s1 = Series(values="Sheet1!$A$1:$A$12") + s2 = Series(values="Sheet1!$B$1:$B$12") + chart = AreaChart(ser=[s1, s2]) + xml = tostring(chart._write()) + expected = """ + + + + + + + + + + + + + + + + Sheet1!$A$1:$A$12 + + + + + + + + + + + + + + Sheet1!$B$1:$B$12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def AreaChart3D(): + from ..area_chart import AreaChart3D + return AreaChart3D + + +class TestAreaChart3D: + + def test_ctor(self, AreaChart3D): + chart = AreaChart3D() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, AreaChart3D): + src = """ + + + + + + + """ + node = fromstring(src) + chart = AreaChart3D.from_tree(node) + assert chart == AreaChart3D(gapDepth=150) diff --git a/openpyxl/chart/tests/test_axis.py b/openpyxl/chart/tests/test_axis.py new file mode 100644 index 0000000..e6e040d --- /dev/null +++ b/openpyxl/chart/tests/test_axis.py @@ -0,0 +1,364 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring, fromstring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def Scaling(): + from ..axis import Scaling + return Scaling + + +class TestScale: + + + def test_ctor(self, Scaling): + + scale = Scaling() + xml = tostring(scale.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Scaling): + + xml = """ + + + + + """ + node = fromstring(xml) + scale = Scaling.from_tree(node) + assert scale == Scaling(logBase=10) + + +@pytest.fixture +def _BaseAxis(): + from ..axis import _BaseAxis + return _BaseAxis + + +class TestAxis: + + def test_ctor(self, _BaseAxis, Scaling): + axis = _BaseAxis(axId=10, crossAx=100) + xml = tostring(axis.to_tree(tagname="baseAxis")) + expected = """ + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + +@pytest.fixture +def TextAxis(): + from ..axis import TextAxis + return TextAxis + + +class TestTextAxis: + + def test_ctor(self, TextAxis): + axis = TextAxis(axId=10, crossAx=100) + xml = tostring(axis.to_tree()) + expected = """ + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def from_xml(self, TextAxis): + src = """ + + + + + + + + + + + + + + + + + + """ + node = fromstring(src) + axis = CatAx.from_tree(node) + assert axis.scaling.orientation == "minMax" + assert axis.auto is True + assert axis.majorTickMark == "out" + assert axis.minorTickMark is None + + +@pytest.fixture +def NumericAxis(): + from ..axis import NumericAxis + return NumericAxis + + +class TestValAx: + + def test_ctor(self, NumericAxis): + axis = NumericAxis(axId=100, crossAx=10) + xml = tostring(axis.to_tree()) + expected = """ + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, NumericAxis): + src = """ + + + + + + + + + + + + + + + + + + """ + node = fromstring(src) + axis = NumericAxis.from_tree(node) + assert axis.delete is False + assert axis.crossAx == 2065276984 + assert axis.crossBetween == "between" + assert axis.scaling.logBase == 10 + + +@pytest.fixture +def DateAxis(): + from ..axis import DateAxis + return DateAxis + + +class TestDateAx: + + + def test_ctor(self, DateAxis): + axis = DateAxis(axId=500, crossAx=10) + xml = tostring(axis.to_tree()) + expected = """ + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DateAxis): + from openpyxl.chart.data_source import NumFmt + + src = """ + + + + + + + + + + + + + + + + + + """ + node = fromstring(src) + axis = DateAxis.from_tree(node) + assert axis == DateAxis(axId=20, crossAx=10, axPos="b", delete=False, + numFmt=NumFmt("d-mmm", True), majorTickMark="out", + crosses="autoZero", tickLblPos="nextTo", auto=True, lblOffset=100, + baseTimeUnit="months") + + +@pytest.fixture +def SeriesAxis(): + from ..axis import SeriesAxis + return SeriesAxis + + +class TestSeriesAxis: + + def test_ctor(self, SeriesAxis): + axis = SeriesAxis(axId=1000, crossAx=10) + xml = tostring(axis.to_tree()) + expected = """ + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, SeriesAxis): + src = """ + + + + + + + + + """ + node = fromstring(src) + axis = SeriesAxis.from_tree(node) + assert axis == SeriesAxis() + + +@pytest.fixture +def DisplayUnitsLabel(): + from ..axis import DisplayUnitsLabel + return DisplayUnitsLabel + + +class TestDispUnitsLabel: + + def test_ctor(self, DisplayUnitsLabel): + axis = DisplayUnitsLabel() + xml = tostring(axis.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DisplayUnitsLabel): + src = """ + + """ + node = fromstring(src) + axis = DisplayUnitsLabel.from_tree(node) + assert axis == DisplayUnitsLabel() + + +@pytest.fixture +def DisplayUnitsLabelList(): + from ..axis import DisplayUnitsLabelList + return DisplayUnitsLabelList + + +class TestDisplayUnitList: + + def test_ctor(self, DisplayUnitsLabelList): + axis = DisplayUnitsLabelList() + xml = tostring(axis.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DisplayUnitsLabelList): + src = """ + + """ + node = fromstring(src) + axis = DisplayUnitsLabelList.from_tree(node) + assert axis == DisplayUnitsLabelList() + + +@pytest.fixture +def ChartLines(): + from ..axis import ChartLines + return ChartLines + + +class TestChartLines: + + def test_ctor(self, ChartLines): + axis = ChartLines() + xml = tostring(axis.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ChartLines): + src = """ + + """ + node = fromstring(src) + axis = ChartLines.from_tree(node) + assert axis == ChartLines() diff --git a/openpyxl/chart/tests/test_bar_chart.py b/openpyxl/chart/tests/test_bar_chart.py new file mode 100644 index 0000000..9abab14 --- /dev/null +++ b/openpyxl/chart/tests/test_bar_chart.py @@ -0,0 +1,188 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring, fromstring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def BarChart(): + from ..bar_chart import BarChart + return BarChart + + +class TestBarChart: + + def test_ctor(self, BarChart): + bc = BarChart() + xml = tostring(bc.to_tree()) + expected = """ + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_tree(self, BarChart): + src = """ + + + + + + + + + """ + node = fromstring(src) + bc = BarChart.from_tree(node) + assert bc == BarChart(varyColors=False,axId=(10, 100)) + assert bc.axId == [10, 100] + assert bc.grouping == "clustered" + + + def test_write(self, BarChart): + chart = BarChart() + xml = tostring(chart._write()) + expected = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_series(self, BarChart): + from .. import Series + s1 = Series(values="Sheet1!$A$1:$A$10") + s2 = Series(values="Sheet1!$B$1:$B$10") + bc = BarChart(ser=[s1, s2]) + xml = tostring(bc.to_tree()) + expected = """ + + + + + + + + + + + + + + Sheet1!$A$1:$A$10 + + + + + + + + + + + + + + Sheet1!$B$1:$B$10 + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def BarChart3D(): + from ..bar_chart import BarChart3D + return BarChart3D + + +class TestBarChart3D: + + def test_ctor(self, BarChart3D): + bc = BarChart3D() + xml = tostring(bc.to_tree()) + expected = """ + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, BarChart3D): + src = """ + + + + + + + + + + """ + node = fromstring(src) + bc = BarChart3D.from_tree(node) + assert bc.axId == [10, 100, 0] diff --git a/openpyxl/chart/tests/test_bubble_chart.py b/openpyxl/chart/tests/test_bubble_chart.py new file mode 100644 index 0000000..6cf3360 --- /dev/null +++ b/openpyxl/chart/tests/test_bubble_chart.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def BubbleChart(): + from ..bubble_chart import BubbleChart + return BubbleChart + + +class TestBubbleChart: + + def test_ctor(self, BubbleChart): + bubble_chart = BubbleChart() + xml = tostring(bubble_chart.to_tree()) + expected = """ + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, BubbleChart): + src = """ + + + + + """ + node = fromstring(src) + bubble_chart = BubbleChart.from_tree(node) + assert bubble_chart.axId == [10, 20] diff --git a/openpyxl/chart/tests/test_chart.py b/openpyxl/chart/tests/test_chart.py new file mode 100644 index 0000000..66d9730 --- /dev/null +++ b/openpyxl/chart/tests/test_chart.py @@ -0,0 +1,134 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring +from openpyxl.tests.helper import compare_xml +from ..chartspace import PlotArea + +from ..series import Series + +@pytest.fixture +def ChartBase(): + from .._chart import ChartBase + return ChartBase + + +class TestChartBase: + + def test_ctor(self, ChartBase): + chart = ChartBase() + with pytest.raises(NotImplementedError): + xml = tostring(chart.to_tree()) + + + def test_iadd(self, ChartBase): + chart1 = ChartBase() + chart2 = ChartBase() + chart1 += chart2 + assert chart1._charts == [chart1, chart2] + + + def test_invalid_add(self, ChartBase): + chart = ChartBase() + s = Series() + with pytest.raises(TypeError): + chart += s + + + def test_set_catgories(self, ChartBase): + from ..series import Series + s1 = Series() + s1.__elements__ = ('cat',) + chart = ChartBase() + chart.ser = [s1] + chart.set_categories("Sheet!A1:A4") + xml = tostring(s1.to_tree()) + expected = """ + + + + Sheet!$A$1:$A$4 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_add_data_cols(self, ChartBase): + chart = ChartBase() + chart.ser = [] + chart.add_data("Sheet!A1:E4") + assert len(chart.ser) == 5 + assert chart.ser[0].val.numRef.f == "Sheet!$A$1:$A$4" + assert chart.ser[-1].val.numRef.f == "Sheet!$E$1:$E$4" + + + def test_add_data_rows(self, ChartBase): + chart = ChartBase() + chart.ser = [] + chart.add_data("Sheet!A1:E4", from_rows=True) + assert len(chart.ser) == 4 + assert chart.ser[0].val.numRef.f == "Sheet!$A$1:$E$1" + assert chart.ser[-1].val.numRef.f == "Sheet!$A$4:$E$4" + + + def test_hash_function(self, ChartBase): + chart = ChartBase() + assert hash(chart) == hash(id(chart)) + + + def test_path(self, ChartBase): + chart = ChartBase() + assert chart.path == "/xl/charts/chart1.xml" + + + def test_plot_area(self, ChartBase): + chart = ChartBase() + assert type(chart.plot_area) is PlotArea + + + def test_save_twice(self, ChartBase): + ChartBase.tagname = "DummyChart" + chart = ChartBase() + chart._write() + chart._write() + area = chart.plot_area + assert len(area._charts) == 1 + assert area._axes == [] + + + def test_axIds(self, ChartBase): + chart = ChartBase() + assert chart.axId == [] + + + def test_plot_visible_cells(self, ChartBase): + chart = ChartBase() + assert chart.visible_cells_only is True + + + def test_plot_visible_cells(self, ChartBase): + chart = ChartBase() + chart.visible_cells_only = False + tree = chart._write() + expected = """ + + + + + + + + + + + + + """ + xml = tostring(tree) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chart/tests/test_chartspace.py b/openpyxl/chart/tests/test_chartspace.py new file mode 100644 index 0000000..aa6bbba --- /dev/null +++ b/openpyxl/chart/tests/test_chartspace.py @@ -0,0 +1,289 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def ChartContainer(): + from ..chartspace import ChartContainer + return ChartContainer + + +class TestChartContainer: + + def test_ctor(self, ChartContainer): + container = ChartContainer() + xml = tostring(container.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ChartContainer): + src = """ + + + + + """ + node = fromstring(src) + container = ChartContainer.from_tree(node) + assert container == ChartContainer() + + +@pytest.fixture +def Surface(): + from .._3d import Surface + return Surface + + +class TestSurface: + + def test_ctor(self, Surface): + surface = Surface(thickness=0) + xml = tostring(surface.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Surface): + src = """ + + + + """ + node = fromstring(src) + surface = Surface.from_tree(node) + assert surface == Surface(thickness=0) + + +@pytest.fixture +def View3D(): + from .._3d import View3D + return View3D + + +class TestView3D: + + def test_ctor(self, View3D): + view = View3D() + xml = tostring(view.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, View3D): + src = """ + + + + + + + """ + node = fromstring(src) + view = View3D.from_tree(node) + assert view == View3D(rotX=15, rotY=20, rAngAx=False, perspective=30) + + +@pytest.fixture +def PivotFormat(): + from ..chartspace import PivotFormat + return PivotFormat + + +class TestPivotFormat: + + def test_ctor(self, PivotFormat): + fmt = PivotFormat() + xml = tostring(fmt.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PivotFormat): + src = """ + + + + """ + node = fromstring(src) + fmt = PivotFormat.from_tree(node) + assert fmt == PivotFormat() + + +@pytest.fixture +def PivotFormatList(): + from ..chartspace import PivotFormatList + return PivotFormatList + + +class TestPivotFormatList: + + def test_ctor(self, PivotFormatList): + fmt = PivotFormatList() + xml = tostring(fmt.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PivotFormatList): + src = """ + + """ + node = fromstring(src) + fmt = PivotFormatList.from_tree(node) + assert fmt == PivotFormatList() + + +@pytest.fixture +def Protection(): + from ..chartspace import Protection + return Protection + + +class TestProtection: + + def test_ctor(self, Protection): + prot = Protection() + xml = tostring(prot.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Protection): + src = """ + + + + """ + node = fromstring(src) + prot = Protection.from_tree(node) + assert prot == Protection(chartObject=True) + + +@pytest.fixture +def PivotSource(): + from ..chartspace import PivotSource + return PivotSource + + +class TestPivotSource: + + def test_ctor(self, PivotSource): + src = PivotSource(name="pivot source", fmtId=1) + xml = tostring(src.to_tree()) + expected = """ + + pivot source + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PivotSource): + src = """ + + pivot source + + + """ + node = fromstring(src) + src = PivotSource.from_tree(node) + assert src == PivotSource(name="pivot source", fmtId=1) + + +@pytest.fixture +def ExternalData(): + from ..chartspace import ExternalData + return ExternalData + + +class TestExternalData: + + def test_ctor(self, ExternalData): + data = ExternalData(id='rId1') + xml = tostring(data.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ExternalData): + src = """ + + """ + node = fromstring(src) + data = ExternalData.from_tree(node) + assert data == ExternalData(id="rId1") + + +@pytest.fixture +def ChartSpace(): + from ..chartspace import ChartSpace + return ChartSpace + + +class TestChartSpace: + + def test_ctor(self, ChartSpace, ChartContainer): + cs = ChartSpace(chart=ChartContainer()) + xml = tostring(cs.to_tree()) + expected = """ + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ChartSpace, ChartContainer): + src = """ + + + + """ + node = fromstring(src) + cs = ChartSpace.from_tree(node) + assert cs == ChartSpace(chart=ChartContainer()) diff --git a/openpyxl/chart/tests/test_data_source.py b/openpyxl/chart/tests/test_data_source.py new file mode 100644 index 0000000..48e710c --- /dev/null +++ b/openpyxl/chart/tests/test_data_source.py @@ -0,0 +1,154 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring, fromstring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def NumRef(): + from ..data_source import NumRef + return NumRef + + +class TestNumRef: + + + def test_from_xml(self, NumRef): + src = """ + + Blatt1!$A$1:$A$12 + + """ + node = fromstring(src) + num = NumRef.from_tree(node) + assert num.ref == "Blatt1!$A$1:$A$12" + + + def test_to_xml(self, NumRef): + num = NumRef(f="Blatt1!$A$1:$A$12") + xml = tostring(num.to_tree("numRef")) + expected = """ + + Blatt1!$A$1:$A$12 + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_tree_degree_sign(self, NumRef): + + src = b""" + + Hoja1!$A$2:$B$2 + + 0\xc2\xb0 + + + 3 + + + 14 + + + + """ + node = fromstring(src) + numRef = NumRef.from_tree(node) + assert numRef.numCache.formatCode == u"0\xb0" + + +@pytest.fixture +def StrRef(): + from ..data_source import StrRef + return StrRef + + +class TestStrRef: + + def test_ctor(self, StrRef): + data_source = StrRef(f="Sheet1!A1") + xml = tostring(data_source.to_tree()) + expected = """ + + Sheet1!A1 + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, StrRef): + src = """ + + 'Render Start'!$A$2 + + """ + node = fromstring(src) + data_source = StrRef.from_tree(node) + assert data_source == StrRef(f="'Render Start'!$A$2") + + +@pytest.fixture +def StrVal(): + from ..data_source import StrVal + return StrVal + + +class TestStrVal: + + def test_ctor(self, StrVal): + val = StrVal(v="something") + xml = tostring(val.to_tree()) + expected = """ + + something + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, StrVal): + src = """ + + else + + """ + node = fromstring(src) + val = StrVal.from_tree(node) + assert val == StrVal(idx=4, v="else") + + +@pytest.fixture +def StrData(): + from ..data_source import StrData + return StrData + + +class TestStrData: + + def test_ctor(self, StrData): + data_source = StrData(ptCount=1) + xml = tostring(data_source.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, StrData): + src = """ + + + + """ + node = fromstring(src) + data_source = StrData.from_tree(node) + assert data_source == StrData(ptCount=4) diff --git a/openpyxl/chart/tests/test_error_bar.py b/openpyxl/chart/tests/test_error_bar.py new file mode 100644 index 0000000..920876b --- /dev/null +++ b/openpyxl/chart/tests/test_error_bar.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def ErrorBars(): + from ..error_bar import ErrorBars + return ErrorBars + + +class TestErrorBar: + + def test_ctor(self, ErrorBars): + bar = ErrorBars() + xml = tostring(bar.to_tree()) + expected = """ + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ErrorBars): + src = """ + + + + + + + + """ + node = fromstring(src) + bar = ErrorBars.from_tree(node) + assert bar == ErrorBars(noEndCap=True, errDir='x', val=10) diff --git a/openpyxl/chart/tests/test_label.py b/openpyxl/chart/tests/test_label.py new file mode 100644 index 0000000..5ad4a36 --- /dev/null +++ b/openpyxl/chart/tests/test_label.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring, fromstring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def DataLabelList(): + from ..label import DataLabelList + return DataLabelList + + +class TestDataLabeList: + + def test_ctor(self, DataLabelList): + labels = DataLabelList(numFmt="0.0%") + xml = tostring(labels.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DataLabelList): + src = """ + + + + + + + + + """ + node = fromstring(src) + dl = DataLabelList.from_tree(node) + + assert dl.showLegendKey is False + assert dl.showVal is False + assert dl.showCatName is False + assert dl.showSerName is False + assert dl.showPercent is False + assert dl.showBubbleSize is False + + +@pytest.fixture +def DataLabel(): + from ..label import DataLabel + return DataLabel + + +class TestDataLabel: + + def test_ctor(self, DataLabel): + label = DataLabel() + xml = tostring(label.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DataLabel): + src = """ + + + + """ + node = fromstring(src) + label = DataLabel.from_tree(node) + assert label == DataLabel(idx=6) diff --git a/openpyxl/chart/tests/test_layout.py b/openpyxl/chart/tests/test_layout.py new file mode 100644 index 0000000..2d44926 --- /dev/null +++ b/openpyxl/chart/tests/test_layout.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def ManualLayout(): + from ..layout import ManualLayout + return ManualLayout + + +class TestManualLayout: + + def test_ctor(self, ManualLayout): + layout = ManualLayout( + layoutTarget="inner", + xMode="edge", + yMode="factor", + wMode="factor", + hMode="edge", + x=10, + y=50, + w=4, + h=100 + ) + xml = tostring(layout.to_tree()) + expected = """ + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ManualLayout): + src = """ + + + + + + + + + + + + """ + node = fromstring(src) + layout = ManualLayout.from_tree(node) + assert layout == ManualLayout(layoutTarget="inner", xMode="edge", + yMode="factor", wMode="factor", hMode="edge", x=10, y=50, w=4, h=100 + ) + + +class TestLayout: + + def test_ctor(self): + from ..layout import Layout + layout = Layout() + xml = tostring(layout.to_tree()) + diff = compare_xml(xml, "") + assert diff is None, diff diff --git a/openpyxl/chart/tests/test_legend.py b/openpyxl/chart/tests/test_legend.py new file mode 100644 index 0000000..8521f5b --- /dev/null +++ b/openpyxl/chart/tests/test_legend.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import + +# Copyright (c) 2010-2018 openpyxl +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def Legend(): + from ..legend import Legend + return Legend + + +class TestLegend: + + def test_ctor(self, Legend): + legend = Legend() + xml = tostring(legend.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Legend): + src = """ + + + + """ + node = fromstring(src) + legend = Legend.from_tree(node) + assert legend == Legend() + + +@pytest.fixture +def LegendEntry(): + from ..legend import LegendEntry + return LegendEntry + + +class TestLegendEntry: + + def test_ctor(self, LegendEntry): + legend = LegendEntry(idx=0, delete=True) + xml = tostring(legend.to_tree()) + expected = """ + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, LegendEntry): + src = """ + + + + """ + node = fromstring(src) + legend = LegendEntry.from_tree(node) + assert legend == LegendEntry() diff --git a/openpyxl/chart/tests/test_line_chart.py b/openpyxl/chart/tests/test_line_chart.py new file mode 100644 index 0000000..c5f4f23 --- /dev/null +++ b/openpyxl/chart/tests/test_line_chart.py @@ -0,0 +1,81 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def LineChart(): + from ..line_chart import LineChart + return LineChart + + +class TestLineChart: + + def test_ctor(self, LineChart): + chart = LineChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, LineChart): + src = """ + + + + + + """ + node = fromstring(src) + chart = LineChart.from_tree(node) + assert chart.axId == [10, 100] + assert chart.grouping == "stacked" + + + def test_axes(self, LineChart): + chart = LineChart() + assert set(chart._axes) == set([10, 100]) + + +@pytest.fixture +def LineChart3D(): + from ..line_chart import LineChart3D + return LineChart3D + + +class TestLineChart3D: + + def test_ctor(self, LineChart3D): + line_chart = LineChart3D() + xml = tostring(line_chart.to_tree()) + expected = """ + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, LineChart3D): + src = """ + + + + """ + node = fromstring(src) + line_chart = LineChart3D.from_tree(node) + assert line_chart == LineChart3D() diff --git a/openpyxl/chart/tests/test_marker.py b/openpyxl/chart/tests/test_marker.py new file mode 100644 index 0000000..10cfd35 --- /dev/null +++ b/openpyxl/chart/tests/test_marker.py @@ -0,0 +1,86 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def Marker(): + from ..marker import Marker + return Marker + + +class TestMarker: + + def test_ctor(self, Marker): + marker = Marker(symbol=None, size=5) + xml = tostring(marker.to_tree()) + expected = """ + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Marker): + src = """ + + + + + """ + node = fromstring(src) + marker = Marker.from_tree(node) + assert marker == Marker(symbol="square", size=5) + + +@pytest.fixture +def DataPoint(): + from ..marker import DataPoint + return DataPoint + + +class TestDataPoint: + + def test_ctor(self, DataPoint): + dp = DataPoint(idx=9) + xml = tostring(dp.to_tree()) + expected = """ + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DataPoint): + src = """ + + + + + + + + + """ + node = fromstring(src) + dp = DataPoint.from_tree(node) + assert dp.idx == 9 + assert dp.bubble3D is False diff --git a/openpyxl/chart/tests/test_picture.py b/openpyxl/chart/tests/test_picture.py new file mode 100644 index 0000000..4e3919f --- /dev/null +++ b/openpyxl/chart/tests/test_picture.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def PictureOptions(): + from ..picture import PictureOptions + return PictureOptions + + +class TestPictureOptions: + + def test_ctor(self, PictureOptions): + picture = PictureOptions() + xml = tostring(picture.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PictureOptions): + src = """ + + """ + node = fromstring(src) + picture = PictureOptions.from_tree(node) + assert picture == PictureOptions() diff --git a/openpyxl/chart/tests/test_pie_chart.py b/openpyxl/chart/tests/test_pie_chart.py new file mode 100644 index 0000000..ecaf4a1 --- /dev/null +++ b/openpyxl/chart/tests/test_pie_chart.py @@ -0,0 +1,183 @@ +from __future__ import absolute_import + +# Copyright (c) 2010-2018 openpyxl +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def PieChart(): + from ..pie_chart import PieChart + return PieChart + + +class TestPieChart: + + def test_ctor(self, PieChart): + chart = PieChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PieChart): + src = """ + + + + + + """ + node = fromstring(src) + chart = PieChart.from_tree(node) + assert dict(chart) == {} + assert chart.varyColors is True + assert chart.firstSliceAng == 60 + + +@pytest.fixture +def PieChart3D(): + from ..pie_chart import PieChart3D + return PieChart3D + + +class TestPieChart3D: + + def test_ctor(self, PieChart3D): + chart = PieChart3D() + xml = tostring(chart.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def DoughnutChart(): + from ..pie_chart import DoughnutChart + return DoughnutChart + + +class TestDoughnutChart: + + def test_ctor(self, DoughnutChart): + chart = DoughnutChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DoughnutChart): + src = """ + + + + + """ + node = fromstring(src) + chart = DoughnutChart.from_tree(node) + assert dict(chart) == {} + assert chart.firstSliceAng == 0 + assert chart.holeSize == 50 + + +@pytest.fixture +def ProjectedPieChart(): + from ..pie_chart import ProjectedPieChart + return ProjectedPieChart + + +class TestProjectedPieChart: + + def test_ctor(self, ProjectedPieChart): + chart = ProjectedPieChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ProjectedPieChart): + src = """ + + + + + + + + + + + + + + + + + + """ + node = fromstring(src) + chart = ProjectedPieChart.from_tree(node) + assert dict(chart) == {} + assert chart.gapWidth == 150 + assert chart.secondPieSize == 75 + + +@pytest.fixture +def CustomSplit(): + from ..pie_chart import CustomSplit + return CustomSplit + + +class TestCustomSplit: + + def test_ctor(self, CustomSplit): + pie_chart = CustomSplit([1, 2, 3]) + xml = tostring(pie_chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, CustomSplit): + src = """ + + + + + """ + node = fromstring(src) + pie_chart = CustomSplit.from_tree(node) + assert pie_chart == CustomSplit([1, 2]) diff --git a/openpyxl/chart/tests/test_plotarea.py b/openpyxl/chart/tests/test_plotarea.py new file mode 100644 index 0000000..de3f33c --- /dev/null +++ b/openpyxl/chart/tests/test_plotarea.py @@ -0,0 +1,148 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +from ..line_chart import LineChart +from ..bar_chart import BarChart + + +@pytest.fixture +def PlotArea(): + from ..plotarea import PlotArea + return PlotArea + + +class TestPlotArea: + + def test_ctor(self, PlotArea): + plot = PlotArea() + xml = tostring(plot.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PlotArea): + src = """ + + """ + node = fromstring(src) + plot = PlotArea.from_tree(node) + assert plot == PlotArea() + + + def test_multi_chart(self, PlotArea): + plot = PlotArea() + plot.lineChart = LineChart() + plot.barChart = BarChart() + plot.lineChart = LineChart() + expected = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + xml = tostring(plot.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_read_multi_chart(self, PlotArea, datadir): + datadir.chdir() + with open("plotarea.xml", "rb") as src: + tree = fromstring(src.read()) + plot = PlotArea.from_tree(tree) + assert len(plot._charts) == 2 + + + def test_read_multi_axes(self, PlotArea, datadir): + datadir.chdir() + with open("plotarea.xml", "rb") as src: + tree = fromstring(src.read()) + plot = PlotArea.from_tree(tree) + assert [ax.tagname for ax in plot._axes] == ["catAx", "valAx", "valAx", "catAx"] + assert plot._charts[0].x_axis == plot._axes[0] + assert plot._charts[0].y_axis == plot._axes[1] + assert plot._charts[1].x_axis == plot._axes[3] + assert plot._charts[1].y_axis == plot._axes[2] + + + def test_read_scatter_chart(self, PlotArea, datadir): + datadir.chdir() + with open("scatterchart_plot_area.xml", "rb") as src: + tree = fromstring(src.read()) + plot = PlotArea.from_tree(tree) + chart = plot._charts[0] + assert chart.axId == [211326240, 211330000] + assert chart.x_axis.axId == 211326240 + assert chart.y_axis.axId == 211330000 + + +@pytest.fixture +def DataTable(): + from ..plotarea import DataTable + return DataTable + + +class TestDataTable: + + def test_ctor(self, DataTable): + table = DataTable() + xml = tostring(table.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, DataTable): + src = """ + + """ + node = fromstring(src) + table = DataTable.from_tree(node) + assert table == DataTable() diff --git a/openpyxl/chart/tests/test_print.py b/openpyxl/chart/tests/test_print.py new file mode 100644 index 0000000..e2c1b50 --- /dev/null +++ b/openpyxl/chart/tests/test_print.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def PrintSettings(): + from ..print_settings import PrintSettings + return PrintSettings + + +class TestPrintSettings: + + def test_ctor(self, PrintSettings): + chartspace = PrintSettings() + xml = tostring(chartspace.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PrintSettings): + src = """ + + """ + node = fromstring(src) + chartspace = PrintSettings.from_tree(node) + assert chartspace == PrintSettings() + + +@pytest.fixture +def PageMargins(): + from ..print_settings import PageMargins + return PageMargins + + +class TestPageMargins: + + def test_ctor(self, PageMargins): + pm = PageMargins() + xml = tostring(pm.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, PageMargins): + src = """ + + """ + node = fromstring(src) + pm = PageMargins.from_tree(node) + assert pm == PageMargins() diff --git a/openpyxl/chart/tests/test_radar_chart.py b/openpyxl/chart/tests/test_radar_chart.py new file mode 100644 index 0000000..825f91e --- /dev/null +++ b/openpyxl/chart/tests/test_radar_chart.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def RadarChart(): + from ..radar_chart import RadarChart + return RadarChart + + +class TestRadarChart: + + def test_ctor(self, RadarChart): + chart = RadarChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, RadarChart): + src = """ + + + + + + + """ + node = fromstring(src) + chart = RadarChart.from_tree(node) + assert dict(chart) == {} + assert chart.type == "marker" + assert chart.axId == [2107159976, 2107207992] diff --git a/openpyxl/chart/tests/test_reader.py b/openpyxl/chart/tests/test_reader.py new file mode 100644 index 0000000..bb1d6f4 --- /dev/null +++ b/openpyxl/chart/tests/test_reader.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from zipfile import ZipFile + +from openpyxl.xml.functions import fromstring + +from .. line_chart import LineChart +from .. axis import NumericAxis, DateAxis +from .. chartspace import ChartSpace + + +def test_read(datadir): + datadir.chdir() + from .. reader import read_chart + + with open("chart1.xml") as src: + xml = src.read() + tree = fromstring(xml) + cs = ChartSpace.from_tree(tree) + chart = read_chart(cs) + + assert isinstance(chart, LineChart) + assert chart.title.tx.rich.p[0].r[0].t == "Website Performance" + + assert isinstance(chart.y_axis, NumericAxis) + assert chart.y_axis.title.tx.rich.p[0].r[0].t == "Time in seconds" + + assert isinstance(chart.x_axis, DateAxis) + assert chart.x_axis.title is None + + assert len(chart.series) == 10 + + +def test_read_drawing(datadir): + datadir.chdir() + + archive = ZipFile("sample.xlsx") + path = "xl/drawings/drawing1.xml" + + from ..reader import find_charts + charts = find_charts(archive, path) + assert len(charts) == 6 diff --git a/openpyxl/chart/tests/test_reference.py b/openpyxl/chart/tests/test_reference.py new file mode 100644 index 0000000..05ecd95 --- /dev/null +++ b/openpyxl/chart/tests/test_reference.py @@ -0,0 +1,102 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.compat import unicode + + +@pytest.fixture +def Reference(): + from ..reference import Reference + return Reference + + +@pytest.fixture +def Worksheet(): + + class DummyWorksheet: + + def __init__(self, title="dummy"): + self.title = title + + return DummyWorksheet + + +class TestReference: + + def test_ctor(self, Reference, Worksheet): + ref = Reference( + worksheet=Worksheet(), + min_col=1, + min_row=1, + max_col=10, + max_row=12 + ) + assert str(ref) == "dummy!$A$1:$J$12" + + + def test_single_cell(self, Reference, Worksheet): + ref = Reference(Worksheet(), min_col=1, min_row=1) + assert str(ref) == "dummy!$A$1" + + + def test_from_string(self, Reference): + ref = Reference(range_string="Sheet1!$A$1:$A$10") + assert (ref.min_col, ref.min_row, ref.max_col, ref.max_row) == (1,1, 1,10) + assert str(ref) == "Sheet1!$A$1:$A$10" + + + def test_cols(self, Reference): + ref = Reference(range_string="Sheet!A1:B2") + assert list(ref.cols) == [ + ('A1', 'A2'), + ('B1', 'B2') + ] + + + def test_rows(self, Reference): + ref = Reference(range_string="Sheet!A1:B2") + assert list(ref.rows) == [ + ('A1', 'B1'), + ('A2', 'B2') + ] + + + @pytest.mark.parametrize("range_string, cells", + [ + ("Sheet!A1:A5", ['A1', 'A2', 'A3', 'A4', 'A5']), + ("Sheet!A1:E1", ['A1', 'B1', 'C1', 'D1', 'E1']), + ] + ) + def test_cells(self, Reference, range_string, cells): + ref = Reference(range_string=range_string) + assert list(ref.cells) == cells + + + @pytest.mark.parametrize("range_string, cell, min_col, min_row", + [ + ("Sheet1!A1:A10", 'A1', 1, 2), + ("Sheet!A1:E1", 'A1', 2, 1), + ] + ) + def test_pop(self, Reference, range_string, cell, min_col, min_row): + ref = Reference(range_string=range_string) + assert cell == ref.pop() + assert (ref.min_col, ref.min_row) == (min_col, min_row) + + + @pytest.mark.parametrize("range_string, length", + [ + ("Sheet1!A1:A10", 10), + ("Sheet!A1:E1", 5), + ] + ) + def test_length(self, Reference, range_string, length): + ref = Reference(range_string=range_string) + assert len(ref) == length + + + def test_repr(self, Reference): + ref = Reference(range_string=b'D\xc3\xbcsseldorf!A1:A10'.decode("utf8")) + assert unicode(ref) == b'D\xc3\xbcsseldorf!$A$1:$A$10'.decode("utf8") diff --git a/openpyxl/chart/tests/test_scatter_chart.py b/openpyxl/chart/tests/test_scatter_chart.py new file mode 100644 index 0000000..652c102 --- /dev/null +++ b/openpyxl/chart/tests/test_scatter_chart.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def ScatterChart(): + from ..scatter_chart import ScatterChart + return ScatterChart + + +class TestScatterChart: + + def test_ctor(self, ScatterChart): + chart = ScatterChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, ScatterChart): + src = """ + + + + + """ + node = fromstring(src) + chart = ScatterChart.from_tree(node) + assert chart.axId == [10, 20] diff --git a/openpyxl/chart/tests/test_series.py b/openpyxl/chart/tests/test_series.py new file mode 100644 index 0000000..9d25b13 --- /dev/null +++ b/openpyxl/chart/tests/test_series.py @@ -0,0 +1,344 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring, fromstring +from openpyxl.tests.helper import compare_xml + + +class TestBarSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.val.numRef.ref == 'Blatt1!$A$1:$A$12' + + ser.__elements__ = attribute_mapping['bar'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +class TestAreaSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.val.numRef.ref == 'Blatt1!$A$1:$A$12' + + ser.__elements__ = attribute_mapping['area'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +class TestBubbleSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + + Blatt1!$B$1:$B$12 + + + + + General + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + 1.1 + + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.xVal.numRef.ref == 'Blatt1!$A$1:$A$12' + assert ser.yVal.numRef.ref == 'Blatt1!$B$1:$B$12' + assert ser.bubbleSize.numLit.ptCount == 12 + assert ser.bubbleSize.numLit.pt[0].v == 1.1 + + ser.__elements__ = attribute_mapping['bubble'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +class TestPieSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.val.numRef.ref == 'Blatt1!$A$1:$A$12' + + ser.__elements__ = attribute_mapping['pie'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + + +class TestRadarSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.val.numRef.ref == 'Blatt1!$A$1:$A$12' + + ser.__elements__ = attribute_mapping['radar'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +class TestScatterSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + + Blatt1!$B$1:$B$12 + + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.xVal.numRef.ref == 'Blatt1!$A$1:$A$12' + assert ser.yVal.numRef.ref == 'Blatt1!$B$1:$B$12' + + ser.__elements__ = attribute_mapping['scatter'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +class TestSurfaceSer: + + def test_from_tree(self): + from ..series import Series, attribute_mapping + + src = """ + + + + + + + + + + + Blatt1!$A$1:$A$12 + + + + """ + node = fromstring(src) + ser = Series.from_tree(node) + assert ser.idx == 0 + assert ser.order == 0 + assert ser.val.numRef.ref == 'Blatt1!$A$1:$A$12' + + ser.__elements__ = attribute_mapping['surface'] + xml = tostring(ser.to_tree()) + diff = compare_xml(xml, src) + assert diff is None, diff + + +@pytest.fixture +def SeriesLabel(): + from ..series import SeriesLabel + return SeriesLabel + + +class TestSeriesLabel: + + def test_ctor(self, SeriesLabel): + label = SeriesLabel(v="Label") + xml = tostring(label.to_tree()) + expected = """ + + Label + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, SeriesLabel): + src = """ + + Label + + """ + node = fromstring(src) + label = SeriesLabel.from_tree(node) + assert label == SeriesLabel(v="Label") diff --git a/openpyxl/chart/tests/test_series_factory.py b/openpyxl/chart/tests/test_series_factory.py new file mode 100644 index 0000000..23b8e8e --- /dev/null +++ b/openpyxl/chart/tests/test_series_factory.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def Series(): + from ..series_factory import SeriesFactory + return SeriesFactory + + +class TestSeriesFactory: + + def test_ctor(self, Series): + series = Series(values="Sheet1!$A$1:$A$10") + series.__elements__ = ('idx', 'order', 'val') + xml = tostring(series.to_tree()) + expected = """ + + + + + + Sheet1!$A$1:$A$10 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_manual_idx(self, Series): + series = Series(values="Sheet1!$A$1:$A$10") + series.__elements__ = ('idx', 'order', 'val') + xml = tostring(series.to_tree(idx=5)) + expected = """ + + + + + + Sheet1!$A$1:$A$10 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_manual_order(self, Series): + series = Series(values="Sheet1!$A$1:$A$10") + series.order = 2 + series.__elements__ = ('idx', 'order', 'val') + xml = tostring(series.to_tree(idx=5)) + expected = """ + + + + + + Sheet1!$A$1:$A$10 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_title(self, Series): + series = Series("Sheet1!A1:A10", title="First Series") + series.__elements__ = ('idx', 'order', 'tx') + xml = tostring(series.to_tree(idx=0)) + expected = """ + + + + + First Series + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_title_from_data(self, Series): + series = Series("Sheet1!A1:A10", title_from_data=True) + series.__elements__ = ('tx', 'val') + xml = tostring(series.to_tree(idx=0)) + expected = """ + + + + Sheet1!A1 + + + + + Sheet1!$A$2:$A$10 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_xy(self, Series): + from ..series import XYSeries + series = Series("Sheet!A1:A10", xvalues="Sheet!B1:B10") + assert isinstance(series, XYSeries) + + + def test_zvalues(self, Series): + series = Series("Sheet!A2:A5", xvalues="Sheet!B2:B5", zvalues="Sheet!C2:C5") + series.__elements__ = ('xVal', 'yVal', 'bubbleSize') + xml = tostring(series.to_tree()) + expected = """ + + + + Sheet!$B$2:$B$5 + + + + + Sheet!$A$2:$A$5 + + + + + Sheet!$C$2:$C$5 + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chart/tests/test_shapes.py b/openpyxl/chart/tests/test_shapes.py new file mode 100644 index 0000000..325b1aa --- /dev/null +++ b/openpyxl/chart/tests/test_shapes.py @@ -0,0 +1,49 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def GraphicalProperties(): + from ..shapes import GraphicalProperties + return GraphicalProperties + + +class TestShapeProperties: + + def test_ctor(self, GraphicalProperties): + shapes = GraphicalProperties() + xml = tostring(shapes.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, GraphicalProperties): + src = """ + + + + + + + + + + + + + + """ + node = fromstring(src) + shapes = GraphicalProperties.from_tree(node) + assert dict(shapes) == {} diff --git a/openpyxl/chart/tests/test_stock_chart.py b/openpyxl/chart/tests/test_stock_chart.py new file mode 100644 index 0000000..b751a82 --- /dev/null +++ b/openpyxl/chart/tests/test_stock_chart.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def StockChart(): + from ..stock_chart import StockChart + return StockChart + + +class TestStockChart: + + def test_ctor(self, StockChart): + from openpyxl.chart.series import Series + + chart = StockChart(ser=[Series(), Series(), Series()]) + xml = tostring(chart.to_tree()) + expected = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, StockChart): + src = """ + + + + + """ + node = fromstring(src) + chart = StockChart.from_tree(node) + assert chart.axId == [10, 100] diff --git a/openpyxl/chart/tests/test_surface_chart.py b/openpyxl/chart/tests/test_surface_chart.py new file mode 100644 index 0000000..29d9d4a --- /dev/null +++ b/openpyxl/chart/tests/test_surface_chart.py @@ -0,0 +1,165 @@ +from __future__ import absolute_import + +# Copyright (c) 2010-2018 openpyxl +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def SurfaceChart(): + from ..surface_chart import SurfaceChart + return SurfaceChart + + +class TestSurfaceChart: + + def test_ctor(self, SurfaceChart): + chart = SurfaceChart() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, SurfaceChart): + src = """ + + + + + + + + + + + + + + + + """ + node = fromstring(src) + chart = SurfaceChart.from_tree(node) + assert chart.axId == [2086876920, 2078923400, 2079274408] + + +@pytest.fixture +def SurfaceChart3D(): + from ..surface_chart import SurfaceChart3D + return SurfaceChart3D + + +class TestSurfaceChart3D: + + def test_ctor(self, SurfaceChart3D): + chart = SurfaceChart3D() + xml = tostring(chart.to_tree()) + expected = """ + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, SurfaceChart3D): + src = """ + + + + + + + + Blatt1!$A$1:$A$12 + + + + + + + + + Blatt1!$B$1:$B$12 + + + + + + + + + """ + node = fromstring(src) + chart = SurfaceChart3D.from_tree(node) + assert len(chart.ser) == 2 + assert chart.axId == [2082935272, 2082938248, 2082941288] + + +@pytest.fixture +def BandFormat(): + from ..surface_chart import BandFormat + return BandFormat + + +class TestBandFormat: + + def test_ctor(self, BandFormat): + fmt = BandFormat() + xml = tostring(fmt.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, BandFormat): + src = """ + + + + """ + node = fromstring(src) + fmt = BandFormat.from_tree(node) + assert fmt == BandFormat(idx=4) + + +@pytest.fixture +def BandFormatList(): + from ..surface_chart import BandFormatList + return BandFormatList + + +class TestBandFormatList: + + def test_ctor(self, BandFormatList): + fmt = BandFormatList() + xml = tostring(fmt.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, BandFormatList): + src = """ + + """ + node = fromstring(src) + fmt = BandFormatList.from_tree(node) + assert fmt == BandFormatList() diff --git a/openpyxl/chart/tests/test_text.py b/openpyxl/chart/tests/test_text.py new file mode 100644 index 0000000..baee94d --- /dev/null +++ b/openpyxl/chart/tests/test_text.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def RichText(): + from ..text import RichText + return RichText + + +class TestRichText: + + def test_ctor(self, RichText): + text = RichText() + xml = tostring(text.to_tree()) + expected = """ + + + + + + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, RichText): + src = """ + + """ + node = fromstring(src) + text = RichText.from_tree(node) + assert text == RichText() diff --git a/openpyxl/chart/tests/test_title.py b/openpyxl/chart/tests/test_title.py new file mode 100644 index 0000000..5b4921e --- /dev/null +++ b/openpyxl/chart/tests/test_title.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def Title(): + from ..title import Title + return Title + + +class TestTitle: + + def test_ctor(self, Title): + title = Title() + xml = tostring(title.to_tree()) + expected = """ + + <tx> + <rich> + <a:bodyPr></a:bodyPr> + <a:p> + <a:r> + <a:t /> + </a:r> + </a:p> + </rich> + </tx> + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Title): + src = """ + + """ + node = fromstring(src) + title = Title.from_tree(node) + assert title == Title() + + +def test_title_maker(): + """ + Create a title element from a string preserving line breaks. + """ + + from ..title import title_maker + text = "Two-line\nText" + title = title_maker(text) + xml = tostring(title.to_tree()) + expected = """ + <title xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"> + <tx> + <rich> + <a:bodyPr /> + <a:p> + <a:r> + <a:t>Two-line</a:t> + </a:r> + </a:p> + <a:p> + <a:r> + <a:t>Text</a:t> + </a:r> + </a:p> + </rich> + </tx> + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chart/tests/test_trendline.py b/openpyxl/chart/tests/test_trendline.py new file mode 100644 index 0000000..c936c12 --- /dev/null +++ b/openpyxl/chart/tests/test_trendline.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def TrendlineLabel(): + from ..trendline import TrendlineLabel + return TrendlineLabel + + +class TestTrendlineLabel: + + def test_ctor(self, TrendlineLabel): + trendline = TrendlineLabel() + xml = tostring(trendline.to_tree()) + expected = """ + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, TrendlineLabel): + src = """ + + """ + node = fromstring(src) + trendline = TrendlineLabel.from_tree(node) + assert trendline == TrendlineLabel() + + +@pytest.fixture +def Trendline(): + from ..trendline import Trendline + return Trendline + + +class TestTrendline: + + def test_ctor(self, Trendline): + trendline = Trendline() + xml = tostring(trendline.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, Trendline): + src = """ + + + + """ + node = fromstring(src) + trendline = Trendline.from_tree(node) + assert trendline == Trendline(trendlineType="log") diff --git a/openpyxl/chart/tests/test_updown_bars.py b/openpyxl/chart/tests/test_updown_bars.py new file mode 100644 index 0000000..1c2e063 --- /dev/null +++ b/openpyxl/chart/tests/test_updown_bars.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import + +# Copyright (c) 2010-2018 openpyxl +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def UpDownBars(): + from ..updown_bars import UpDownBars + return UpDownBars + + +class TestUpDownBars: + + def test_ctor(self, UpDownBars): + bars = UpDownBars(gapWidth=150) + xml = tostring(bars.to_tree()) + expected = """ + + + + """ + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_from_xml(self, UpDownBars): + src = """ + + + + """ + node = fromstring(src) + bars = UpDownBars.from_tree(node) + assert bars == UpDownBars(gapWidth=156) diff --git a/openpyxl/chart/text.py b/openpyxl/chart/text.py new file mode 100644 index 0000000..ce181d0 --- /dev/null +++ b/openpyxl/chart/text.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Alias, + Sequence, +) + + +from openpyxl.drawing.text import ( + RichTextProperties, + ListStyle, + Paragraph, +) + +from .data_source import StrRef + + +class RichText(Serialisable): + + """ + From the specification: 21.2.2.216 + + This element specifies text formatting. The lstStyle element is not supported. + """ + + tagname = "rich" + + bodyPr = Typed(expected_type=RichTextProperties) + properties = Alias("bodyPr") + lstStyle = Typed(expected_type=ListStyle, allow_none=True) + p = Sequence(expected_type=Paragraph) + paragraphs = Alias('p') + + __elements__ = ("bodyPr", "lstStyle", "p") + + def __init__(self, + bodyPr=None, + lstStyle=None, + p=None, + ): + if bodyPr is None: + bodyPr = RichTextProperties() + self.bodyPr = bodyPr + self.lstStyle = lstStyle + if p is None: + p = [Paragraph()] + self.p = p + + +class Text(Serialisable): + + strRef = Typed(expected_type=StrRef, allow_none=True) + rich = Typed(expected_type=RichText, allow_none=True) + + __elements__ = ("strRef", "rich") + + def __init__(self, + strRef=None, + rich=None + ): + self.strRef = strRef + if rich is None: + rich = RichText() + self.rich = rich diff --git a/openpyxl/chart/title.py b/openpyxl/chart/title.py new file mode 100644 index 0000000..dd256b2 --- /dev/null +++ b/openpyxl/chart/title.py @@ -0,0 +1,74 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.compat import basestring + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Alias, +) + +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import NestedBool + +from .text import Text, RichText +from .layout import Layout +from .shapes import GraphicalProperties + +from openpyxl.drawing.text import ( + Paragraph, + RegularTextRun, + LineBreak +) + + +class Title(Serialisable): + tagname = "title" + + tx = Typed(expected_type=Text, allow_none=True) + text = Alias('tx') + layout = Typed(expected_type=Layout, allow_none=True) + overlay = NestedBool(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + txPr = Typed(expected_type=RichText, allow_none=True) + body = Alias('txPr') + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('tx', 'layout', 'overlay', 'spPr', 'txPr') + + def __init__(self, + tx=None, + layout=None, + overlay=None, + spPr=None, + txPr=None, + extLst=None, + ): + if tx is None: + tx = Text() + self.tx = tx + self.layout = layout + self.overlay = overlay + self.spPr = spPr + self.txPr = txPr + + +def title_maker(text): + title = Title() + paras = [Paragraph(r=[RegularTextRun(t=s)]) for s in text.split("\n")] + + title.tx.rich.paragraphs = paras + return title + + +class TitleDescriptor(Typed): + + expected_type = Title + allow_none = True + + def __set__(self, instance, value): + if isinstance(value, basestring): + value = title_maker(value) + super(TitleDescriptor, self).__set__(instance, value) diff --git a/openpyxl/chart/trendline.py b/openpyxl/chart/trendline.py new file mode 100644 index 0000000..652461e --- /dev/null +++ b/openpyxl/chart/trendline.py @@ -0,0 +1,96 @@ +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + String, + Alias +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.nested import ( + NestedBool, + NestedInteger, + NestedFloat, + NestedSet +) + +from .data_source import NumFmt +from .shapes import GraphicalProperties +from .text import RichText, Text +from .layout import Layout + + +class TrendlineLabel(Serialisable): + + tagname = "trendlineLbl" + + layout = Typed(expected_type=Layout, allow_none=True) + tx = Typed(expected_type=Text, allow_none=True) + numFmt = Typed(expected_type=NumFmt, allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias("spPr") + txPr = Typed(expected_type=RichText, allow_none=True) + textProperties = Alias("txPr") + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('layout', 'tx', 'numFmt', 'spPr', 'txPr') + + def __init__(self, + layout=None, + tx=None, + numFmt=None, + spPr=None, + txPr=None, + extLst=None, + ): + self.layout = layout + self.tx = tx + self.numFmt = numFmt + self.spPr = spPr + self.txPr = txPr + + +class Trendline(Serialisable): + + tagname = "trendline" + + name = String(allow_none=True) + spPr = Typed(expected_type=GraphicalProperties, allow_none=True) + graphicalProperties = Alias('spPr') + trendlineType = NestedSet(values=(['exp', 'linear', 'log', 'movingAvg', 'poly', 'power'])) + order = NestedInteger(allow_none=True) + period = NestedInteger(allow_none=True) + forward = NestedFloat(allow_none=True) + backward = NestedFloat(allow_none=True) + intercept = NestedFloat(allow_none=True) + dispRSqr = NestedBool(allow_none=True) + dispEq = NestedBool(allow_none=True) + trendlineLbl = Typed(expected_type=TrendlineLabel, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('name', 'spPr', 'trendlineType', 'order', 'period', + 'forward', 'backward', 'intercept', 'dispRSqr', 'dispEq', 'trendlineLbl') + + def __init__(self, + name=None, + spPr=None, + trendlineType='linear', + order=None, + period=None, + forward=None, + backward=None, + intercept=None, + dispRSqr=None, + dispEq=None, + trendlineLbl=None, + extLst=None, + ): + self.name = name + self.spPr = spPr + self.trendlineType = trendlineType + self.order = order + self.period = period + self.forward = forward + self.backward = backward + self.intercept = intercept + self.dispRSqr = dispRSqr + self.dispEq = dispEq + self.trendlineLbl = trendlineLbl diff --git a/openpyxl/chart/updown_bars.py b/openpyxl/chart/updown_bars.py new file mode 100644 index 0000000..d44566c --- /dev/null +++ b/openpyxl/chart/updown_bars.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import Typed +from openpyxl.descriptors.excel import ExtensionList + +from .shapes import GraphicalProperties +from .axis import ChartLines +from .descriptors import NestedGapAmount + + +class UpDownBars(Serialisable): + + tagname = "upbars" + + gapWidth = NestedGapAmount() + upBars = Typed(expected_type=ChartLines, allow_none=True) + downBars = Typed(expected_type=ChartLines, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('gapWidth', 'upBars', 'downBars') + + def __init__(self, + gapWidth=150, + upBars=None, + downBars=None, + extLst=None, + ): + self.gapWidth = gapWidth + self.upBars = upBars + self.downBars = downBars diff --git a/openpyxl/chartsheet/__init__.py b/openpyxl/chartsheet/__init__.py new file mode 100644 index 0000000..17fc91f --- /dev/null +++ b/openpyxl/chartsheet/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from .chartsheet import Chartsheet diff --git a/openpyxl/chartsheet/chartsheet.py b/openpyxl/chartsheet/chartsheet.py new file mode 100644 index 0000000..627d443 --- /dev/null +++ b/openpyxl/chartsheet/chartsheet.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from weakref import ref + +from openpyxl.descriptors import Typed, Set, Alias +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.drawing.spreadsheet_drawing import ( + AbsoluteAnchor, + SpreadsheetDrawing, +) +from openpyxl.worksheet.page import ( + PageMargins, + PrintPageSetup +) +from openpyxl.packaging.relationship import Relationship, RelationshipList +from openpyxl.worksheet.drawing import Drawing +from openpyxl.worksheet.header_footer import HeaderFooter +from openpyxl.workbook.child import _WorkbookChild +from openpyxl.xml.constants import SHEET_MAIN_NS, REL_NS + +from .relation import DrawingHF, SheetBackgroundPicture +from .properties import ChartsheetProperties +from .protection import ChartsheetProtection +from .views import ChartsheetViewList +from .custom import CustomChartsheetViews +from .publish import WebPublishItems + + +class Chartsheet(_WorkbookChild, Serialisable): + + tagname = "chartsheet" + _default_title = "Chart" + _rel_type = "chartsheet" + _path = "/xl/chartsheets/sheet{0}.xml" + mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + + sheetPr = Typed(expected_type=ChartsheetProperties, allow_none=True) + sheetViews = Typed(expected_type=ChartsheetViewList) + sheetProtection = Typed(expected_type=ChartsheetProtection, allow_none=True) + customSheetViews = Typed(expected_type=CustomChartsheetViews, allow_none=True) + pageMargins = Typed(expected_type=PageMargins, allow_none=True) + pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True) + drawing = Typed(expected_type=Drawing, allow_none=True) + drawingHF = Typed(expected_type=DrawingHF, allow_none=True) + picture = Typed(expected_type=SheetBackgroundPicture, allow_none=True) + webPublishItems = Typed(expected_type=WebPublishItems, allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + sheet_state = Set(values=('visible', 'hidden', 'veryHidden')) + headerFooter = Typed(expected_type=HeaderFooter) + HeaderFooter = Alias('headerFooter') + + __elements__ = ( + 'sheetPr', 'sheetViews', 'sheetProtection', 'customSheetViews', + 'pageMargins', 'pageSetup', 'headerFooter', 'drawing', 'drawingHF', + 'picture', 'webPublishItems') + + __attrs__ = () + + def __init__(self, + sheetPr=None, + sheetViews=None, + sheetProtection=None, + customSheetViews=None, + pageMargins=None, + pageSetup=None, + headerFooter=None, + drawing=None, + drawingHF=None, + picture=None, + webPublishItems=None, + extLst=None, + parent=None, + title="", + sheet_state='visible', + ): + super(Chartsheet, self).__init__(parent, title) + self._charts = [] + self.sheetPr = sheetPr + if sheetViews is None: + sheetViews = ChartsheetViewList() + self.sheetViews = sheetViews + self.sheetProtection = sheetProtection + self.customSheetViews = customSheetViews + self.pageMargins = pageMargins + self.pageSetup = pageSetup + if headerFooter is not None: + self.headerFooter = headerFooter + self.drawing = Drawing("rId1") + self.drawingHF = drawingHF + self.picture = picture + self.webPublishItems = webPublishItems + self.sheet_state = sheet_state + + + def add_chart(self, chart): + chart.anchor = AbsoluteAnchor() + self._charts.append(chart) + + + def to_tree(self): + self._drawing = SpreadsheetDrawing() + self._drawing.charts = self._charts + tree = super(Chartsheet, self).to_tree() + if not self.headerFooter: + el = tree.find('headerFooter') + tree.remove(el) + tree.set("xmlns", SHEET_MAIN_NS) + return tree diff --git a/openpyxl/chartsheet/custom.py b/openpyxl/chartsheet/custom.py new file mode 100644 index 0000000..c9373ed --- /dev/null +++ b/openpyxl/chartsheet/custom.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.worksheet.header_footer import HeaderFooter + +from openpyxl.descriptors import ( + Bool, + Integer, + Set, + Typed, + Sequence +) +from openpyxl.descriptors.excel import Guid +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.worksheet.page import ( + PageMargins, + PrintPageSetup +) + + +class CustomChartsheetView(Serialisable): + tagname = "customSheetView" + + guid = Guid() + scale = Integer() + state = Set(values=(['visible', 'hidden', 'veryHidden'])) + zoomToFit = Bool(allow_none=True) + pageMargins = Typed(expected_type=PageMargins, allow_none=True) + pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True) + headerFooter = Typed(expected_type=HeaderFooter, allow_none=True) + + __elements__ = ('pageMargins', 'pageSetup', 'headerFooter') + + def __init__(self, + guid=None, + scale=None, + state='visible', + zoomToFit=None, + pageMargins=None, + pageSetup=None, + headerFooter=None, + ): + self.guid = guid + self.scale = scale + self.state = state + self.zoomToFit = zoomToFit + self.pageMargins = pageMargins + self.pageSetup = pageSetup + self.headerFooter = headerFooter + + +class CustomChartsheetViews(Serialisable): + tagname = "customSheetViews" + + customSheetView = Sequence(expected_type=CustomChartsheetView, allow_none=True) + + __elements__ = ('customSheetView',) + + def __init__(self, + customSheetView=None, + ): + self.customSheetView = customSheetView diff --git a/openpyxl/chartsheet/properties.py b/openpyxl/chartsheet/properties.py new file mode 100644 index 0000000..30ccad1 --- /dev/null +++ b/openpyxl/chartsheet/properties.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors import ( + Bool, + String, + Typed +) +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.styles import Color + + +class ChartsheetProperties(Serialisable): + tagname = "sheetPr" + + published = Bool(allow_none=True) + codeName = String(allow_none=True) + tabColor = Typed(expected_type=Color, allow_none=True) + + __elements__ = ('tabColor',) + + def __init__(self, + published=None, + codeName=None, + tabColor=None, + ): + self.published = published + self.codeName = codeName + self.tabColor = tabColor diff --git a/openpyxl/chartsheet/protection.py b/openpyxl/chartsheet/protection.py new file mode 100644 index 0000000..6d12392 --- /dev/null +++ b/openpyxl/chartsheet/protection.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +import hashlib + +from openpyxl.descriptors import (Bool, Integer, String) +from openpyxl.descriptors.excel import Base64Binary +from openpyxl.descriptors.serialisable import Serialisable + +from openpyxl.worksheet.protection import ( + hash_password, + _Protected +) + + +class ChartsheetProtection(Serialisable, _Protected): + tagname = "sheetProtection" + + algorithmName = String(allow_none=True) + hashValue = Base64Binary(allow_none=True) + saltValue = Base64Binary(allow_none=True) + spinCount = Integer(allow_none=True) + content = Bool(allow_none=True) + objects = Bool(allow_none=True) + + __attrs__ = ("content", "objects", "password", "hashValue", "spinCount", "saltValue", "algorithmName") + + def __init__(self, + content=None, + objects=None, + hashValue=None, + spinCount=None, + saltValue=None, + algorithmName=None, + password=None, + ): + self.content = content + self.objects = objects + self.hashValue = hashValue + self.spinCount = spinCount + self.saltValue = saltValue + self.algorithmName = algorithmName + if password is not None: + self.password = password diff --git a/openpyxl/chartsheet/publish.py b/openpyxl/chartsheet/publish.py new file mode 100644 index 0000000..7a49db1 --- /dev/null +++ b/openpyxl/chartsheet/publish.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors import ( + Bool, + Integer, + String, + Set, + Sequence +) +from openpyxl.descriptors.serialisable import Serialisable + + +class WebPublishItem(Serialisable): + tagname = "webPublishItem" + + id = Integer() + divId = String() + sourceType = Set(values=(['sheet', 'printArea', 'autoFilter', 'range', 'chart', 'pivotTable', 'query', 'label'])) + sourceRef = String() + sourceObject = String(allow_none=True) + destinationFile = String() + title = String(allow_none=True) + autoRepublish = Bool(allow_none=True) + + def __init__(self, + id=None, + divId=None, + sourceType=None, + sourceRef=None, + sourceObject=None, + destinationFile=None, + title=None, + autoRepublish=None, + ): + self.id = id + self.divId = divId + self.sourceType = sourceType + self.sourceRef = sourceRef + self.sourceObject = sourceObject + self.destinationFile = destinationFile + self.title = title + self.autoRepublish = autoRepublish + + +class WebPublishItems(Serialisable): + tagname = "WebPublishItems" + + count = Integer(allow_none=True) + webPublishItem = Sequence(expected_type=WebPublishItem, ) + + __elements__ = ('webPublishItem',) + + def __init__(self, + count=None, + webPublishItem=None, + ): + self.count = len(webPublishItem) + self.webPublishItem = webPublishItem diff --git a/openpyxl/chartsheet/relation.py b/openpyxl/chartsheet/relation.py new file mode 100644 index 0000000..fe9528d --- /dev/null +++ b/openpyxl/chartsheet/relation.py @@ -0,0 +1,98 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors import ( + Integer, + Alias +) +from openpyxl.descriptors.excel import Relation +from openpyxl.descriptors.serialisable import Serialisable + + +class SheetBackgroundPicture(Serialisable): + tagname = "picture" + id = Relation() + + def __init__(self, id): + self.id = id + + +class DrawingHF(Serialisable): + id = Relation() + lho = Integer(allow_none=True) + leftHeaderOddPages = Alias('lho') + lhe = Integer(allow_none=True) + leftHeaderEvenPages = Alias('lhe') + lhf = Integer(allow_none=True) + leftHeaderFirstPage = Alias('lhf') + cho = Integer(allow_none=True) + centerHeaderOddPages = Alias('cho') + che = Integer(allow_none=True) + centerHeaderEvenPages = Alias('che') + chf = Integer(allow_none=True) + centerHeaderFirstPage = Alias('chf') + rho = Integer(allow_none=True) + rightHeaderOddPages = Alias('rho') + rhe = Integer(allow_none=True) + rightHeaderEvenPages = Alias('rhe') + rhf = Integer(allow_none=True) + rightHeaderFirstPage = Alias('rhf') + lfo = Integer(allow_none=True) + leftFooterOddPages = Alias('lfo') + lfe = Integer(allow_none=True) + leftFooterEvenPages = Alias('lfe') + lff = Integer(allow_none=True) + leftFooterFirstPage = Alias('lff') + cfo = Integer(allow_none=True) + centerFooterOddPages = Alias('cfo') + cfe = Integer(allow_none=True) + centerFooterEvenPages = Alias('cfe') + cff = Integer(allow_none=True) + centerFooterFirstPage = Alias('cff') + rfo = Integer(allow_none=True) + rightFooterOddPages = Alias('rfo') + rfe = Integer(allow_none=True) + rightFooterEvenPages = Alias('rfe') + rff = Integer(allow_none=True) + rightFooterFirstPage = Alias('rff') + + def __init__(self, + id=None, + lho=None, + lhe=None, + lhf=None, + cho=None, + che=None, + chf=None, + rho=None, + rhe=None, + rhf=None, + lfo=None, + lfe=None, + lff=None, + cfo=None, + cfe=None, + cff=None, + rfo=None, + rfe=None, + rff=None, + ): + self.id = id + self.lho = lho + self.lhe = lhe + self.lhf = lhf + self.cho = cho + self.che = che + self.chf = chf + self.rho = rho + self.rhe = rhe + self.rhf = rhf + self.lfo = lfo + self.lfe = lfe + self.lff = lff + self.cfo = cfo + self.cfe = cfe + self.cff = cff + self.rfo = rfo + self.rfe = rfe + self.rff = rff diff --git a/openpyxl/chartsheet/tests/__init__.py b/openpyxl/chartsheet/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openpyxl/chartsheet/tests/test_chartsheet.py b/openpyxl/chartsheet/tests/test_chartsheet.py new file mode 100644 index 0000000..daaf547 --- /dev/null +++ b/openpyxl/chartsheet/tests/test_chartsheet.py @@ -0,0 +1,88 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.worksheet.drawing import Drawing +from openpyxl.worksheet.page import PageMargins +from ..views import ChartsheetView, ChartsheetViewList + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml +import pytest + +class DummyWorkbook: + + def __init__(self): + self.sheetnames = [] + self._charts = [] + + +@pytest.fixture +def Chartsheet(): + from ..chartsheet import Chartsheet + + return Chartsheet + +class TestChartsheet: + + def test_ctor(self, Chartsheet): + cs = Chartsheet(parent=DummyWorkbook()) + assert cs.title == "Chart" + + def test_read(self, Chartsheet): + src = """ + + + + + + + + + """ + xml = fromstring(src) + chart = Chartsheet.from_tree(xml) + assert chart.pageMargins.left == 0.7 + assert chart.sheetViews.sheetView[0].tabSelected == True + + def test_write(self, Chartsheet): + + sheetview = ChartsheetView(tabSelected=True, zoomScale=80, workbookViewId=0, zoomToFit=True) + chartsheetViews = ChartsheetViewList(sheetView=[sheetview]) + pageMargins = PageMargins(left=0.7, right=0.7, top=0.75, bottom=0.75, header=0.3, footer=0.3) + drawing = Drawing("rId1") + item = Chartsheet(sheetViews=chartsheetViews, pageMargins=pageMargins, drawing=drawing) + expected = """ + + + + + + + + """ + xml = tostring(item.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_write_charts(self, Chartsheet): + + class DummyChart: + + pass + + cs = Chartsheet(parent=DummyWorkbook()) + cs.add_chart(DummyChart()) + expected = """ + + + + + + + """ + xml = tostring(cs.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/tests/test_custom.py b/openpyxl/chartsheet/tests/test_custom.py new file mode 100644 index 0000000..31379a6 --- /dev/null +++ b/openpyxl/chartsheet/tests/test_custom.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.worksheet.page import PageMargins +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def CustomChartsheetView(): + from ..custom import CustomChartsheetView + + return CustomChartsheetView + + +class TestCustomChartsheetView: + def test_read(self, CustomChartsheetView): + src = """ + + + + + + """ + xml = fromstring(src) + customChartsheetView = CustomChartsheetView.from_tree(xml) + assert customChartsheetView.state == 'visible' + assert customChartsheetView.scale == 88 + assert customChartsheetView.pageMargins.left == 0.23622047244094491 + + def test_write(self, CustomChartsheetView): + + pageMargins = PageMargins(left=0.2362204724409449, right=0.2362204724409449, top=0.7480314960629921, + bottom=0.7480314960629921, header=0.3149606299212598, footer=0.3149606299212598) + customChartsheetView = CustomChartsheetView(guid="{C43F44F8-8CE9-4A07-A9A9-0646C7C6B826}", scale=88, + zoomToFit=1, + pageMargins=pageMargins) + expected = """ + + + + """ + + xml = tostring(customChartsheetView.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def CustomChartsheetViews(): + from ..custom import CustomChartsheetViews + + return CustomChartsheetViews + + +class TestCustomChartsheetViews: + def test_read(self, CustomChartsheetViews): + src = """ + + + + + + + + """ + xml = fromstring(src) + customChartsheetViews = CustomChartsheetViews.from_tree(xml) + assert customChartsheetViews.customSheetView[0].state == 'visible' + assert customChartsheetViews.customSheetView[0].scale == 88 + assert customChartsheetViews.customSheetView[0].pageMargins.left == 0.23622047244094491 + + def test_write(self, CustomChartsheetViews): + from ..custom import CustomChartsheetView + + pageMargins = PageMargins(left=0.2362204724409449, right=0.2362204724409449, top=0.7480314960629921, + bottom=0.7480314960629921, header=0.3149606299212598, footer=0.3149606299212598) + customChartsheetView = CustomChartsheetView(guid="{C43F44F8-8CE9-4A07-A9A9-0646C7C6B826}", scale=88, + zoomToFit=1, + pageMargins=pageMargins) + customChartsheetViews = CustomChartsheetViews(customSheetView=[customChartsheetView]) + expected = """ + + + + + + """ + + xml = tostring(customChartsheetViews.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/tests/test_properties.py b/openpyxl/chartsheet/tests/test_properties.py new file mode 100644 index 0000000..f8f8527 --- /dev/null +++ b/openpyxl/chartsheet/tests/test_properties.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + + +@pytest.fixture +def ChartsheetProperties(): + from ..properties import ChartsheetProperties + + return ChartsheetProperties + + +class TestChartsheetPr: + def test_read(self, ChartsheetProperties): + src = """ + + + + """ + xml = fromstring(src) + chartsheetPr = ChartsheetProperties.from_tree(xml) + assert chartsheetPr.codeName == "Chart1" + assert chartsheetPr.tabColor.rgb == "FFDCD8F4" + + def test_write(self, ChartsheetProperties): + from openpyxl.styles import Color + + chartsheetPr = ChartsheetProperties() + chartsheetPr.codeName = "Chart Openpyxl" + tabColor = Color(rgb="FFFFFFF4") + chartsheetPr.tabColor = tabColor + expected = """ + + + + """ + xml = tostring(chartsheetPr.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/tests/test_protection.py b/openpyxl/chartsheet/tests/test_protection.py new file mode 100644 index 0000000..6e90d65 --- /dev/null +++ b/openpyxl/chartsheet/tests/test_protection.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def ChartsheetProtection(): + from ..protection import ChartsheetProtection + + return ChartsheetProtection + + +class TestChartsheetProtection: + def test_read(self, ChartsheetProtection): + src = """ + + """ + xml = fromstring(src) + chartsheetProtection = ChartsheetProtection.from_tree(xml) + assert chartsheetProtection.algorithmName == "SHA-512" + assert chartsheetProtection.saltValue == "Bo89+SCcqbFEcOS/6LcjBw==" + + + def test_write(self, ChartsheetProtection): + chartsheetProtection = ChartsheetProtection() + chartsheetProtection.saltValue = "Bo89+SCcqbFEcOS/6LcjBw==" + chartsheetProtection.content = "1" + chartsheetProtection.objects = "1" + chartsheetProtection.algorithmName = "SHA-512" + chartsheetProtection.spinCount = "100000" + expected = """ + + """ + + xml = tostring(chartsheetProtection.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + + def test_password(self, ChartsheetProtection): + prot = ChartsheetProtection() + prot.password = "secret" + assert prot.password == "DAA7" diff --git a/openpyxl/chartsheet/tests/test_publish.py b/openpyxl/chartsheet/tests/test_publish.py new file mode 100644 index 0000000..2c83ff6 --- /dev/null +++ b/openpyxl/chartsheet/tests/test_publish.py @@ -0,0 +1,81 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def WebPublishItem(): + from ..publish import WebPublishItem + + return WebPublishItem + + +class TestWebPulishItem: + def test_read(self, WebPublishItem): + src = r""" + + """ + xml = fromstring(src) + webPulishItem = WebPublishItem.from_tree(xml) + assert webPulishItem.id == 6433 + assert webPulishItem.sourceObject == "Chart 1" + + def test_write(self, WebPublishItem): + webPublish = WebPublishItem(id=6433, divId="Views_6433", sourceType="chart", sourceRef="", + sourceObject="Chart 1", destinationFile=r"D:\Publish.mht", title="First Chart", + autoRepublish=False) + expected = r""" + + """ + xml = tostring(webPublish.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def WebPublishItems(): + from ..publish import WebPublishItems + + return WebPublishItems + + +class TestWebPublishItems: + def test_read(self, WebPublishItems): + src = r""" + + + + """ + xml = fromstring(src) + webPublishItems = WebPublishItems.from_tree(xml) + assert webPublishItems.count == 1 + assert webPublishItems.webPublishItem[0].sourceObject == "Chart 1" + + def test_write(self, WebPublishItems): + from ..publish import WebPublishItem + + webPublish_6433 = WebPublishItem(id=6433, divId="Views_6433", sourceType="chart", sourceRef="", + sourceObject="Chart 1", destinationFile=r"D:\Publish.mht", title="First Chart", + autoRepublish=False) + webPublish_64487 = WebPublishItem(id=64487, divId="Views_64487", sourceType="chart", sourceRef="Ref_545421", + sourceObject="Chart 15", destinationFile=r"D:\Publish_12.mht", + title="Second Chart", + autoRepublish=True) + webPublishItems = WebPublishItems(webPublishItem=[webPublish_6433, webPublish_64487]) + expected = r""" + + + + + """ + xml = tostring(webPublishItems.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/tests/test_relation.py b/openpyxl/chartsheet/tests/test_relation.py new file mode 100644 index 0000000..aa887bc --- /dev/null +++ b/openpyxl/chartsheet/tests/test_relation.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + + +@pytest.fixture +def SheetBackgroundPicture(): + from ..chartsheet import SheetBackgroundPicture + + return SheetBackgroundPicture + + +class TestSheetBackgroundPicture: + def test_read(self, SheetBackgroundPicture): + src = """ + + """ + xml = fromstring(src) + sheetBackgroundPicture = SheetBackgroundPicture.from_tree(xml) + assert sheetBackgroundPicture.id == "rId5" + + def test_write(self, SheetBackgroundPicture): + sheetBackgroundPicture = SheetBackgroundPicture(id="rId5") + expected = """ + + """ + xml = tostring(sheetBackgroundPicture.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + +@pytest.fixture +def DrawingHF(): + from ..chartsheet import DrawingHF + + return DrawingHF + + +class TestDrawingHF: + def test_read(self, DrawingHF): + src = """ + + """ + xml = fromstring(src) + drawingHF = DrawingHF.from_tree(xml) + assert drawingHF.lho == 7 + + def test_write(self, DrawingHF): + drawingHF = DrawingHF(lho=7, lhf=6, id='rId3') + expected = """ + + """ + xml = tostring(drawingHF.to_tree("drawingHF")) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/tests/test_views.py b/openpyxl/chartsheet/tests/test_views.py new file mode 100644 index 0000000..6a372fe --- /dev/null +++ b/openpyxl/chartsheet/tests/test_views.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +import pytest + +from openpyxl.xml.functions import fromstring, tostring +from openpyxl.tests.helper import compare_xml + +@pytest.fixture +def ChartsheetView(): + from ..views import ChartsheetView + + return ChartsheetView + + +class TestChartsheetView: + def test_read(self, ChartsheetView): + src = """ + + """ + xml = fromstring(src) + chart = ChartsheetView.from_tree(xml) + assert chart.tabSelected == True + + def test_write(self, ChartsheetView): + sheetview = ChartsheetView(tabSelected=True, zoomScale=80, workbookViewId=0, zoomToFit=True) + expected = """""" + xml = tostring(sheetview.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff + + +@pytest.fixture +def ChartsheetViewList(): + from ..views import ChartsheetViewList + return ChartsheetViewList + + +class TestChartsheetViewList: + + + def test_read(self, ChartsheetViewList): + src = """ + + + + """ + xml = fromstring(src) + views = ChartsheetViewList.from_tree(xml) + assert views.sheetView[0].tabSelected == 1 + + + def test_write(self, ChartsheetViewList): + views = ChartsheetViewList() + + expected = """ + + + + """ + xml = tostring(views.to_tree()) + diff = compare_xml(xml, expected) + assert diff is None, diff diff --git a/openpyxl/chartsheet/views.py b/openpyxl/chartsheet/views.py new file mode 100644 index 0000000..ee81caa --- /dev/null +++ b/openpyxl/chartsheet/views.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.descriptors import ( + Bool, + Integer, + Typed, + Sequence +) +from openpyxl.descriptors.excel import ExtensionList +from openpyxl.descriptors.serialisable import Serialisable + + +class ChartsheetView(Serialisable): + tagname = "sheetView" + + tabSelected = Bool(allow_none=True) + zoomScale = Integer(allow_none=True) + workbookViewId = Integer() + zoomToFit = Bool(allow_none=True) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = () + + def __init__(self, + tabSelected=None, + zoomScale=None, + workbookViewId=0, + zoomToFit=None, + extLst=None, + ): + self.tabSelected = tabSelected + self.zoomScale = zoomScale + self.workbookViewId = workbookViewId + self.zoomToFit = zoomToFit + + +class ChartsheetViewList(Serialisable): + tagname = "sheetViews" + + sheetView = Sequence(expected_type=ChartsheetView, ) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + __elements__ = ('sheetView',) + + def __init__(self, + sheetView=None, + extLst=None, + ): + if sheetView is None: + sheetView = [ChartsheetView()] + self.sheetView = sheetView diff --git a/openpyxl/comments/__init__.py b/openpyxl/comments/__init__.py new file mode 100644 index 0000000..574de25 --- /dev/null +++ b/openpyxl/comments/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + + +from .comments import Comment diff --git a/openpyxl/comments/author.py b/openpyxl/comments/author.py new file mode 100644 index 0000000..13f2b26 --- /dev/null +++ b/openpyxl/comments/author.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl +from openpyxl.compat import unicode + +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Sequence, + Alias +) + + +class AuthorList(Serialisable): + + tagname = "authors" + + author = Sequence(expected_type=unicode) + authors = Alias("author") + + def __init__(self, + author=(), + ): + self.author = author diff --git a/openpyxl/comments/comment_sheet.py b/openpyxl/comments/comment_sheet.py new file mode 100644 index 0000000..fb5ea2f --- /dev/null +++ b/openpyxl/comments/comment_sheet.py @@ -0,0 +1,232 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +## Incomplete! +from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.descriptors import ( + Typed, + Float, + Integer, + Set, + String, + Bool, +) +from openpyxl.descriptors.excel import Guid, ExtensionList +from openpyxl.descriptors.sequence import NestedSequence + +from openpyxl.utils.indexed_list import IndexedList +from openpyxl.xml.constants import SHEET_MAIN_NS +from openpyxl.xml.functions import tostring + +from openpyxl.cell.text import Text +from .author import AuthorList +from .comments import Comment +from .shape_writer import ShapeWriter + + +class ObjectAnchor(Serialisable): + + moveWithCells = Bool(allow_none=True) + sizeWithCells = Bool(allow_none=True) + #z-order = Integer(allow_none=True) needs alias + #from + #to defs from xdr + + def __init__(self, + moveWithCells=None, + sizeWithCells=None, + #z-order=None, + ): + self.moveWithCells = moveWithCells + self.sizeWithCells = sizeWithCells + #self.z-order = z-order + + +class Properties(Serialisable): + + locked = Bool(allow_none=True) + defaultSize = Bool(allow_none=True) + _print = Bool(allow_none=True) + disabled = Bool(allow_none=True) + uiObject = Bool(allow_none=True) + autoFill = Bool(allow_none=True) + autoLine = Bool(allow_none=True) + altText = String(allow_none=True) + textHAlign = Set(values=(['left', 'center', 'right', 'justify', 'distributed'])) + textVAlign = Set(values=(['top', 'center', 'bottom', 'justify', 'distributed'])) + lockText = Bool(allow_none=True) + justLastX = Bool(allow_none=True) + autoScale = Bool(allow_none=True) + rowHidden = Bool(allow_none=True) + colHidden = Bool(allow_none=True) + anchor = Typed(expected_type=ObjectAnchor, ) + + __elements__ = ('anchor',) + + def __init__(self, + locked=None, + defaultSize=None, + _print=None, + disabled=None, + uiObject=None, + autoFill=None, + autoLine=None, + altText=None, + textHAlign=None, + textVAlign=None, + lockText=None, + justLastX=None, + autoScale=None, + rowHidden=None, + colHidden=None, + anchor=None, + ): + self.locked = locked + self.defaultSize = defaultSize + self._print = _print + self.disabled = disabled + self.uiObject = uiObject + self.autoFill = autoFill + self.autoLine = autoLine + self.altText = altText + self.textHAlign = textHAlign + self.textVAlign = textVAlign + self.lockText = lockText + self.justLastX = justLastX + self.autoScale = autoScale + self.rowHidden = rowHidden + self.colHidden = colHidden + self.anchor = anchor + + +class CommentRecord(Serialisable): + + tagname = "comment" + + ref = String() + authorId = Integer() + guid = Guid(allow_none=True) + shapeId = Integer(allow_none=True) + text = Typed(expected_type=Text) + commentPr = Typed(expected_type=Properties, allow_none=True) + author = String(allow_none=True) + + __elements__ = ('text', 'commentPr') + __attrs__ = ('ref', 'authorId', 'guid', 'shapeId') + + def __init__(self, + ref="", + authorId=0, + guid=None, + shapeId=0, + text=None, + commentPr=None, + author=None, + height=79, + width=144 + ): + self.ref = ref + self.authorId = authorId + self.guid = guid + self.shapeId = shapeId + if text is None: + text = Text() + self.text = text + self.commentPr = commentPr + self.author = author + self.height = height + self.width = width + + + @classmethod + def from_cell(cls, cell): + """ + Class method to convert cell comment + """ + comment = cell._comment + ref = cell.coordinate + self = cls(ref=ref, author=comment.author) + self.text.t = comment.content + self.height = comment.height + self.width = comment.width + return self + + + @property + def content(self): + """ + Remove all inline formatting and stuff + """ + return self.text.content + + +class CommentSheet(Serialisable): + + tagname = "comments" + + authors = Typed(expected_type=AuthorList) + commentList = NestedSequence(expected_type=CommentRecord, count=0) + extLst = Typed(expected_type=ExtensionList, allow_none=True) + + _id = None + _path = "/xl/comments/comment{0}.xml" + mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + _rel_type = "comments" + _rel_id = None + + __elements__ = ('authors', 'commentList') + + def __init__(self, + authors=None, + commentList=None, + extLst=None, + ): + self.authors = authors + self.commentList = commentList + + + def to_tree(self): + tree = super(CommentSheet, self).to_tree() + tree.set("xmlns", SHEET_MAIN_NS) + return tree + + + @property + def comments(self): + """ + Return a dictionary of comments keyed by coord + """ + authors = self.authors.author + + for c in self.commentList: + yield c.ref, Comment(c.content, authors[c.authorId], c.height, c.width) + + + @classmethod + def from_comments(cls, comments): + """ + Create a comment sheet from a list of comments for a particular worksheet + """ + authors = IndexedList() + + # dedupe authors and get indexes + for comment in comments: + comment.authorId = authors.add(comment.author) + + return cls(authors=AuthorList(authors), commentList=comments) + + + def write_shapes(self, vml=None): + """ + Create the VML for comments + """ + sw = ShapeWriter(self.comments) + return sw.write(vml) + + + @property + def path(self): + """ + Return path within the archive + """ + return self._path.format(self._id) diff --git a/openpyxl/comments/comments.py b/openpyxl/comments/comments.py new file mode 100644 index 0000000..8aa2225 --- /dev/null +++ b/openpyxl/comments/comments.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + + +class Comment(object): + + _parent = None + + def __init__(self, text, author, height=79, width=144): + self.content = text + self.author = author + self.height = height + self.width = width + + + @property + def parent(self): + return self._parent + + + def __eq__(self, other): + return ( + self.content == other.content + and self.author == other.author + ) + + def __repr__(self): + return "Comment: {0} by {1}".format(self.content, self.author) + + + def __copy__(self): + """Create a detached copy of this comment.""" + clone = self.__class__(self.content, self.author, self.height, self.width) + return clone + + + def bind(self, cell): + """ + Bind comment to a particular cell + """ + if cell is not None and self._parent is not None and self._parent != cell: + fmt = "Comment already assigned to {0} in worksheet {1}. Cannot assign a comment to more than one cell" + raise AttributeError(fmt.format(cell.coordinate, cell.parent.title)) + self._parent = cell + + + def unbind(self): + """ + Unbind a comment from a cell + """ + self._parent = None + + + @property + def text(self): + """ + Any comment text stripped of all formatting. + """ + return self.content + + @text.setter + def text(self, value): + self.content = value diff --git a/openpyxl/comments/shape_writer.py b/openpyxl/comments/shape_writer.py new file mode 100644 index 0000000..ee2635f --- /dev/null +++ b/openpyxl/comments/shape_writer.py @@ -0,0 +1,117 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl + +from openpyxl.xml.functions import ( + Element, + SubElement, + tostring, + fromstring, +) + +from openpyxl.utils import ( + column_index_from_string, + coordinate_from_string, +) + +vmlns = "urn:schemas-microsoft-com:vml" +officens = "urn:schemas-microsoft-com:office:office" +excelns = "urn:schemas-microsoft-com:office:excel" + + +class ShapeWriter(object): + """ + Create VML for comments + """ + + vml = None + vml_path = None + + + def __init__(self, comments): + self.comments = comments + + + def add_comment_shapetype(self, root): + shape_layout = SubElement(root, "{%s}shapelayout" % officens, + {"{%s}ext" % vmlns: "edit"}) + SubElement(shape_layout, + "{%s}idmap" % officens, + {"{%s}ext" % vmlns: "edit", "data": "1"}) + shape_type = SubElement(root, + "{%s}shapetype" % vmlns, + {"id": "_x0000_t202", + "coordsize": "21600,21600", + "{%s}spt" % officens: "202", + "path": "m,l,21600r21600,l21600,xe"}) + SubElement(shape_type, "{%s}stroke" % vmlns, {"joinstyle": "miter"}) + SubElement(shape_type, + "{%s}path" % vmlns, + {"gradientshapeok": "t", + "{%s}connecttype" % officens: "rect"}) + + + def add_comment_shape(self, root, idx, coord, height, width): + col, row = coordinate_from_string(coord) + row -= 1 + column = column_index_from_string(col) - 1 + shape = _shape_factory(row, column, height, width) + + shape.set('id', "_x0000_s%04d" % idx) + root.append(shape) + + + def write(self, root): + + if not hasattr(root, "findall"): + root = Element("xml") + + # Remove any existing comment shapes + comments = root.findall("{%s}shape[@type='#_x0000_t202']" % vmlns) + for c in comments: + root.remove(c) + + # check whether comments shape type already exists + shape_types = root.find("{%s}shapetype[@id='_x0000_t202']" % vmlns) + if not shape_types: + self.add_comment_shapetype(root) + + for idx, (coord, comment) in enumerate(self.comments, 1026): + self.add_comment_shape(root, idx, coord, comment.height, comment.width) + + return tostring(root) + + +def _shape_factory(row, column, height, width): + style = ("position:absolute; " + "margin-left:59.25pt;" + "margin-top:1.5pt;" + "width:{width}px;" + "height:{height}px;" + "z-index:1;" + "visibility:hidden").format(height=height, + width=width) + attrs = { + "type": "#_x0000_t202", + "style": style, + "fillcolor": "#ffffe1", + "{%s}insetmode" % officens: "auto" + } + shape = Element("{%s}shape" % vmlns, attrs) + + SubElement(shape, "{%s}fill" % vmlns, + {"color2": "#ffffe1"}) + SubElement(shape, "{%s}shadow" % vmlns, + {"color": "black", "obscured": "t"}) + SubElement(shape, "{%s}path" % vmlns, + {"{%s}connecttype" % officens: "none"}) + textbox = SubElement(shape, "{%s}textbox" % vmlns, + {"style": "mso-direction-alt:auto"}) + SubElement(textbox, "div", {"style": "text-align:left"}) + client_data = SubElement(shape, "{%s}ClientData" % excelns, + {"ObjectType": "Note"}) + SubElement(client_data, "{%s}MoveWithCells" % excelns) + SubElement(client_data, "{%s}SizeWithCells" % excelns) + SubElement(client_data, "{%s}AutoFill" % excelns).text = "False" + SubElement(client_data, "{%s}Row" % excelns).text = str(row) + SubElement(client_data, "{%s}Column" % excelns).text = str(column) + return shape diff --git a/openpyxl/comments/tests/__init__.py b/openpyxl/comments/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openpyxl/comments/tests/conftest.py b/openpyxl/comments/tests/conftest.py new file mode 100644 index 0000000..3563b4c --- /dev/null +++ b/openpyxl/comments/tests/conftest.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +# Copyright (c) 2010-2018 openpyxl +import pytest + + +@pytest.fixture +def datadir(): + """DATADIR as a LocalPath""" + import os + here = os.path.split(__file__)[0] + DATADIR = os.path.join(here, "data") + from py._path.local import LocalPath + return LocalPath(DATADIR) + + +# objects under test diff --git a/openpyxl/comments/tests/data/comments.xlsx b/openpyxl/comments/tests/data/comments.xlsx new file mode 100644 index 0000000..f59c846 Binary files /dev/null and b/openpyxl/comments/tests/data/comments.xlsx differ diff --git a/openpyxl/comments/tests/data/comments1.xml b/openpyxl/comments/tests/data/comments1.xml new file mode 100644 index 0000000..5895378 --- /dev/null +++ b/openpyxl/comments/tests/data/comments1.xml @@ -0,0 +1,33 @@ + + + author2 + author + author3 + + + + + + + text2 + + + + + + + + text + + + + + + + + text3 + + + + + \ No newline at end of file diff --git a/openpyxl/comments/tests/data/comments2.xml b/openpyxl/comments/tests/data/comments2.xml new file mode 100644 index 0000000..3390bef --- /dev/null +++ b/openpyxl/comments/tests/data/comments2.xml @@ -0,0 +1,81 @@ + + + + Cuke + Not Cuke + + + + + + + + + + + + + Cuke: + + + + + + + + + + First Comment + + + + + + + + + + + + + + Cuke: + + + + + + + + + + Second Comment + + + + + + + + + + + + + + Not Cuke: + + + + + + + + + + Third Comment + + + + + diff --git a/openpyxl/comments/tests/data/commentsDrawing1.vml b/openpyxl/comments/tests/data/commentsDrawing1.vml new file mode 100644 index 0000000..7b74360 --- /dev/null +++ b/openpyxl/comments/tests/data/commentsDrawing1.vml @@ -0,0 +1,54 @@ + + + + + + + + +