aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py272
1 files changed, 272 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
new file mode 100644
index 00000000..30b21272
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
@@ -0,0 +1,272 @@
+"""Chart builder and related objects."""
+
+from __future__ import annotations
+
+import io
+from contextlib import contextmanager
+
+from xlsxwriter import Workbook
+
+
+class _BaseWorkbookWriter(object):
+ """Base class for workbook writers, providing shared members."""
+
+ def __init__(self, chart_data):
+ super(_BaseWorkbookWriter, self).__init__()
+ self._chart_data = chart_data
+
+ @property
+ def xlsx_blob(self):
+ """bytes for Excel file containing chart_data."""
+ xlsx_file = io.BytesIO()
+ with self._open_worksheet(xlsx_file) as (workbook, worksheet):
+ self._populate_worksheet(workbook, worksheet)
+ return xlsx_file.getvalue()
+
+ @contextmanager
+ def _open_worksheet(self, xlsx_file):
+ """
+ Enable XlsxWriter Worksheet object to be opened, operated on, and
+ then automatically closed within a `with` statement. A filename or
+ stream object (such as an `io.BytesIO` instance) is expected as
+ *xlsx_file*.
+ """
+ workbook = Workbook(xlsx_file, {"in_memory": True})
+ worksheet = workbook.add_worksheet()
+ yield workbook, worksheet
+ workbook.close()
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Must be overridden by each subclass to provide the particulars of
+ writing the spreadsheet data.
+ """
+ raise NotImplementedError("must be provided by each subclass")
+
+
+class CategoryWorkbookWriter(_BaseWorkbookWriter):
+ """
+ Determines Excel worksheet layout and can write an Excel workbook from
+ a CategoryChartData object. Serves as the authority for Excel worksheet
+ ranges.
+ """
+
+ @property
+ def categories_ref(self):
+ """
+ The Excel worksheet reference to the categories for this chart (not
+ including the column heading).
+ """
+ categories = self._chart_data.categories
+ if categories.depth == 0:
+ raise ValueError("chart data contains no categories")
+ right_col = chr(ord("A") + categories.depth - 1)
+ bottom_row = categories.leaf_count + 1
+ return "Sheet1!$A$2:$%s$%d" % (right_col, bottom_row)
+
+ def series_name_ref(self, series):
+ """
+ Return the Excel worksheet reference to the cell containing the name
+ for *series*. This also serves as the column heading for the series
+ values.
+ """
+ return "Sheet1!$%s$1" % self._series_col_letter(series)
+
+ def values_ref(self, series):
+ """
+ The Excel worksheet reference to the values for this series (not
+ including the column heading).
+ """
+ return "Sheet1!${col_letter}$2:${col_letter}${bottom_row}".format(
+ **{
+ "col_letter": self._series_col_letter(series),
+ "bottom_row": len(series) + 1,
+ }
+ )
+
+ @staticmethod
+ def _column_reference(column_number):
+ """Return str Excel column reference like 'BQ' for *column_number*.
+
+ *column_number* is an int in the range 1-16384 inclusive, where
+ 1 maps to column 'A'.
+ """
+ if column_number < 1 or column_number > 16384:
+ raise ValueError("column_number must be in range 1-16384")
+
+ # ---Work right-to-left, one order of magnitude at a time. Note there
+ # is no zero representation in Excel address scheme, so this is
+ # not just a conversion to base-26---
+
+ col_ref = ""
+ while column_number:
+ remainder = column_number % 26
+ if remainder == 0:
+ remainder = 26
+
+ col_letter = chr(ord("A") + remainder - 1)
+ col_ref = col_letter + col_ref
+
+ # ---Advance to next order of magnitude or terminate loop. The
+ # minus-one in this expression reflects the fact the next lower
+ # order of magnitude has a minumum value of 1 (not zero). This is
+ # essentially the complement to the "if it's 0 make it 26' step
+ # above.---
+ column_number = (column_number - 1) // 26
+
+ return col_ref
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write the chart data contents to *worksheet* in category chart
+ layout. Write categories starting in the first column starting in
+ the second row, and proceeding one column per category level (for
+ charts having multi-level categories). Write series as columns
+ starting in the next following column, placing the series title in
+ the first cell.
+ """
+ self._write_categories(workbook, worksheet)
+ self._write_series(workbook, worksheet)
+
+ def _series_col_letter(self, series):
+ """
+ The letter of the Excel worksheet column in which the data for a
+ series appears.
+ """
+ column_number = 1 + series.categories.depth + series.index
+ return self._column_reference(column_number)
+
+ def _write_categories(self, workbook, worksheet):
+ """
+ Write the categories column(s) to *worksheet*. Categories start in
+ the first column starting in the second row, and proceeding one
+ column per category level (for charts having multi-level categories).
+ A date category is formatted as a date. All others are formatted
+ `General`.
+ """
+ categories = self._chart_data.categories
+ num_format = workbook.add_format({"num_format": categories.number_format})
+ depth = categories.depth
+ for idx, level in enumerate(categories.levels):
+ col = depth - idx - 1
+ self._write_cat_column(worksheet, col, level, num_format)
+
+ def _write_cat_column(self, worksheet, col, level, num_format):
+ """
+ Write a category column defined by *level* to *worksheet* at offset
+ *col* and formatted with *num_format*.
+ """
+ worksheet.set_column(col, col, 10) # wide enough for a date
+ for off, name in level:
+ row = off + 1
+ worksheet.write(row, col, name, num_format)
+
+ def _write_series(self, workbook, worksheet):
+ """
+ Write the series column(s) to *worksheet*. Series start in the column
+ following the last categories column, placing the series title in the
+ first cell.
+ """
+ col_offset = self._chart_data.categories.depth
+ for idx, series in enumerate(self._chart_data):
+ num_format = workbook.add_format({"num_format": series.number_format})
+ series_col = idx + col_offset
+ worksheet.write(0, series_col, series.name)
+ worksheet.write_column(1, series_col, series.values, num_format)
+
+
+class XyWorkbookWriter(_BaseWorkbookWriter):
+ """
+ Determines Excel worksheet layout and can write an Excel workbook from XY
+ chart data. Serves as the authority for Excel worksheet ranges.
+ """
+
+ def series_name_ref(self, series):
+ """
+ Return the Excel worksheet reference to the cell containing the name
+ for *series*. This also serves as the column heading for the series
+ Y values.
+ """
+ row = self.series_table_row_offset(series) + 1
+ return "Sheet1!$B$%d" % row
+
+ def series_table_row_offset(self, series):
+ """
+ Return the number of rows preceding the data table for *series* in
+ the Excel worksheet.
+ """
+ title_and_spacer_rows = series.index * 2
+ data_point_rows = series.data_point_offset
+ return title_and_spacer_rows + data_point_rows
+
+ def x_values_ref(self, series):
+ """
+ The Excel worksheet reference to the X values for this chart (not
+ including the column label).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$A$%d:$A$%d" % (top_row, bottom_row)
+
+ def y_values_ref(self, series):
+ """
+ The Excel worksheet reference to the Y values for this chart (not
+ including the column label).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$B$%d:$B$%d" % (top_row, bottom_row)
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write chart data contents to *worksheet* in the standard XY chart
+ layout. Write the data for each series to a separate two-column
+ table, X values in column A and Y values in column B. Place the
+ series label in the first (heading) cell of the column.
+ """
+ chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+ for series in self._chart_data:
+ series_num_format = workbook.add_format({"num_format": series.number_format})
+ offset = self.series_table_row_offset(series)
+ # write X values
+ worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+ # write Y values
+ worksheet.write(offset, 1, series.name)
+ worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+
+
+class BubbleWorkbookWriter(XyWorkbookWriter):
+ """
+ Service object that knows how to write an Excel workbook from bubble
+ chart data.
+ """
+
+ def bubble_sizes_ref(self, series):
+ """
+ The Excel worksheet reference to the range containing the bubble
+ sizes for *series* (not including the column heading cell).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$C$%d:$C$%d" % (top_row, bottom_row)
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write chart data contents to *worksheet* in the bubble chart layout.
+ Write the data for each series to a separate three-column table with
+ X values in column A, Y values in column B, and bubble sizes in
+ column C. Place the series label in the first (heading) cell of the
+ values column.
+ """
+ chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+ for series in self._chart_data:
+ series_num_format = workbook.add_format({"num_format": series.number_format})
+ offset = self.series_table_row_offset(series)
+ # write X values
+ worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+ # write Y values
+ worksheet.write(offset, 1, series.name)
+ worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+ # write bubble sizes
+ worksheet.write(offset, 2, "Size")
+ worksheet.write_column(offset + 1, 2, series.bubble_sizes, chart_num_format)