From 5b3fc9f7cb28f7880da5d782d4d965eabfbceae6 Mon Sep 17 00:00:00 2001 From: huand Date: Tue, 14 Oct 2014 21:11:49 +0800 Subject: [PATCH 01/12] cht: add chart.data.ChartDataMoreDetails support: * categories with multiple levels * categories and vals with blanks * column letters exceeding 'Z' * formatCode --- pptx/chart/data.py | 289 +++++++++++++++++++++++++++++++++++++- pptx/chart/plot.py | 15 ++ pptx/chart/series.py | 29 ++++ pptx/chart/xlsx.py | 22 ++- pptx/oxml/__init__.py | 19 ++- pptx/oxml/chart/series.py | 183 +++++++++++++++++++++++- 6 files changed, 548 insertions(+), 9 deletions(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 95cf3e4cd..d75a31fb0 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -6,11 +6,13 @@ from __future__ import absolute_import, print_function, unicode_literals +import warnings + from ..oxml import parse_xml from ..oxml.ns import nsdecls from .xlsx import WorkbookWriter from .xmlwriter import ChartXmlWriter - +from xlsxwriter.utility import xl_rowcol_to_cell, xl_col_to_name class ChartData(object): """ @@ -308,3 +310,288 @@ def _values_ref(self): return "Sheet1!$%s$2:$%s$%d" % ( self._col_letter, self._col_letter, len(self._values)+1 ) + + +class ChartDataMoreDetails(ChartData): + """ + Subclass of ChartData, support categories and vals with more details: + categories with multiple levels, categories and vals with blanks. + + See also :class: `ChartData`. + """ + def __init__(self): + super(ChartDataMoreDetails, self).__init__() + self._categories_len = None + self._values_len = None + + @property + def categories_len(self): + """ + Read-write. The length of categories. Assigned value + will be applied to all sers + """ + return self._categories_len + + @categories_len.setter + def categories_len(self, value): + self._categories_len = value + #make sure all sers have this categories_len + for series in self._series_lst: + series._categories_len = value + + @property + def values_len(self): + """ + Read-write. The length of values.Assigned value + will be applied to all sers + """ + return self._values_len + + @values_len.setter + def values_len(self, value): + self._values_len = value + #make sure all sers have this values_len + for series in self._series_lst: + series._values_len = value + + def add_series(self, name, values, format_code=None): + """ + Add a series to this data set entitled *name* and the data points + specified by *values*, an iterable of numeric values. + """ + series_idx = len(self._series_lst) + series = _SeriesDataMoreDetails(series_idx, name, values, + self.categories, + values_len=self._values_len, + categories_len=self._categories_len, + format_code=format_code, + ) + self._series_lst.append(series) + + +class _SeriesDataMoreDetails(_SeriesData): + """ + Subclass of _SeriesData, support categories and vals with more details: + + * categories with multiple levels + * categories and vals with blanks + * column letters exceeding 'Z' + * formatCode + + Arguments : values & categories must be 2D sequence of (idx, value) + + See also : :class: `_SeriesData`. + """ + def __init__(self, series_idx, name, values, categories, + values_len=None, categories_len=None, format_code=None): + super(_SeriesDataMoreDetails, self).__init__(series_idx, name, + values, categories) + self._values_len = values_len or (max(i[0] for i in values[0]) + 1) + self._categories_len = categories_len or (max(max(j[0] for j in i) + for i in self._categories) + 1) + self._format_code = format_code or 'General' + + def __len__(self): + """ + The number of values this series contains. + """ + if not self._values_len == self._categories_len: + warnings.warn('Return max : Categories and Values have different \ +lengths. Will break data range adjustment by dragging in MS PowerPoint.') + return max(self._values_len, self._categories_len) + + @property + def format_code(self): + """ + format code string in ```` element + """ + return self._format_code + + @property + def is_cat_multilvl(self): + """ + whether ```` element has multiple levels + """ + if len(self._categories) > 1: + return True + else: + return False + + @property + def prefix(self): + """ + prefix for ```` and ```` element + """ + if self.is_cat_multilvl: + return 'multiLvlStr' + else: + return 'str' + + @property + def cat(self): + """ + The ```` element XML for this series, as an oxml element. + """ + xml = self._cat_tmpl.format( + prefix=self.prefix, + wksht_ref=self._categories_ref, cat_count=self._categories_len, + cat_pt_xml=self._cat_pt_xml, nsdecls=' %s' % nsdecls('c') + ) + return parse_xml(xml) + + @property + def cat_xml(self): + """ + The unicode XML snippet for the ```` element for this series, + containing the category labels and spreadsheet reference. + """ + return self._cat_tmpl.format( + prefix=self.prefix, + wksht_ref=self._categories_ref, cat_count=self._categories_len, + cat_pt_xml=self._cat_pt_xml, nsdecls='' + ) + + @property + def val(self): + """ + The ```` XML for this series, as an oxml element. + """ + xml = self._val_tmpl.format( + wksht_ref=self._values_ref, val_count=self._values_len, + format_code=self._format_code, + val_pt_xml=self._val_pt_xml, nsdecls=' %s' % nsdecls('c') + ) + return parse_xml(xml) + + @property + def val_xml(self): + """ + Return the unicode XML snippet for the ```` element describing + this series. + """ + return self._val_tmpl.format( + wksht_ref=self._values_ref, val_count=self._values_len, + format_code=self._format_code, + val_pt_xml=self._val_pt_xml, nsdecls='' + ) + + @property + def values(self): + """ + The values in this series as a tuple of a sequence of float. + """ + return self._values + + @property + def _categories_ref(self): + """ + The Excel worksheet reference to the categories for this series. + """ + end_col_number = len(self._categories) - 1 + end_row_number = self._categories_len + return "Sheet1!$A$2:%s" % xl_rowcol_to_cell( + end_row_number, end_col_number, + row_abs=True, col_abs=True) + + @property + def _cat_pt_xml(self): + """ + The unicode XML snippet for the ```` elements containing the + category names for this series. + """ + xml = '' + #16 spaces + if self.is_cat_multilvl: + lvl_start_tag = ' ' + '\n' + lvl_end_tag = ' ' + '\n' + pt_indent_spaces = ' ' + else: + lvl_start_tag = '' + lvl_end_tag = '' + pt_indent_spaces = '' + pt_xml = pt_indent_spaces.join( + ('', + ' \n', + ' %s\n', + ' \n',)) + #ref lvl is in reverse sequence in xml + loop_range = range(len(self._categories)) + loop_range.reverse() + for ilvl in loop_range: + lvl = self._categories[ilvl] + xml += lvl_start_tag + for idx, name in lvl: + xml += pt_xml % (idx, name) + xml += lvl_end_tag + return xml + + @property + def _cat_tmpl(self): + """ + The template for the ```` element for this series, containing + the category labels and spreadsheet reference. + """ + return ( + ' \n' + ' \n' + ' {wksht_ref}\n' + ' \n' + ' \n' + '{cat_pt_xml}' + ' \n' + ' \n' + ' \n' + ) + + @property + def _col_letter(self): + """ + The letter of the Excel worksheet column in which the data for this + series appears. + """ + return xl_col_to_name(len(self._categories) + self._series_idx) + + @property + def _val_pt_xml(self): + """ + The unicode XML snippet containing the ```` elements for this + series. + """ + xml = '' + for idx, value in self._values[0]: + xml += ( + ' \n' + ' %s\n' + ' \n' + ) % (idx, value) + return xml + + @property + def _val_tmpl(self): + """ + The template for the ```` element for this series, containing + the series values and their spreadsheet range reference. + """ + return ( + ' \n' + ' \n' + ' {wksht_ref}\n' + ' \n' + ' {format_code}\n' + ' \n' + '{val_pt_xml}' + ' \n' + ' \n' + ' \n' + ) + + @property + def _values_ref(self): + """ + The Excel worksheet reference to the values for this series (not + including the series name). + """ + return "Sheet1!${col_letter}$2:${col_letter}${end_row_number}".format( + col_letter = self._col_letter, + end_row_number = self._values_len + 1, + ) diff --git a/pptx/chart/plot.py b/pptx/chart/plot.py index 48906d26a..55a602e6d 100644 --- a/pptx/chart/plot.py +++ b/pptx/chart/plot.py @@ -37,6 +37,21 @@ def categories(self): category_pt_elms = xChart.cat_pts return tuple(pt.v.text for pt in category_pt_elms) + @property + def categories_tuples(self): + """ + Tuples containing the category indexes and strings for this plot. + If len > 1, the plot has multilvl categories. + """ + return self._element.iter_sers().next().cat.tuples_pts + + @property + def categories_len(self): + """ + Return length of categories, equal to ticks in categories axis + """ + return self._element.iter_sers().next().cat.val_ptCount + @property def chart(self): """ diff --git a/pptx/chart/series.py b/pptx/chart/series.py index 6f5f7f71c..0aebfed34 100644 --- a/pptx/chart/series.py +++ b/pptx/chart/series.py @@ -51,6 +51,35 @@ def values(self): value_pt_elms = ser.val_pts return tuple(pt.value for pt in value_pt_elms) + @property + def categories_tuples(self): + """ + Tuples containing the category indexes and strings for this series. + If len > 1, the series has multilvl categories. + """ + return self._element.cat.tuples_pts + + @property + def categories_len(self): + """ + Return length of categories, equal to ticks in categories axis + """ + return self._element.cat.val_ptCount + + @property + def values_tuple(self): + """ + Tuple containing the value indexes and value for this series. + """ + return self._element.val.tuples_pts + + @property + def values_len(self): + """ + Return length of values, equal to ticks in categories axis + """ + return self._element.val.val_ptCount + class BarSeries(_BaseSeries): """ diff --git a/pptx/chart/xlsx.py b/pptx/chart/xlsx.py index 681aeb4a7..0a1a611af 100644 --- a/pptx/chart/xlsx.py +++ b/pptx/chart/xlsx.py @@ -49,8 +49,20 @@ def _populate_worksheet(cls, worksheet, categories, series): as columns starting in second column, series title in first cell. Make the whole range an Excel List. """ - worksheet.write_column(1, 0, categories) - for series in series: - series_col = series.index + 1 - worksheet.write(0, series_col, series.name) - worksheet.write_column(1, series_col, series.values) + _sequence_types = (list, tuple) + if len(categories) != 0 and isinstance(categories[0], (list, tuple)): + for ilvl in xrange(len(categories)): + for idx, token in categories[ilvl]: + worksheet.write(1+idx, ilvl, token) + value_start_col = len(categories) + else: + worksheet.write_column(1, 0, categories) + value_start_col = 1 + for item_series in series: + series_col = item_series.index + value_start_col + worksheet.write(0, series_col, item_series.name) + if len(item_series.values) != 0 and isinstance(item_series.values[0], (list, tuple)): + for idx, token in item_series.values[0]: + worksheet.write(1+idx, series_col, token) + else: + worksheet.write_column(1, series_col, item_series.values) diff --git a/pptx/oxml/__init__.py b/pptx/oxml/__init__.py index 6ec67cfdf..a60da615e 100644 --- a/pptx/oxml/__init__.py +++ b/pptx/oxml/__init__.py @@ -84,9 +84,24 @@ def register_element_cls(nsptagname, cls): register_element_cls('c:pieChart', CT_PieChart) -from .chart.series import CT_SeriesComposite, CT_StrVal_NumVal_Composite +from .chart.series import ( + CT_SeriesComposite, CT_StrVal_NumVal_Composite, + CT_Val, CT_Cat, + CT_MultiLvlStrRef, CT_StrRef, CT_NumRef, + CT_MultiLvlStrCache, CT_Lvl, CT_StrCache, CT_NumCache +) register_element_cls('c:pt', CT_StrVal_NumVal_Composite) register_element_cls('c:ser', CT_SeriesComposite) +register_element_cls('c:val', CT_Val) +register_element_cls('c:cat', CT_Cat) +register_element_cls('c:multiLvlStrRef', CT_MultiLvlStrRef) +register_element_cls('c:strRef', CT_StrRef) +register_element_cls('c:numRef', CT_NumRef) +register_element_cls('c:multiLvlStrCache', CT_MultiLvlStrCache) +register_element_cls('c:lvl', CT_Lvl) +register_element_cls('c:strCache', CT_StrCache) +register_element_cls('c:numCache', CT_NumCache) + from .chart.shared import ( @@ -108,7 +123,7 @@ def register_element_cls(nsptagname, cls): register_element_cls('c:varyColors', CT_Boolean) register_element_cls('c:x', CT_Double) register_element_cls('c:xMode', CT_LayoutMode) - +register_element_cls('c:ptCount', CT_UnsignedInt) from .dml.color import ( CT_HslColor, CT_Percentage, CT_PresetColor, CT_SchemeColor, diff --git a/pptx/oxml/chart/series.py b/pptx/oxml/chart/series.py index af77b55a9..46689a91e 100644 --- a/pptx/oxml/chart/series.py +++ b/pptx/oxml/chart/series.py @@ -8,7 +8,7 @@ from ..simpletypes import XsdUnsignedInt from ..xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne + BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne, OneOrMore ) @@ -85,3 +85,184 @@ def value(self): The float value of the text in the required ```` child. """ return float(self.v.text) + +class _Base_Seq(BaseOxmlElement): + """ + base class for sequence element + provides similar properties and methods for element with ref and cache + """ + #ref element must be implemented in subclass + + @property + def ref(self): + """ + ref element, must be implemented in subclass + """ + raise NotImplemented('property ref must be implemented in subclass') + + @property + def text_ref(self): + """ + reference text + """ + return self.ref.text_cf + + @property + def val_ptCount(self): + """ + val of ptCount + """ + return self.ref.cache.ptCount.val + + @property + def tuples_pts(self): + """ + tuples of pts + """ + return (self.ref.cache.tuple_pts,) + + +class CT_Val(_Base_Seq): + """ + ```` element, contains values + """ + _ref = ZeroOrOne('c:numRef') + + @property + def ref(self): + """ + ref element of ```` child + """ + return self._ref + + +class CT_Cat(_Base_Seq): + """ + ```` element, contains categories + """ + _multilvlstrref = ZeroOrOne('c:multiLvlStrRef') + _strref = ZeroOrOne('c:strRef') + + @property + def is_multilvl(self): + """ + True if with ``c:multiLvlStrRef>`` child + """ + if self._multilvlstrref is None: + return False + else: + return True + + @property + def ref(self): + """ + ref element of ```` or ```` child + """ + if self.is_multilvl: + return self._multilvlstrref + else: + return self._strref + + @property + def tuples_pts(self): + """ + tuples of pts + """ + if self.is_multilvl: + return tuple( + (lvl.tuple_pts for lvl in self.ref.cache.lvl_lst) + ) + else: + return (self.ref.cache.tuple_pts,) + + +class _Base_Ref(BaseOxmlElement): + """ + base class for ````, ````, and ```` element + """ + cf = ZeroOrOne('c:f', successors=('c:multiLvlStrCache', 'c:strCache', 'c:numCache')) + + @property + def text_cf(self): + """ + reference text of ```` child. + """ + if not self.cf is None: + return self.cf.text + + +class CT_MultiLvlStrRef(_Base_Ref): + """ + ```` element + """ + cache = OneAndOnlyOne('c:multiLvlStrCache') + + +class CT_StrRef(_Base_Ref): + """ + ```` element + """ + cache = OneAndOnlyOne('c:strCache') + +class CT_NumRef(_Base_Ref): + """ + ```` element + """ + cache = OneAndOnlyOne('c:numCache') + + +class _Base_Cache(BaseOxmlElement): + """ + base class for ````, ````, and ```` element + """ + formatCode = ZeroOrOne('c:formatCode', successors=('c:ptCount',)) + ptCount = OneAndOnlyOne('c:ptCount') + + +class CT_MultiLvlStrCache(_Base_Cache): + """ + ```` element + """ + lvl = OneOrMore('c:lvl') + + +class _PtMixin(BaseOxmlElement): + """ + mixin class for ```` children, provides pt_tuples + """ + pt = OneOrMore('c:pt') + + @property + def tuple_pts(self): + """ + return pt tuples as : (idx, v.text) + """ + return tuple( + ((pt.idx, pt.v.text) for pt in self.pt_lst) + ) + + +class CT_Lvl(_PtMixin): + """ + ```` element + """ + pass + + +class CT_StrCache(_Base_Cache, _PtMixin): + """ + ```` element + """ + pass + + +class CT_NumCache(_Base_Cache, _PtMixin): + """ + ```` element + """ + @property + def text_fomatCode(self): + """ + return text of formatCode + """ + return self.formatCode.text From 482e8b30b7a54d8f9138da98614b53f926d760a3 Mon Sep 17 00:00:00 2001 From: huand Date: Fri, 17 Oct 2014 10:06:29 +0800 Subject: [PATCH 02/12] cht: chart values in tuple is now one tuple Old : (, ) New : --- pptx/chart/chart.py | 18 +++++++ pptx/chart/data.py | 104 ++++++++++++++++++++++++++++---------- pptx/chart/series.py | 7 +++ pptx/chart/xlsx.py | 10 ++-- pptx/oxml/chart/series.py | 11 ++-- 5 files changed, 115 insertions(+), 35 deletions(-) diff --git a/pptx/chart/chart.py b/pptx/chart/chart.py index 9070dd316..ec276fedb 100644 --- a/pptx/chart/chart.py +++ b/pptx/chart/chart.py @@ -14,6 +14,7 @@ from .plot import PlotFactory, PlotTypeInspector from .series import SeriesCollection from ..util import lazyproperty +from .data import ChartDataMoreDetails class Chart(object): @@ -147,6 +148,23 @@ def _workbook(self): """ return self._chart_part.chart_workbook + def get_data_more_details(self): + """ + return |ChartDataMoreDetails| object contains categories and series + in the xml of this chart + """ + chart_data = ChartDataMoreDetails() + chart_data.categories = self.series[0].categories_tuples + chart_data.categories_len = self.series[0].categories_len + for item_series in self.series: + chart_data.add_series( + item_series.name, + item_series.values_tuple, + values_len=item_series.values_len, + format_code=item_series.format_code, + ) + return chart_data + class Legend(object): """ diff --git a/pptx/chart/data.py b/pptx/chart/data.py index d75a31fb0..90ebf4b71 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -337,7 +337,7 @@ def categories_len(self, value): self._categories_len = value #make sure all sers have this categories_len for series in self._series_lst: - series._categories_len = value + series.categories_len = value @property def values_len(self): @@ -350,22 +350,28 @@ def values_len(self): @values_len.setter def values_len(self, value): self._values_len = value + if self._values_len < self._categories_len: + warnings.warn( + '''Length of values is less than that of categories. +Over bound categories will not be displayed.''') #make sure all sers have this values_len for series in self._series_lst: - series._values_len = value + series.values_len = value - def add_series(self, name, values, format_code=None): + + def add_series(self, name, values, values_len=None, format_code=None): """ Add a series to this data set entitled *name* and the data points specified by *values*, an iterable of numeric values. """ series_idx = len(self._series_lst) - series = _SeriesDataMoreDetails(series_idx, name, values, - self.categories, - values_len=self._values_len, - categories_len=self._categories_len, - format_code=format_code, - ) + series = _SeriesDataMoreDetails( + series_idx, name, values, + self.categories, + values_len = values_len or self._values_len, + categories_len=self._categories_len, + format_code=format_code, + ) self._series_lst.append(series) @@ -386,19 +392,36 @@ def __init__(self, series_idx, name, values, categories, values_len=None, categories_len=None, format_code=None): super(_SeriesDataMoreDetails, self).__init__(series_idx, name, values, categories) - self._values_len = values_len or (max(i[0] for i in values[0]) + 1) + self._values_len = values_len or max(i[0] for i in values) + 1 + self._auto_values_len = True if values_len else False self._categories_len = categories_len or (max(max(j[0] for j in i) for i in self._categories) + 1) + if self._values_len != self._categories_len: + warnings.warn('''Categories and Values have different lengths. + Will break data range adjustment by dragging in MS PowerPoint.''') self._format_code = format_code or 'General' - def __len__(self): + @property + def categories_len(self): """ - The number of values this series contains. + Read-write. The length of categories. """ - if not self._values_len == self._categories_len: - warnings.warn('Return max : Categories and Values have different \ -lengths. Will break data range adjustment by dragging in MS PowerPoint.') - return max(self._values_len, self._categories_len) + return self._categories_len + + @categories_len.setter + def categories_len(self, value): + self._categories_len = value + + @property + def values_len(self): + """ + Read-write. The length of values. + """ + return self._values_len + + @values_len.setter + def values_len(self, value): + self._values_len = value @property def format_code(self): @@ -500,10 +523,9 @@ def _cat_pt_xml(self): category names for this series. """ xml = '' - #16 spaces if self.is_cat_multilvl: - lvl_start_tag = ' ' + '\n' - lvl_end_tag = ' ' + '\n' + lvl_start_tag = ' \n' + lvl_end_tag = ' \n' pt_indent_spaces = ' ' else: lvl_start_tag = '' @@ -521,7 +543,9 @@ def _cat_pt_xml(self): lvl = self._categories[ilvl] xml += lvl_start_tag for idx, name in lvl: - xml += pt_xml % (idx, name) + #ignore idx out bound + if idx < self.categories_len: + xml += pt_xml % (idx, name) xml += lvl_end_tag return xml @@ -558,12 +582,13 @@ def _val_pt_xml(self): series. """ xml = '' - for idx, value in self._values[0]: - xml += ( - ' \n' - ' %s\n' - ' \n' - ) % (idx, value) + for idx, value in self._values: + if idx < self.values_len: + xml += ( + ' \n' + ' %s\n' + ' \n' + ) % (idx, value) return xml @property @@ -595,3 +620,30 @@ def _values_ref(self): col_letter = self._col_letter, end_row_number = self._values_len + 1, ) + + @property + def name(self): + """ + The name of this series. + """ + return self._name + + @name.setter + def name(self, value): + #name setter + self._name = value + + @property + def values(self): + """ + The values in this series as a sequence of float. + """ + return self._values + + @values.setter + def values(self, _values): + #values setter + self._values = _values + #update values len if auto + if self._auto_values_len: + self._values_len = max(i[0] for i in _values) + 1 diff --git a/pptx/chart/series.py b/pptx/chart/series.py index 0aebfed34..bf8f7e0ba 100644 --- a/pptx/chart/series.py +++ b/pptx/chart/series.py @@ -80,6 +80,13 @@ def values_len(self): """ return self._element.val.val_ptCount + @property + def format_code(self): + """ + Return formatCode + """ + return self._element.val.ref.cache.text_fomatCode + class BarSeries(_BaseSeries): """ diff --git a/pptx/chart/xlsx.py b/pptx/chart/xlsx.py index 0a1a611af..5365e501d 100644 --- a/pptx/chart/xlsx.py +++ b/pptx/chart/xlsx.py @@ -46,10 +46,9 @@ def _populate_worksheet(cls, worksheet, categories, series): """ Write *categories* and *series* to *worksheet* in the standard layout, categories in first column starting in second row, and series - as columns starting in second column, series title in first cell. - Make the whole range an Excel List. + as columns starting in column next to categories, series title in first + cell. Make the whole range an Excel List. """ - _sequence_types = (list, tuple) if len(categories) != 0 and isinstance(categories[0], (list, tuple)): for ilvl in xrange(len(categories)): for idx, token in categories[ilvl]: @@ -61,8 +60,9 @@ def _populate_worksheet(cls, worksheet, categories, series): for item_series in series: series_col = item_series.index + value_start_col worksheet.write(0, series_col, item_series.name) - if len(item_series.values) != 0 and isinstance(item_series.values[0], (list, tuple)): - for idx, token in item_series.values[0]: + if len(item_series.values) != 0 and isinstance( + item_series.values[0], tuple): + for idx, token in item_series.values: worksheet.write(1+idx, series_col, token) else: worksheet.write_column(1, series_col, item_series.values) diff --git a/pptx/oxml/chart/series.py b/pptx/oxml/chart/series.py index 46689a91e..58fe45c9f 100644 --- a/pptx/oxml/chart/series.py +++ b/pptx/oxml/chart/series.py @@ -98,7 +98,8 @@ def ref(self): """ ref element, must be implemented in subclass """ - raise NotImplemented('property ref must be implemented in subclass') + raise NotImplementedError( + 'property ref must be implemented in subclass') @property def text_ref(self): @@ -119,7 +120,7 @@ def tuples_pts(self): """ tuples of pts """ - return (self.ref.cache.tuple_pts,) + return self.ref.cache.tuple_pts class CT_Val(_Base_Seq): @@ -178,9 +179,11 @@ def tuples_pts(self): class _Base_Ref(BaseOxmlElement): """ - base class for ````, ````, and ```` element + base class for ````, ````, and ```` + element """ - cf = ZeroOrOne('c:f', successors=('c:multiLvlStrCache', 'c:strCache', 'c:numCache')) + cf = ZeroOrOne('c:f', successors=('c:multiLvlStrCache', 'c:strCache', + 'c:numCache')) @property def text_cf(self): From db5f08984112c704f16bebe3d5ca0733f01d2856 Mon Sep 17 00:00:00 2001 From: huand Date: Fri, 17 Oct 2014 16:21:01 +0800 Subject: [PATCH 03/12] Fix #123 * CT_ChartSpace.sers now just read oxml * add method _sort_and_correct_order_sers --- pptx/oxml/chart/chart.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pptx/oxml/chart/chart.py b/pptx/oxml/chart/chart.py index 2b4ffb5d6..089284b75 100644 --- a/pptx/oxml/chart/chart.py +++ b/pptx/oxml/chart/chart.py @@ -105,18 +105,22 @@ def last_doc_order_ser(self): def sers(self): """ An immutable sequence of the `c:ser` elements under this chartSpace - element, sorted in order of their `c:ser/c:idx/@val` value and with - any gaps in numbering collapsed. + element """ - def ser_idx(ser): - return ser.idx.val + return self.xpath('.//c:ser') - sers = sorted(self.xpath('.//c:ser'), key=ser_idx) + def _sort_and_correct_order_sers(self, by_='order'): + """ + Sorted in order of their `c:ser/c:order/@val` value or + `c:ser/c:idx/@val` value and with any gaps in numbering collapsed. + """ + if not by_ in ['order', 'idx']: + raise NotImplementedError('Only support ordering by order or idx') + sers = sorted(self.xpath('.//c:ser'), + key=lambda ser: ser.__getattribute__(by_)) for idx, ser in enumerate(sers): - if ser.idx.val != idx: - ser.idx.val = idx - ser.order.val = idx - return sers + ser.idx.val = idx + ser.order.val = idx @property def valAx(self): From e9e2c33190d58fb29bb0c7324cc548e0cd547ea2 Mon Sep 17 00:00:00 2001 From: "huandzh@gmail.com" Date: Sat, 18 Oct 2014 02:22:38 +0800 Subject: [PATCH 04/12] Fix #123 : Mode property CT_ChartSpace.sers oxml readonly and compatible * sers sorted by `idx` * add method reindex_sers (support reindex as previous behavior) * add method reset_order_sers (avoid duplicate `order` * compatibility for _SeriesRewriter --- pptx/chart/chart.py | 1 + pptx/oxml/chart/chart.py | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pptx/chart/chart.py b/pptx/chart/chart.py index 9070dd316..dc4ded8b1 100644 --- a/pptx/chart/chart.py +++ b/pptx/chart/chart.py @@ -277,6 +277,7 @@ def _adjust_ser_count(cls, chartSpace, new_ser_count): increasing order of the c:ser/c:idx value, starting with 0 and with any gaps in numbering collapsed. """ + chartSpace.reindex_sers() ser_count_diff = new_ser_count - len(chartSpace.sers) if ser_count_diff > 0: cls._add_cloned_sers(chartSpace, ser_count_diff) diff --git a/pptx/oxml/chart/chart.py b/pptx/oxml/chart/chart.py index 089284b75..824eb2db8 100644 --- a/pptx/oxml/chart/chart.py +++ b/pptx/oxml/chart/chart.py @@ -105,23 +105,30 @@ def last_doc_order_ser(self): def sers(self): """ An immutable sequence of the `c:ser` elements under this chartSpace - element + element in order of `c:ser/c:idx/@val` (same as Official VBA API : + Chart.SeriesCollection(Index)) """ - return self.xpath('.//c:ser') + return sorted(self.xpath('.//c:ser'), key=lambda ser: ser.idx.val) - def _sort_and_correct_order_sers(self, by_='order'): + def reindex_sers(self): """ - Sorted in order of their `c:ser/c:order/@val` value or - `c:ser/c:idx/@val` value and with any gaps in numbering collapsed. + Reindex `c:ser` elements in order of their `c:ser/c:idx/@val` value + and with any gaps in numbering collapsed. (method for backwards + compatible) Beware : Altering `c:ser/c:idx/@val` value frequently + change auto style of the series """ - if not by_ in ['order', 'idx']: - raise NotImplementedError('Only support ordering by order or idx') - sers = sorted(self.xpath('.//c:ser'), - key=lambda ser: ser.__getattribute__(by_)) - for idx, ser in enumerate(sers): - ser.idx.val = idx + for idx, ser in enumerate(self.sers): + if ser.idx.val != idx: + ser.idx.val = idx ser.order.val = idx + def reset_order_sers(self): + """ + Duplicated `c:ser/c:order/@val` is not allowed in MS PowerPoint, reset + order starting from zero""" + for order, ser in enumerate(self.sers): + ser.order.val = order + @property def valAx(self): return self.chart.valAx From 9c886cdfc8e5edd274d4a6d5781f06930631ab7f Mon Sep 17 00:00:00 2001 From: huand Date: Wed, 22 Oct 2014 19:32:48 +0800 Subject: [PATCH 05/12] chart:CT_NumCache.tuple_pts return (idx, float) istead of (idx, text) --- pptx/oxml/chart/series.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pptx/oxml/chart/series.py b/pptx/oxml/chart/series.py index 58fe45c9f..02c5843ee 100644 --- a/pptx/oxml/chart/series.py +++ b/pptx/oxml/chart/series.py @@ -269,3 +269,12 @@ def text_fomatCode(self): return text of formatCode """ return self.formatCode.text + + @property + def tuple_pts(self): + """ + return pt tuples as : (idx, float(v.text)) + """ + return tuple( + ((pt.idx, float(pt.v.text)) for pt in self.pt_lst) + ) From 7634386edf1dea0c52a1dda565164e581bb47f9b Mon Sep 17 00:00:00 2001 From: huand Date: Thu, 23 Oct 2014 11:47:37 +0800 Subject: [PATCH 06/12] CT_NumCache.tuple_pts try return float and return text if failed --- pptx/oxml/chart/series.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pptx/oxml/chart/series.py b/pptx/oxml/chart/series.py index 02c5843ee..d58f28b5c 100644 --- a/pptx/oxml/chart/series.py +++ b/pptx/oxml/chart/series.py @@ -273,8 +273,14 @@ def text_fomatCode(self): @property def tuple_pts(self): """ - return pt tuples as : (idx, float(v.text)) - """ - return tuple( - ((pt.idx, float(pt.v.text)) for pt in self.pt_lst) - ) + try return pt tuple as : (idx, float(v.text)) + and return (idx, v.text) if failed + """ + pt_tuple_lst = list() + for pt in self.pt_lst: + try: + value = float(pt.v.text) + except ValueError: + value = pt.v.text + pt_tuple_lst.append((pt.idx, value)) + return tuple(pt_tuple_lst) From 55e26f3f57dfe8cab4eb15097c1e50486b5536a0 Mon Sep 17 00:00:00 2001 From: huand Date: Mon, 27 Oct 2014 11:12:03 +0800 Subject: [PATCH 07/12] Chart: support no categories and no series --- pptx/chart/chart.py | 12 +++++++++--- pptx/chart/data.py | 39 +++++++++++++++++++++++++-------------- pptx/chart/series.py | 6 ++++-- pptx/oxml/chart/series.py | 4 ++++ 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pptx/chart/chart.py b/pptx/chart/chart.py index 0b5286e8d..3094e615d 100644 --- a/pptx/chart/chart.py +++ b/pptx/chart/chart.py @@ -154,8 +154,13 @@ def get_data_more_details(self): in the xml of this chart """ chart_data = ChartDataMoreDetails() - chart_data.categories = self.series[0].categories_tuples - chart_data.categories_len = self.series[0].categories_len + if len(self.series) > 0: + categories = self.series[0].categories_tuples + else: + categories = None + if not categories is None: + chart_data.categories = categories + chart_data.categories_len = self.series[0].categories_len for item_series in self.series: chart_data.add_series( item_series.name, @@ -313,7 +318,8 @@ def _rewrite_ser_data(cls, ser, series_data): ser._remove_cat() ser._remove_val() ser._insert_tx(series_data.tx) - ser._insert_cat(series_data.cat) + if not series_data.cat is None: + ser._insert_cat(series_data.cat) ser._insert_val(series_data.val) @classmethod diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 90ebf4b71..a29c6b161 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -394,8 +394,12 @@ def __init__(self, series_idx, name, values, categories, values, categories) self._values_len = values_len or max(i[0] for i in values) + 1 self._auto_values_len = True if values_len else False - self._categories_len = categories_len or (max(max(j[0] for j in i) - for i in self._categories) + 1) + if categories_len is None and (not self._categories is None) and ( + len(self._categories) != 0): + self._categories_len = max(max(j[0] for j in i) + for i in self._categories) + 1 + else: + self._categories_len = categories_len if self._values_len != self._categories_len: warnings.warn('''Categories and Values have different lengths. Will break data range adjustment by dragging in MS PowerPoint.''') @@ -455,12 +459,16 @@ def cat(self): """ The ```` element XML for this series, as an oxml element. """ - xml = self._cat_tmpl.format( - prefix=self.prefix, - wksht_ref=self._categories_ref, cat_count=self._categories_len, - cat_pt_xml=self._cat_pt_xml, nsdecls=' %s' % nsdecls('c') - ) - return parse_xml(xml) + if self._categories_len > 0: + xml = self._cat_tmpl.format( + prefix=self.prefix, + wksht_ref=self._categories_ref, cat_count=self._categories_len, + cat_pt_xml=self._cat_pt_xml, nsdecls=' %s' % nsdecls('c') + ) + return parse_xml(xml) + else: + return None + @property def cat_xml(self): @@ -468,11 +476,14 @@ def cat_xml(self): The unicode XML snippet for the ```` element for this series, containing the category labels and spreadsheet reference. """ - return self._cat_tmpl.format( - prefix=self.prefix, - wksht_ref=self._categories_ref, cat_count=self._categories_len, - cat_pt_xml=self._cat_pt_xml, nsdecls='' - ) + if self._categories_len > 0: + return self._cat_tmpl.format( + prefix=self.prefix, + wksht_ref=self._categories_ref, cat_count=self._categories_len, + cat_pt_xml=self._cat_pt_xml, nsdecls='' + ) + else: + return '' @property def val(self): @@ -573,7 +584,7 @@ def _col_letter(self): The letter of the Excel worksheet column in which the data for this series appears. """ - return xl_col_to_name(len(self._categories) + self._series_idx) + return xl_col_to_name(max(1, len(self._categories)) + self._series_idx) @property def _val_pt_xml(self): diff --git a/pptx/chart/series.py b/pptx/chart/series.py index bf8f7e0ba..1ce8ae4cd 100644 --- a/pptx/chart/series.py +++ b/pptx/chart/series.py @@ -57,14 +57,16 @@ def categories_tuples(self): Tuples containing the category indexes and strings for this series. If len > 1, the series has multilvl categories. """ - return self._element.cat.tuples_pts + if not self._element.cat is None: + return self._element.cat.tuples_pts @property def categories_len(self): """ Return length of categories, equal to ticks in categories axis """ - return self._element.cat.val_ptCount + if not self._element.cat is None: + return self._element.cat.val_ptCount @property def values_tuple(self): diff --git a/pptx/oxml/chart/series.py b/pptx/oxml/chart/series.py index d58f28b5c..710dd5152 100644 --- a/pptx/oxml/chart/series.py +++ b/pptx/oxml/chart/series.py @@ -143,6 +143,7 @@ class CT_Cat(_Base_Seq): """ _multilvlstrref = ZeroOrOne('c:multiLvlStrRef') _strref = ZeroOrOne('c:strRef') + _numref = ZeroOrOne('c:numRef') @property def is_multilvl(self): @@ -161,9 +162,12 @@ def ref(self): """ if self.is_multilvl: return self._multilvlstrref + elif not self._numref is None: + return self._numref else: return self._strref + @property def tuples_pts(self): """ From 10f21f4acc27ee75f01dd0386a14f1bb16c2e3a3 Mon Sep 17 00:00:00 2001 From: huand Date: Thu, 30 Oct 2014 13:02:24 +0800 Subject: [PATCH 08/12] Escape series name --- pptx/chart/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 95cf3e4cd..617cc01af 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -10,7 +10,7 @@ from ..oxml.ns import nsdecls from .xlsx import WorkbookWriter from .xmlwriter import ChartXmlWriter - +from xml.sax.saxutils import escape class ChartData(object): """ @@ -144,7 +144,7 @@ def tx(self): series name. """ xml = self._tx_tmpl.format( - wksht_ref=self._series_name_ref, series_name=self.name, + wksht_ref=self._series_name_ref, series_name=escape(self.name), nsdecls=' %s' % nsdecls('c') ) return parse_xml(xml) @@ -156,7 +156,7 @@ def tx_xml(self): element contains the series name. """ return self._tx_tmpl.format( - wksht_ref=self._series_name_ref, series_name=self.name, + wksht_ref=self._series_name_ref, series_name=escape(self.name), nsdecls='' ) From a6a19b8626b35bc76df15d0fa0d82db73de3b0f9 Mon Sep 17 00:00:00 2001 From: huandzh Date: Tue, 4 Nov 2014 10:45:30 +0800 Subject: [PATCH 09/12] escape `` text of categories --- pptx/chart/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 617cc01af..969ec803d 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -209,7 +209,7 @@ def _cat_pt_xml(self): ' \n' ' %s\n' ' \n' - ) % (idx, name) + ) % (idx, escape(name)) return xml @property From b81adf761fce22bfd397266a803e8bb25b696c13 Mon Sep 17 00:00:00 2001 From: huandzh Date: Tue, 4 Nov 2014 10:52:15 +0800 Subject: [PATCH 10/12] escape text for categories escape text for categories in _SeriesDataMoreDetails --- pptx/chart/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 2574274a4..c65c08dd2 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -558,7 +558,7 @@ def _cat_pt_xml(self): for idx, name in lvl: #ignore idx out bound if idx < self.categories_len: - xml += pt_xml % (idx, name) + xml += pt_xml % (idx, escape(name)) xml += lvl_end_tag return xml From a034aa00a73885b34a95fe08d10d1ae738446295 Mon Sep 17 00:00:00 2001 From: huandzh Date: Tue, 4 Nov 2014 11:28:00 +0800 Subject: [PATCH 11/12] only escape for strings --- pptx/chart/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 969ec803d..6599c9dab 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -209,7 +209,7 @@ def _cat_pt_xml(self): ' \n' ' %s\n' ' \n' - ) % (idx, escape(name)) + ) % (idx, escape(name) if isinstance(name,(str, unicode)) else name) return xml @property From ff251ce3e17e8805374778f72f45584681cbb662 Mon Sep 17 00:00:00 2001 From: huandzh Date: Tue, 4 Nov 2014 11:33:29 +0800 Subject: [PATCH 12/12] escape only strings --- pptx/chart/data.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pptx/chart/data.py b/pptx/chart/data.py index 5e6c6b7d8..f2f52a3ee 100644 --- a/pptx/chart/data.py +++ b/pptx/chart/data.py @@ -213,7 +213,8 @@ def _cat_pt_xml(self): ' \n' ' %s\n' ' \n' - ) % (idx, escape(name) if isinstance(name,(str, unicode)) else name) + ) % (idx, (escape(name) if + isinstance(name,(str, unicode)) else name)) return xml @property @@ -558,7 +559,8 @@ def _cat_pt_xml(self): for idx, name in lvl: #ignore idx out bound if idx < self.categories_len: - xml += pt_xml % (idx, escape(name)) + xml += pt_xml % (idx, (escape(name) if + isinstance(name,(str, unicode)) else name)) xml += lvl_end_tag return xml