1

I was wondering if it was possible to write a StyleFrame to an arbitrary position in an existing excel worksheet while maintaining the original formatting and styling of pre-existing cells?

E.g In the example below, I'd like to set the output of the Styleframe to start from cell 'A9' while maintaining the formatting and coloring of the other cells (Cells 'A1','A2','A3' etc):

enter image description here

Sector97
  • 116
  • 9
  • 1
    Since styleframe uses `pandas.ExcelWriter` the same solution to the duplicated question applies here. – DeepSpace Oct 13 '21 at 08:26
  • Hi DeepSpace, Thanks very much for connecting me with this answer stackoverflow.com/a/38075046/14251756. The function does indeed work but it needed a couple of minor edits to make it compatible with styleframes e.g attempting to run the raw code directly on a styleframe gets the glitch "'StyleFrame' object has no attribute 'dtypes'" I'd like to leave this question up, just in case anyone else encounters a similar issue in making MaxU's function compatible with Styleframes. – Sector97 Oct 13 '21 at 08:39
  • Hmmm.... I'd like to post the modified function but it seems that I can't post answers while this post has been marked as a duplicate – Sector97 Oct 13 '21 at 08:42
  • 1
    I see. I've reopened the question so you can post your answer. However please make sure to include a link to the original answer so it is clear that your answer builds upon and references that answer – DeepSpace Oct 13 '21 at 11:07
  • 1
    Will do, thanks very much for your help with this. Also wanted to say thank you for creating the Styleframe library, it's incredibly helpful to my day to day work and makes it so much easier to format data-frames. Regards, – Sector97 Oct 13 '21 at 22:42

1 Answers1

1

So user @MaxU has a helpful answer over here outlining a function that appends regular dataframes to arbitrary positions in an existing excel worksheet. With some minor changes I've been able to modify this function to work on styleframes. Note that while the modified function successfully writes most cell properties into existing worksheets for some reason it does not copy cell alignments over. So I have hard-coded the Alignments of appended cells to be top,left and wrapped. Please see the modified code below:

from pathlib import Path
from copy import copy
from typing import Union, Optional
import numpy as np
import pandas as pd
import openpyxl
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
from styleframe import StyleFrame
from openpyxl.styles.alignment import Alignment


def copy_excel_cell_range(
        src_ws: openpyxl.worksheet.worksheet.Worksheet,
        min_row: int = None,
        max_row: int = None,
        min_col: int = None,
        max_col: int = None,
        tgt_ws: openpyxl.worksheet.worksheet.Worksheet = None,
        tgt_min_row: int = 1,
        tgt_min_col: int = 1,
        with_style: bool = True
) -> openpyxl.worksheet.worksheet.Worksheet:
    if tgt_ws is None:
        tgt_ws = src_ws

    # https://stackoverflow.com/a/34838233/5741205
    for row in src_ws.iter_rows(min_row=min_row, max_row=max_row,
                                min_col=min_col, max_col=max_col):
        for cell in row:
            tgt_cell = tgt_ws.cell(
                row=cell.row + tgt_min_row - 1,
                column=cell.col_idx + tgt_min_col - 1,
                value=cell.value
            )
            if with_style and cell.has_style:
                # tgt_cell._style = copy(cell._style)
                tgt_cell.font = copy(cell.font)
                tgt_cell.border = copy(cell.border)
                tgt_cell.fill = copy(cell.fill)
                tgt_cell.number_format = copy(cell.number_format)
                tgt_cell.protection = copy(cell.protection)
                tgt_cell.alignment = Alignment(horizontal='left', vertical='top',wrapText=True)
    return tgt_ws

