"""PowerPoint presentation generation for Share of Search analysis."""
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
import pandas as pd
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pptx.dml.color import RGBColor
from ..utils.errors import ProcessingError
from ..utils.logging import get_logger
logger = get_logger(__name__)
[docs]
class PowerPointGenerator:
"""Generate McKinsey-style PowerPoint presentations."""
# McKinsey brand colors
NAVY = RGBColor(0, 51, 102) # #003366
DARK_GRAY = RGBColor(51, 51, 51) # #333333
LIGHT_GRAY = RGBColor(128, 128, 128) # #808080
def __init__(self):
"""Initialize PowerPoint generator."""
self.prs = Presentation()
self.prs.slide_width = Inches(10)
self.prs.slide_height = Inches(7.5)
def _add_title_slide(self, project_name: str) -> None:
"""Add professional title slide."""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6]) # Blank layout
# Title
title_box = slide.shapes.add_textbox(
Inches(1), Inches(2.5), Inches(8), Inches(1.5)
)
title_frame = title_box.text_frame
title_frame.text = "Share of Search Analysis"
title_para = title_frame.paragraphs[0]
title_para.font.size = Pt(36)
title_para.font.color.rgb = self.NAVY
title_para.font.name = 'Arial'
title_para.alignment = PP_ALIGN.LEFT
# Subtitle
subtitle_box = slide.shapes.add_textbox(
Inches(1), Inches(4), Inches(8), Inches(0.8)
)
subtitle_frame = subtitle_box.text_frame
subtitle_frame.text = project_name
subtitle_para = subtitle_frame.paragraphs[0]
subtitle_para.font.size = Pt(20)
subtitle_para.font.color.rgb = self.DARK_GRAY
subtitle_para.font.name = 'Arial'
subtitle_para.alignment = PP_ALIGN.LEFT
# Date and confidential
date_box = slide.shapes.add_textbox(
Inches(1), Inches(6.5), Inches(8), Inches(0.5)
)
date_frame = date_box.text_frame
date_frame.text = f"{datetime.now().strftime('%B %Y')} | Confidential"
date_para = date_frame.paragraphs[0]
date_para.font.size = Pt(10)
date_para.font.color.rgb = self.LIGHT_GRAY
date_para.font.name = 'Arial'
def _add_content_slide(
self,
title: str,
content: str,
image_path: Optional[Path] = None
) -> None:
"""Add content slide with optional image."""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# Title
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.4), Inches(9), Inches(0.6)
)
title_frame = title_box.text_frame
title_frame.text = title
title_para = title_frame.paragraphs[0]
title_para.font.size = Pt(18)
title_para.font.bold = False
title_para.font.color.rgb = self.NAVY
title_para.font.name = 'Arial'
title_para.alignment = PP_ALIGN.LEFT
if image_path and image_path.exists():
# Add image
slide.shapes.add_picture(
str(image_path),
Inches(0.5), Inches(1.2),
width=Inches(9)
)
else:
# Add text content
content_box = slide.shapes.add_textbox(
Inches(0.5), Inches(1.2), Inches(9), Inches(5.5)
)
content_frame = content_box.text_frame
content_frame.word_wrap = True
# Parse content into paragraphs
for line in content.split('\n'):
if line.strip():
p = content_frame.add_paragraph()
p.text = line.strip()
p.font.size = Pt(11)
p.font.color.rgb = self.DARK_GRAY
p.font.name = 'Arial'
p.space_before = Pt(6)
p.space_after = Pt(6)
# Footer
footer_box = slide.shapes.add_textbox(
Inches(0.5), Inches(7), Inches(9), Inches(0.3)
)
footer_frame = footer_box.text_frame
footer_frame.text = "Source: Google Trends | Confidential"
footer_para = footer_frame.paragraphs[0]
footer_para.font.size = Pt(8)
footer_para.font.color.rgb = self.LIGHT_GRAY
footer_para.font.name = 'Arial'
[docs]
def generate_presentation(
self,
project_name: str,
executive_summary: str,
competitive_insights: str,
metrics_df: pd.DataFrame,
chart_paths: Dict[str, Path],
market_concentration: Dict[str, Any],
output_path: Path
) -> None:
"""
Generate complete PowerPoint presentation.
Args:
project_name: Project name
executive_summary: Executive summary text
competitive_insights: Competitive insights text
metrics_df: Metrics DataFrame
chart_paths: Dictionary of chart paths
market_concentration: Market concentration metrics
output_path: Path to save presentation
"""
try:
logger.info("Generating PowerPoint presentation...")
# Slide 1: Title
self._add_title_slide(project_name)
# Slide 2: Executive Summary
summary_clean = self._clean_text_for_slide(executive_summary, max_length=600)
self._add_content_slide("Executive Summary", summary_clean)
# Slide 3: Market Overview (Bar Chart)
if 'bar' in chart_paths:
self._add_content_slide(
"Market Overview - Share Distribution",
"",
chart_paths['bar']
)
# Slide 4: Temporal Trends (Line Chart)
if 'line' in chart_paths:
self._add_content_slide(
"Temporal Dynamics - Search Trends",
"",
chart_paths['line']
)
# Slide 5: Competitive Positioning (Area Chart)
if 'area' in chart_paths:
self._add_content_slide(
"Competitive Positioning - Share Evolution",
"",
chart_paths['area']
)
# Slide 6: Statistical Analysis
insights_clean = self._clean_text_for_slide(competitive_insights, max_length=600)
self._add_content_slide("Statistical Analysis", insights_clean)
# Slide 7: Data Limitations
limitations = self._create_limitations_text(market_concentration)
self._add_content_slide("Data Quality & Limitations", limitations)
# Slide 8: Brand Metrics Table
self._add_metrics_slide(metrics_df)
# Save presentation
self.prs.save(str(output_path))
logger.info(f"PowerPoint presentation saved: {output_path}")
except Exception as e:
raise ProcessingError(f"Failed to generate PowerPoint: {e}")
def _clean_text_for_slide(self, text: str, max_length: int = 600) -> str:
"""Clean and truncate text for slide."""
# Remove section headers
lines = []
for line in text.split('\n'):
line = line.strip()
if line and not line.endswith(':'):
# Remove markdown and numbers
line = line.replace('**', '').replace('*', '')
if not line.startswith(('1.', '2.', '3.', '4.', '-')):
lines.append(line)
full_text = ' '.join(lines)
if len(full_text) > max_length:
full_text = full_text[:max_length] + '...'
return full_text
def _create_limitations_text(self, market_concentration: Dict[str, Any]) -> str:
"""Create data limitations text."""
hhi = market_concentration.get('hhi', 0)
text = f"""DATA QUALITY NOTICE
Google Trends measurement error: ±5% variability between retrievals (Cebrián & Domenech 2023)
Same query on different days shows correlation of only 0.79-0.94
Undisclosed sampling methods used by Google (Choi & Varian 2012)
Coverage bias: Excludes non-Google users and specialized search platforms
Market Concentration: HHI = {hhi:.0f}
INTERPRETATION GUIDELINES
This analysis represents search interest patterns, not actual market performance
Correlations observed do not imply causation
Strategic decisions require additional data sources beyond Google Trends
All statistical observations require external validation for causal claims"""
return text
def _add_metrics_slide(self, metrics_df: pd.DataFrame) -> None:
"""Add slide with metrics table."""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# Title
title_box = slide.shapes.add_textbox(
Inches(0.5), Inches(0.4), Inches(9), Inches(0.6)
)
title_frame = title_box.text_frame
title_frame.text = "Brand Metrics Summary"
title_para = title_frame.paragraphs[0]
title_para.font.size = Pt(18)
title_para.font.color.rgb = self.NAVY
title_para.font.name = 'Arial'
# Table
rows = len(metrics_df) + 1
cols = 3
table = slide.shapes.add_table(
rows, cols,
Inches(1), Inches(1.5),
Inches(8), Inches(4)
).table
# Header row
table.cell(0, 0).text = "Brand"
table.cell(0, 1).text = "Avg Share (%)"
table.cell(0, 2).text = "Volatility"
for i in range(cols):
cell = table.cell(0, i)
cell.text_frame.paragraphs[0].font.size = Pt(11)
cell.text_frame.paragraphs[0].font.bold = True
cell.text_frame.paragraphs[0].font.color.rgb = self.NAVY
# Data rows
for idx, (_, row) in enumerate(metrics_df.iterrows(), start=1):
table.cell(idx, 0).text = str(row['query'])
table.cell(idx, 1).text = f"{row['avg_share']:.1f}%"
table.cell(idx, 2).text = f"{row.get('volatility', 0):.2f}"
for i in range(cols):
cell = table.cell(idx, i)
cell.text_frame.paragraphs[0].font.size = Pt(10)
cell.text_frame.paragraphs[0].font.color.rgb = self.DARK_GRAY
# Footer
footer_box = slide.shapes.add_textbox(
Inches(0.5), Inches(7), Inches(9), Inches(0.3)
)
footer_frame = footer_box.text_frame
footer_frame.text = "Source: Google Trends | Note: ±5% measurement error"
footer_para = footer_frame.paragraphs[0]
footer_para.font.size = Pt(8)
footer_para.font.color.rgb = self.LIGHT_GRAY
footer_para.font.name = 'Arial'