Source code for gslides.table

# -*- coding: utf-8 -*-
"""
Creates the table in Google slides
"""
import logging
import pprint
from typing import Any, Dict, List, Optional, Tuple, Union

import numpy as np
import pandas as pd

from . import creds, package_font
from .colors import translate_color
from .frame import Frame
from .utils import (
    black_or_white,
    clean_dtypes,
    clean_nan,
    determine_col_proportion,
    hex_to_rgb,
)

logger = logging.getLogger(__name__)


[docs]class Table: """The class that creates a table. :param data: Data to insert into the table :type data: Frame or pd.DataFrame :param font_size: Font size in the unit PT :type font_size: int :param header: Whether to enable formatting on the header row :type header: bool :param stub: Whether to enable formatting on the stub (1st) column :type stub: bool :param header_background_color: A color to set the header background. Parameters can either be a hex-code or a named color. See gslides.config.color_mapping.keys() for accepted named colors :type header_background_color: str :param stub_background_color: A color to set the stub background. Parameters can either be a hex-code or a named color. See gslides.config.color_mapping.keys() for accepted named colors :type stub_background_color: str :param column_proportions: A list of floats representing the proportion of each column :type column_proportions: list of floats """ def __init__( self, data: Union[Frame, pd.DataFrame], font_size: int = 12, header: bool = True, stub: bool = False, header_background_color: str = "black", stub_background_color: str = "black", column_proportions: Optional[List[float]] = None, ) -> None: """Constructor method""" self.df = self._reset_header(self._resolve_df(data)) self.font_size = font_size self.header = header self.stub = stub self.header_background_color = hex_to_rgb( translate_color(header_background_color) ) self.stub_background_color = hex_to_rgb(translate_color(stub_background_color)) self.header_font_color = black_or_white(self.header_background_color) self.stub_font_color = black_or_white(self.stub_background_color) self.column_proportions = column_proportions def __repr__(self) -> str: """Prints class information. :return: String with helpful class infromation :rtype: str """ output = f"Table\n" f"{self.df.to_markdown(index = False)}" return output def _resolve_df(self, data: Union[Frame, pd.DataFrame]): """Outputs a cleaned dataframe :param data: Data to insert into the table :type data: Frame or pd.DataFrame :raises ValueError: Only pd.DataFrame or Frame accepted :return: A cleaned dataframe :rtype: pd.DataFrame """ if isinstance(data, Frame): return data.df elif isinstance(data, pd.DataFrame): df = clean_nan(data) df = df.applymap(clean_dtypes) return df else: raise ValueError("Only pd.DataFrame or Frame accepted") def _reset_header(self, df: pd.DataFrame): """Transforms a dataframe to set the 1st row as the header :param df: Dataframe to transform :type df: pd.DataFrame :return: Transformed dataframe :rtype: pd.DataFrame """ return df.T.reset_index().T
[docs] def render_create_table_json(self, sl_id: str) -> Dict[str, Any]: """Renders the create table json :param sl_id: Slide id :type sl_id: str :return: json for the API call :rtype: dict """ json: Dict[str, Any] = { "requests": [ { "createTable": { "elementProperties": { "pageObjectId": sl_id, }, "rows": self.df.shape[0], "columns": self.df.shape[1], } }, ] } return json
def _table_move_request( self, tbl_id: str, translate_x: float, translate_y: float ) -> List[Any]: """Renders the move table requests :param tbl_id: Table id :type tbl_id: str :param translate_x: The number of EMU to translate the object by :type translate_x: float :param translate_y: The number of EMU to translate the object by :type translate_y: float :return: requests for the API call :rtype: list """ requests: List[Any] = [] requests.append( { "updatePageElementTransform": { "objectId": tbl_id, "transform": { "scaleX": 1, "scaleY": 1, "translateX": translate_x, "translateY": translate_y, "unit": "EMU", }, "applyMode": "ABSOLUTE", }, } ) return requests def _table_add_text_request(self, tbl_id: str) -> List[Any]: """Renders the add text requests :param tbl_id: Table id :type tbl_id: str :return: requests for the API call :rtype: list """ requests: List[Any] = [] for row_cnt, col in enumerate( self.df.applymap(str).values.tolist() ): # fix_later for col_cnt, val in enumerate(col): requests.append( { "insertText": { "objectId": tbl_id, "cellLocation": { "rowIndex": row_cnt, "columnIndex": col_cnt, }, "text": val, "insertionIndex": 0, } } ) return requests def _table_style_text_request( self, tbl_id: str, header: bool, stub: bool, font_size: int, header_font_color: Tuple[float, ...], stub_font_color: Tuple[float, ...], ) -> List[Any]: """Renders the style text requests :param tbl_id: Table id :type tbl_id: str :param header: Whether to enable formatting on the header row :type header: bool :param stub: Whether to enable formatting on the stub (1st) column :type stub: bool :param font_size: The size of font in the table :type font_size: int :param header_font_color: A color to set the header font. :type header_font_color: tuple :param stub_font_color: A color to set the stub font. :type stub_font_color: tuple :return: requests for the API call :rtype: list """ requests: List[Any] = [] for row_cnt, col in enumerate(self.df.values.tolist()): for col_cnt, val in enumerate(col): if header and row_cnt == 0: font_color = header_font_color bold = True elif stub and col_cnt == 0: font_color = stub_font_color bold = True else: font_color = (0, 0, 0) bold = False requests.append( { "updateTextStyle": { "objectId": tbl_id, "cellLocation": { "rowIndex": row_cnt, "columnIndex": col_cnt, }, "style": { "foregroundColor": { "opaqueColor": { "rgbColor": { "red": font_color[0], "green": font_color[1], "blue": font_color[2], } } }, "bold": bold, "fontFamily": package_font.font, "fontSize": {"magnitude": font_size, "unit": "PT"}, }, "textRange": {"type": "ALL"}, "fields": "foregroundColor,bold,fontFamily,fontSize", } } ) return requests def _table_update_cell( self, tbl_id: str, header: bool, stub: bool, header_background_color: Tuple[float, ...], stub_background_color: Tuple[float, ...], ) -> List[Any]: """Renders the update cell requests :param tbl_id: Table id :type tbl_id: str :param header: Whether to enable formatting on the header row :type header: bool :param stub: Whether to enable formatting on the stub (1st) column :type stub: bool :param header_background_color: A color to set the header font. :type header_background_color: tuple :param stub_background_color: A color to set the stub font. :type stub_background_color: tuple :return: requests for the API call :rtype: list """ requests: List[Any] = [] requests.append( { "updateTableCellProperties": { "objectId": tbl_id, "tableRange": { "location": {"rowIndex": 0, "columnIndex": 0}, "rowSpan": self.df.shape[0], "columnSpan": self.df.shape[1], }, "tableCellProperties": {"contentAlignment": "MIDDLE"}, "fields": "contentAlignment", } } ) if stub: requests.append( { "updateTableCellProperties": { "objectId": tbl_id, "tableRange": { "location": {"rowIndex": 0, "columnIndex": 0}, "rowSpan": self.df.shape[0], "columnSpan": 1, }, "tableCellProperties": { "tableCellBackgroundFill": { "solidFill": { "color": { "rgbColor": { "red": stub_background_color[0], "green": stub_background_color[1], "blue": stub_background_color[2], } } } } }, "fields": "tableCellBackgroundFill.solidFill.color", } } ) if header: requests.append( { "updateTableCellProperties": { "objectId": tbl_id, "tableRange": { "location": {"rowIndex": 0, "columnIndex": 0}, "rowSpan": 1, "columnSpan": self.df.shape[1], }, "tableCellProperties": { "tableCellBackgroundFill": { "solidFill": { "color": { "rgbColor": { "red": header_background_color[0], "green": header_background_color[1], "blue": header_background_color[2], } } } } }, "fields": "tableCellBackgroundFill.solidFill.color", } } ) return requests def _table_update_paragraph_style(self, tbl_id: str) -> List[Any]: """Renders the update paragraph style requests :param tbl_id: Table id :type tbl_id: str :return: requests for the API call :rtype: list """ requests: List[Any] = [] for row_cnt, col in enumerate(self.df.values.tolist()): # fix later for col_cnt, val in enumerate(col): requests.append( { "updateParagraphStyle": { "objectId": tbl_id, "cellLocation": { "rowIndex": row_cnt, "columnIndex": col_cnt, }, "style": {"alignment": "CENTER"}, "textRange": {"type": "ALL"}, "fields": "alignment", } } ) return requests def _table_update_row(self, tbl_id: str, row_height: float) -> List: """Renders the update row height request :param tbl_id: Table id :type tbl_id: str :param row_height: The miminum height of a row. To note that this is simply the minimum height. Actual height of the row may be greater due to font size and text wrapping :type row_height: float :return: requests for the API call :rtype: list """ requests: List[Any] = [ { "updateTableRowProperties": { "tableRowProperties": { "minRowHeight": {"magnitude": row_height, "unit": "EMU"} }, "rowIndices": None, "objectId": tbl_id, "fields": "minRowHeight", } } ] return requests def _table_update_column(self, tbl_id: str, col_widths: Tuple[float, ...]) -> list: """Renders the update column width request :param tbl_id: Table id :type tbl_id: str :param col_widths: The width of each column. :type col_widths: tuple :return: requests for the API call :rtype: list """ requests: List[Any] = [] for cnt, col_width in enumerate(col_widths): requests.append( { "updateTableColumnProperties": { "tableColumnProperties": { "columnWidth": {"magnitude": col_width, "unit": "EMU"} }, "columnIndices": [cnt], "objectId": tbl_id, "fields": "columnWidth", } } ) return requests
[docs] def render_update_table_json( self, tbl_id: str, size: Tuple[float, float], translate_x: float, translate_y: float, ) -> dict: """Renders the json for the update of table properties. :param tbl_id: Table id :type tbl_id: str :param size: Tuple of width and height in EMU :type size: tuple :param translate_x: The number of EMU to translate the object by :type translate_x: float :param translate_y: The number of EMU to translate the object by :type translate_y: float :return: json for the API call :rtype: dict """ json: Dict[str, Any] = {"requests": []} if self.column_proportions: col_widths = size[0] * np.array(self.column_proportions) else: col_widths = size[0] * determine_col_proportion(self.df) row_height = size[1] / (self.df.shape[0]) json["requests"].extend( self._table_move_request(tbl_id, translate_x, translate_y) ) json["requests"].extend(self._table_add_text_request(tbl_id)) json["requests"].extend( self._table_style_text_request( tbl_id, self.header, self.stub, self.font_size, self.header_font_color, self.stub_font_color, ) ) json["requests"].extend( self._table_update_cell( tbl_id, self.header, self.stub, self.header_background_color, self.stub_background_color, ) ) json["requests"].extend(self._table_update_paragraph_style(tbl_id)) json["requests"].extend(self._table_update_row(tbl_id, row_height)) json["requests"].extend(self._table_update_column(tbl_id, col_widths)) return json
[docs] def create( self, presentation_id: str, slide_id: str, size: Tuple[float, float] = (3000000, 3000000), translate_x: float = 0, translate_y: float = 0, ) -> None: """Creates the table in Google slides :param presentation_id: The presentation_id of the presentation to create to create table in :type presentation_id: str :param slide_id: The slide_id of the slide to create table in :type slide_id: str :param size: Tuple of width and height in EMU :type size: tuple :param translate_x: The number of EMU to translate the object by :type translate_x: float :param translate_y: The number of EMU to translate the object by :type translate_y: float """ service = creds.slide_service body = self.render_create_table_json(slide_id) logger.info("Executing table creation") logger.info(f"Request: {pprint.pformat(body)}") output = ( service.presentations() .batchUpdate( presentationId=presentation_id, body=body, ) .execute() ) logger.info("Table created successfully") body = self.render_update_table_json( output["replies"][0]["createTable"]["objectId"], size, translate_x, translate_y, ) logger.info("Executing table updates") logger.info(f"Request: {pprint.pformat(body)}") ( service.presentations() .batchUpdate( presentationId=presentation_id, body=body, ) .execute() ) logger.info("Table updated successfully")