def append_sf_to_excel(
        filename: Union[str, Path],
        sf: StyleFrame,
        sheet_name: str = 'Sheet1',
        startrow: Optional[int] = None,
        max_col_width: int = 30,
        autofilter: bool = False,
        fmt_int: str = "#,##0",
        fmt_float: str = "#,##0.00",
        fmt_date: str = "yyyy-mm-dd",
        fmt_datetime: str = "yyyy-mm-dd hh:mm",
        truncate_sheet: bool = False,
        storage_options: Optional[dict] = None,
        **to_excel_kwargs
) -> None:

    def set_column_format(ws, column_letter, fmt):
        for cell in ws[column_letter]:
            cell.number_format = fmt
    filename = Path(filename)
    file_exists = filename.is_file()
    # process parameters
    # calculate first column number
    # if the sf will be written using `index=True`, then `first_col = 2`, else `first_col = 1`
    first_col = int(to_excel_kwargs.get("index", True)) + 1
    # ignore [engine] parameter if it was passed
    if 'engine' in to_excel_kwargs:
        to_excel_kwargs.pop('engine')
    # save content of existing sheets
    if file_exists:
        wb = load_workbook(filename)
        sheet_names = wb.sheetnames
        sheet_exists = sheet_name in sheet_names
        sheets = {ws.title: ws for ws in wb.worksheets}

    with StyleFrame.ExcelWriter(
        filename.with_suffix(".xlsx"),
        mode="a" if file_exists else "w",
        if_sheet_exists="new" if file_exists else None,
        date_format=fmt_date,
        datetime_format=fmt_datetime,
        storage_options=storage_options
    ) as writer:
        if file_exists:
            # try to open an existing workbook
            writer.book = wb
            # get the last row in the existing Excel sheet
            # if it was not specified explicitly
            # for row in wb['Sheet1'].iter_rows():
            #     for cell in row:
            #         print(f'{cell.alignment}\n\n')
            if startrow is None and sheet_name in writer.book.sheetnames:
                startrow = writer.book[sheet_name].max_row
            # truncate sheet
            if truncate_sheet and sheet_name in writer.book.sheetnames:
                # index of [sheet_name] sheet
                idx = writer.book.sheetnames.index(sheet_name)
                # remove [sheet_name]
                writer.book.remove(writer.book.worksheets[idx])
                # create an empty sheet [sheet_name] using old index
                writer.book.create_sheet(sheet_name, idx)
            # copy existing sheets
            writer.sheets = sheets
        else:
            # file doesn't exist, we are creating a new one
            startrow = 0

        # write out the DataFrame to an ExcelWriter
        sf.to_excel(writer, sheet_name=sheet_name)
        worksheet = writer.sheets[sheet_name]

        if autofilter:
            worksheet.auto_filter.ref = worksheet.dimensions

        for xl_col_no, dtyp in enumerate(sf.data_df.dtypes, first_col):
            col_no = xl_col_no - first_col
            width = max(sf.iloc[:, col_no].astype(str).str.len().max(),
                        len(sf.columns[col_no]) + 6)
            width = min(max_col_width, width)
            column_letter = get_column_letter(xl_col_no)
            worksheet.column_dimensions[column_letter].width = width
            if np.issubdtype(dtyp, np.integer):
                set_column_format(worksheet, column_letter, fmt_int)
            if np.issubdtype(dtyp, np.floating):
                set_column_format(worksheet, column_letter, fmt_float)

    if file_exists and sheet_exists:
        # move (append) rows from new worksheet to the `sheet_name` worksheet
        wb = load_workbook(filename)
        # retrieve generated worksheet name
        new_sheet_name = set(wb.sheetnames) - set(sheet_names)
        if new_sheet_name:
            new_sheet_name = list(new_sheet_name)[0]
        # copy rows written by `sf.to_excel(...)` to

        copy_excel_cell_range(
            src_ws=wb[new_sheet_name],
            tgt_ws=wb[sheet_name],
            tgt_min_row=startrow + 1,
            with_style=True
        )
        # remove new (generated by Pandas) worksheet
        del wb[new_sheet_name]
        wb.save(filename)
        wb.close()

Credit to Maxu for writing this function, and thanks to Deepspace for making me aware of this solution.

Sector97
  • 116
  • 9
  • 1
    Hi @Sector97 - I have a similar problem where I am trying to write my output to a formatted excel file. can you help me with this post? https://stackoverflow.com/questions/71527992/pandas-dataframe-to-specific-sheet-in-a-excel-file-without-losing-formatting – The Great Mar 18 '22 at 15:44
  • Answered on the link you shared, let me know if the solution I suggested works for you :) – Sector97 Mar 20 '22 at 21:43