# Chart Generator Module # Contains parse_embedded_chart_data() and create_chart_from_data() functions import re import io import os import sys import base64 import matplotlib.pyplot as plt import matplotlib.patches as patches from typing import List, Dict, Any, Optional # Add the parent directory to sys.path to import from models.py sys.path.append(os.path.dirname(os.path.dirname(__file__))) from models import ExportSection def parse_embedded_chart_data(sections: List[ExportSection]) -> List[Dict[str, Any]]: """Parse chart data embedded in section content using CHARTDATA markers""" charts = [] for section in sections: if section.content: # Look for chart data markers - handle both formats chart_matches = re.findall(r'(.*?)', section.content, re.DOTALL) # Also check for alternative markers as fallback alt_chart_matches = re.findall(r'(.*?)', section.content, re.DOTALL) chart_matches.extend(alt_chart_matches) # logging.info(f"Section '{section.title}': Found {len(chart_matches)} chart data markers") for chart_data in chart_matches: try: # Parse the chart data array chart_array = eval(chart_data.strip()) # logging.info(f"Parsed chart array with {len(chart_array)} items") # logging.info(f"Chart array type: {type(chart_array)}") # Each chart is an array with: [Section, ChartType, Title, Period, Data] for chart_item in chart_array: if len(chart_item) >= 5: section_name = chart_item[0] chart_type = chart_item[1] # DC=Doughnut, BG=Bar Graph, etc. chart_title = chart_item[2] period = chart_item[3] data = chart_item[4] chart_info = { "section": section_name, "type": chart_type, "title": chart_title, "period": period, "data": data, "source_section": section.title } charts.append(chart_info) # logging.info(f"Added chart: {chart_type} - {chart_title} for section {section_name}") else: # logging.warning(f"Chart item has insufficient data: {len(chart_item)} items, expected 5") pass except Exception as e: # logging.error(f"Failed to parse chart data for section '{section.title}': {str(e)}") continue # Additional debugging: Check for any chart-related content if not chart_matches: # Look for any chart-related markers or text chart_indicators = re.findall(r'', section.content, re.DOTALL) if chart_indicators: # logging.info(f"Section '{section.title}': Found chart indicators: {chart_indicators}") pass # Check for general chart text if 'CHART' in section.content.upper() or 'chart' in section.content.lower(): # logging.info(f"Section '{section.title}': Contains chart-related text") pass # logging.info(f"Total charts parsed: {len(charts)}") return charts def create_enhanced_chart_style(chart_type, data, title, business_idea): """Create charts with enhanced professional styling""" # Set modern style plt.style.use('default') # Design palette from image colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500'] if chart_type == "DC": # Doughnut Chart fig, ax = plt.subplots(figsize=(14, 10)) # Extract labels and values from data labels = [item[0] for item in data] values = [item[1] for item in data] # Create doughnut chart with enhanced styling wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)], autopct='%1.1f%%', startangle=90, wedgeprops=dict(width=0.6, edgecolor='white', linewidth=2), textprops={'fontsize': 18}) # Enhanced title styling ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151') # Add legend with enhanced styling legend = ax.legend(wedges, labels, title="Categories", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1), fontsize=16, title_fontsize=18) legend.get_title().set_color('#FF6B6B') # Add business name watermark fig.text(0.5, 0.02, f"Generated for: {business_idea}", ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic') plt.tight_layout() return fig elif chart_type == "BG": # Bar Graph fig, ax = plt.subplots(figsize=(16, 10)) # Extract labels and values labels = [item[0] for item in data] values = [item[1] for item in data] # Create bar chart with enhanced styling bars = ax.bar(labels, values, color=colors[:len(values)], edgecolor='white', linewidth=1, alpha=0.8) # Add value labels on bars for bar, value in zip(bars, values): height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height + max(values)*0.01, f'{value}', ha='center', va='bottom', fontweight='bold', fontsize=18) # Enhanced styling ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151') ax.set_xlabel('Categories', fontsize=18, color='#6B7280') ax.set_ylabel('Values', fontsize=18, color='#6B7280') # Rotate x-axis labels for better readability plt.setp(ax.get_xticklabels(), rotation=45, ha='right', fontsize=16) plt.setp(ax.get_yticklabels(), fontsize=16) # Add grid for better readability ax.yaxis.grid(True, alpha=0.3) ax.set_axisbelow(True) # Add business name watermark fig.text(0.5, 0.02, f"Generated for: {business_idea}", ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic') plt.tight_layout() return fig elif chart_type == "LG": # Line Graph fig, ax = plt.subplots(figsize=(16, 10)) # Extract data x_values = [item[0] for item in data] y_values = [item[1] for item in data] # Create line chart with enhanced styling ax.plot(x_values, y_values, color='#FF6B6B', linewidth=3, marker='o', markersize=8, markerfacecolor='white', markeredgecolor='#FF6B6B') # Fill area under line ax.fill_between(x_values, y_values, alpha=0.3, color='#4ECDC4') # Enhanced styling ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151') ax.set_xlabel('Time Period', fontsize=18, color='#6B7280') ax.set_ylabel('Values', fontsize=18, color='#6B7280') # Add grid ax.grid(True, alpha=0.3) ax.set_axisbelow(True) # Increase tick label font sizes plt.setp(ax.get_xticklabels(), fontsize=16) plt.setp(ax.get_yticklabels(), fontsize=16) # Add business name watermark fig.text(0.5, 0.02, f"Generated for: {business_idea}", ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic') plt.tight_layout() return fig elif chart_type == "PG": # Pie Graph fig, ax = plt.subplots(figsize=(14, 10)) # Extract labels and values labels = [item[0] for item in data] values = [item[1] for item in data] # Create pie chart with enhanced styling wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)], autopct='%1.1f%%', startangle=90, wedgeprops=dict(edgecolor='white', linewidth=2), textprops={'fontsize': 18}) # Enhanced title styling ax.set_title(title, fontsize=24, fontweight='bold', pad=20, color='#374151') # Add business name watermark fig.text(0.5, 0.02, f"Generated for: {business_idea}", ha='center', va='bottom', fontsize=8, color='#9CA3AF', style='italic') plt.tight_layout() return fig return None def create_chart_from_data(chart_info: Dict[str, Any], business_idea: str) -> bytes: """Create a matplotlib chart from parsed chart data with enhanced styling""" try: chart_type = chart_info["type"] chart_title = chart_info["title"] data = chart_info["data"] # logging.info(f"Creating chart: {chart_type} - {chart_title} with {len(data)} data points") # Use enhanced styling function fig = create_enhanced_chart_style(chart_type, data, chart_title, business_idea) if fig is None: # Fallback to original method if enhanced styling fails if chart_type == "DC": # Doughnut Chart fig, ax = plt.subplots(figsize=(14, 10)) # Larger size for better visibility # Extract labels and values from data labels = [item[0] for item in data] values = [item[1] for item in data] # High contrast color scheme colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500'] # Create doughnut chart wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)], autopct='%1.1f%%', startangle=90, wedgeprops=dict(width=0.6, edgecolor='white', linewidth=2), textprops={'fontsize': 18}) # Enhanced title ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151') # Add legend ax.legend(wedges, labels, title="Categories", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1), fontsize=16) plt.tight_layout() elif chart_type == "BG": # Bar Graph fig, ax = plt.subplots(figsize=(16, 10)) # Extract labels and values labels = [item[0] for item in data] values = [item[1] for item in data] # High contrast color scheme colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500'] # Create bar chart bars = ax.bar(labels, values, color=colors[:len(values)], edgecolor='white', linewidth=1, alpha=0.8) # Add value labels on bars for bar, value in zip(bars, values): height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height + max(values)*0.01, f'{value}', ha='center', va='bottom', fontweight='bold', fontsize=18) ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151') ax.set_xlabel('Categories', fontsize=18, color='#6B7280') ax.set_ylabel('Values', fontsize=18, color='#6B7280') # Rotate x-axis labels plt.setp(ax.get_xticklabels(), rotation=45, ha='right', fontsize=16) plt.setp(ax.get_yticklabels(), fontsize=16) # Add grid ax.yaxis.grid(True, alpha=0.3) ax.set_axisbelow(True) plt.tight_layout() elif chart_type == "LG": # Line Graph fig, ax = plt.subplots(figsize=(16, 10)) # Extract data x_values = [item[0] for item in data] y_values = [item[1] for item in data] # Create line chart ax.plot(x_values, y_values, color='#FF6B6B', linewidth=3, marker='o', markersize=8, markerfacecolor='white', markeredgecolor='#FF6B6B') # Fill area under line ax.fill_between(x_values, y_values, alpha=0.3, color='#4ECDC4') ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151') ax.set_xlabel('Time Period', fontsize=18, color='#6B7280') ax.set_ylabel('Values', fontsize=18, color='#6B7280') # Add grid ax.grid(True, alpha=0.3) ax.set_axisbelow(True) # Increase tick label font sizes plt.setp(ax.get_xticklabels(), fontsize=16) plt.setp(ax.get_yticklabels(), fontsize=16) plt.tight_layout() elif chart_type == "PG": # Pie Graph fig, ax = plt.subplots(figsize=(14, 10)) # Extract labels and values labels = [item[0] for item in data] values = [item[1] for item in data] # High contrast color scheme colors = ['#6A2EAB', '#5A2E9B', '#C0C0C0', '#FF8C00', '#FFD700', '#696969', '#FF4500'] # Create pie chart wedges, texts, autotexts = ax.pie(values, labels=labels, colors=colors[:len(values)], autopct='%1.1f%%', startangle=90, wedgeprops=dict(edgecolor='white', linewidth=2), textprops={'fontsize': 18}) ax.set_title(chart_title, fontsize=22, fontweight='bold', pad=15, color='#374151') plt.tight_layout() else: # logging.warning(f"Unknown chart type: {chart_type}") return None # Save to bytes buffer = io.BytesIO() fig.savefig(buffer, format='png', dpi=300, bbox_inches='tight', facecolor='white', edgecolor='none') buffer.seek(0) chart_bytes = buffer.getvalue() buffer.close() # Close the figure to free memory plt.close(fig) return chart_bytes except Exception as e: # logging.error(f"Failed to create chart: {str(e)}") return None def fix_base64_padding(base64_string: str) -> str: """Fix common base64 padding issues""" # Remove any whitespace or newlines base64_string = base64_string.strip() # Add padding if needed missing_padding = len(base64_string) % 4 if missing_padding: base64_string += '=' * (4 - missing_padding) return base64_string def robust_base64_decode(base64_string: str) -> Optional[bytes]: """Robustly decode base64 string with error handling and padding correction""" try: # First try direct decoding return base64.b64decode(base64_string) except Exception as e1: try: # Try with padding correction fixed_string = fix_base64_padding(base64_string) return base64.b64decode(fixed_string) except Exception as e2: try: # Try with URL-safe base64 return base64.urlsafe_b64decode(fix_base64_padding(base64_string)) except Exception as e3: try: # Try with standard base64 ignoring padding return base64.b64decode(base64_string + '==', validate=False) except Exception as e4: return None