scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.39k stars 518 forks source link

Unable to Arrange Tickers Side-by-Side in PowerPoint Table Cells #937

Open Kituva opened 8 months ago

Kituva commented 8 months ago

I am working on a Python script that generates a PowerPoint presentation with a table containing tickers. I am using the python-pptx library for this task. However, I am facing an issue in arranging the tickers side-by-side within each cell of the table.

I have a dictionary (segregated_data) containing sectors and their respective rankings, where each combination has a list of tickers. The tickers are currently displayed in a column within each cell, but I want them to be arranged side-by-side.

import pandas as pd
from pptx import Presentation
from pptx.util import Inches
from pptx.enum.text import PP_ALIGN
from pptx.enum.text import MSO_AUTO_SIZE
from pptx.util import Pt
from datetime import date

big_grid = pd.read_excel("WHG Testing.xlsx")

sector_definitions = {
    100: 'Energy',
    200: 'Basic Materials',
    300: 'Industrial Manufacturing',
    400: 'Utilities',
    500: 'Services',
    600: 'Financials',
    700: 'Transportation',
    800: 'HealthCare',
    900: 'Consumer Staples',
    1000: 'Consumer Cyclicals',
    1200: 'Technology',
    1400: 'Communication / Media'
}

ranking_definitions = {
    1: 'Bottom Fishing',
    2: 'Positive Developing',
    3: 'Positive Trending',
    4: 'Pullback Opportunity',
    5: 'Negative Developing',
    6: 'Negative Trending'
}

# Segregate the data based on sector and ranking definitions
segregated_data = {}
for sector_code, sector_name in sector_definitions.items():
    sector_data = big_grid[big_grid['Sector'] == sector_code]
    for ranking_code, ranking_name in ranking_definitions.items():
        ranking_data = sector_data[sector_data['Ranking'] == ranking_code]
        if not ranking_data.empty:
            key = f'{ranking_name} - {sector_name}'
            segregated_data[key] = ranking_data['Tickers'].tolist()

# Create a PowerPoint presentation
presentation = Presentation()
slide = presentation.slides.add_slide(presentation.slide_layouts[6])

# Get the dimensions of the slide
slide_width = presentation.slide_width
slide_height = presentation.slide_height

# Set the margins and padding
margin_left = Inches(0.3)
margin_right = Inches(0.3)
margin_top = Inches(1.2)
margin_bottom = Inches(0.7)

# Calculate the available width and height for the table
available_width = slide_width - margin_left - margin_right
available_height = slide_height - margin_top - margin_bottom

top_left_text = slide.shapes.add_textbox(left=Inches(0.3), top=Inches(0.3), width=Inches(2), height=Inches(0.1)).text_frame
top_left_text.text = "Macro Research | Technical Research"
for paragraph in top_left_text.paragraphs:
    paragraph.font.size = Pt(12)
    paragraph.font.bold = True

top_right_text = slide.shapes.add_textbox(left=Inches(8.7), top=Inches(0.3), width=Inches(2), height=Inches(0.1)).text_frame
top_right_text.text = date.today().strftime('%B %d, %Y')
for paragraph in top_right_text.paragraphs:
    paragraph.font.size = Pt(8)

title = slide.shapes.add_textbox(left=margin_left, top=margin_top / 2, width=available_width, height=Inches(0.5)).text_frame
title.text = "Weekly Homework Grids"
title.paragraphs[0].alignment = PP_ALIGN.CENTER
for paragraph in title.paragraphs:
    paragraph.font.size = Pt(16)
    paragraph.font.bold = True

# Determine the number of columns and rows based on the data
num_columns = len(ranking_definitions) + 1
num_rows = len(sector_definitions) + 1

# Add a table to the slide
table_left = margin_left
table_top = margin_top
table_width = available_width
table_height = available_height

shape = slide.shapes.add_table(rows=num_rows, cols=num_columns, left=table_left, top=table_top, width=table_width, height=table_height)
shape.auto_fit = True
tbl =  shape._element.graphic.graphicData.tbl
style_id = "{5940675A-B579-460E-94D1-54222C63F5DA}"
tbl[0][-1].text = style_id

table = shape.table

for row in table.rows:
    for cell in row.cells:
        cell.text_frame.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
        cell.text_frame.word_wrap = True

# Set header row style
header_row = table.rows[0]
for i, (ranking_code, ranking_name) in enumerate(ranking_definitions.items()):
    cell = header_row.cells[i + 1]  # Shift by 1 to accommodate row names
    cell.text = ranking_name
    cell.text_frame.paragraphs[0].font.size = Pt(7)  # Adjust the font size for the header row
    cell.text_frame.paragraphs[0].font.bold = True
    cell.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER

# Populate table with sector data
for i, (sector_code, sector_name) in enumerate(sector_definitions.items()):
    row = table.rows[i + 1]
    row.cells[0].text = sector_name
    row.cells[0].text_frame.text = sector_name
    row.cells[0].text_frame.paragraphs[0].font.size = Pt(7)  # Adjust the font size for data rows
    row.cells[0].text_frame.paragraphs[0].font.bold = True
    row.cells[0].text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER

    table.autofit = True

    for j, (ranking_code, ranking_name) in enumerate(ranking_definitions.items()):
        key = f'{ranking_name} - {sector_name}'
        tickers = segregated_data.get(key, [])
        cell = row.cells[j + 1]  # Shift by 1 to accommodate row names

        cell.text = "\n".join(tickers)
        for paragraph in cell.text_frame.paragraphs:
            paragraph.font.name = 'Consolas'
            paragraph.font.size = Pt(5)

presentation.save("BigGrid_Output.pptx")

I've attempted to modify paragraph alignment and utilize line breaks ("\n"), but I haven't achieved the desired side-by-side arrangement yet. Additionally, the output is currently overflowing the PowerPoint slide. I aim to make it dynamically adjust within the PowerPoint slide area. Below, you'll find screenshots of the current and desired output, and I've also attached the Excel file to help you replicate the issue locally.

Current Output image

Desired Output image

Excel file WHG Testing.xlsx

Any guidance on how to modify the code to arrange tickers side-by-side within each table cell would be greatly appreciated. Thank you!

MartinPacker commented 8 months ago

Hi @Kituva. (I see you've moved this over from Stack Overflow.)

There I suggested using a fixed pitch font but I think I have a better idea: You could do it by making each column a separate cell. The issue you will find is how to delete the vertical lines between cells.

Sorry to advertise my md2pptx Open Source project but scan the file md2pptx for function applyCellBorderStyling. It's where I've extended what python-pptx does to apply styling to the sides of a table cell.

That function should help you solve the "deleting cell lines" problem.

The other problem will be merging cells for the titles. python-pptx can already do that (I think).

Note: I don't think md2pptx solves your problem itself.

Kituva commented 8 months ago

Hi @MartinPacker, Thanks for your input, However, I would like the tickers in a cell to be arranged side by side (Parallelly) and dynamically adjust the width of the columns to accommodate the tickers. For example, you can refer to the Basic Materials cell for bottom fishing in the image below.

image

MartinPacker commented 8 months ago

Right. Cells with invisible boundaries are the way to do that. And "Bottom Fishing" would be spanning more than one cell (column).

Unless, that is, you want cells to flow - so that CGAU through to VALE are one item.

Kituva commented 8 months ago

Actually, there are no cells without boundaries. In the image, it looks like it doesn't have boundaries, but when you zoom in on the PowerPoint, the boundary lines become visible.

MartinPacker commented 8 months ago

My point is - with the function I pointed to - you can control the boundary lines, including turning them off - selectively for each side of the cell.