Spaces:
Sleeping
Sleeping
thadillo
Fix HuggingFace deployment errors: database locking, matplotlib permissions, and deprecation warnings
e60b22c
| """ | |
| PDF export utility for dashboard data | |
| Generates professional PDF reports with charts and maps using matplotlib | |
| """ | |
| import io | |
| from datetime import datetime | |
| from reportlab.lib import colors | |
| from reportlab.lib.pagesizes import letter, A4 | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak, Image | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-interactive backend | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| try: | |
| import contextily as cx | |
| HAS_CONTEXTILY = True | |
| except ImportError: | |
| HAS_CONTEXTILY = False | |
| class DashboardPDFExporter: | |
| """Export dashboard data to PDF with charts and maps""" | |
| def __init__(self, pagesize=letter): | |
| self.pagesize = pagesize | |
| self.styles = getSampleStyleSheet() | |
| self._setup_custom_styles() | |
| def _setup_custom_styles(self): | |
| """Setup custom paragraph styles""" | |
| self.styles.add(ParagraphStyle( | |
| name='CustomTitle', | |
| parent=self.styles['Heading1'], | |
| fontSize=24, | |
| textColor=colors.HexColor('#2c3e50'), | |
| spaceAfter=30, | |
| alignment=TA_CENTER | |
| )) | |
| self.styles.add(ParagraphStyle( | |
| name='SectionHeader', | |
| parent=self.styles['Heading2'], | |
| fontSize=16, | |
| textColor=colors.HexColor('#34495e'), | |
| spaceAfter=12, | |
| spaceBefore=12 | |
| )) | |
| def generate_pdf(self, buffer, data): | |
| """ | |
| Generate PDF report | |
| Args: | |
| buffer: BytesIO buffer to write PDF to | |
| data: Dictionary containing dashboard data | |
| """ | |
| doc = SimpleDocTemplate(buffer, pagesize=self.pagesize, | |
| rightMargin=72, leftMargin=72, | |
| topMargin=72, bottomMargin=18) | |
| story = [] | |
| # Title | |
| title = Paragraph("Participatory Planning Dashboard Report", self.styles['CustomTitle']) | |
| story.append(title) | |
| story.append(Spacer(1, 12)) | |
| # Metadata | |
| view_mode_label = "Sentence-Level" if data['view_mode'] == 'sentences' else "Submission-Level" | |
| metadata = Paragraph( | |
| f"<font size=10>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br/>" | |
| f"Analysis Mode: {view_mode_label}</font>", | |
| self.styles['Normal'] | |
| ) | |
| story.append(metadata) | |
| story.append(Spacer(1, 24)) | |
| # Summary Statistics | |
| story.append(Paragraph("Summary Statistics", self.styles['SectionHeader'])) | |
| story.extend(self._create_summary_stats(data)) | |
| story.append(Spacer(1, 24)) | |
| # Category Distribution Chart | |
| story.append(Paragraph("Category Distribution", self.styles['SectionHeader'])) | |
| category_chart = self._create_category_chart(data['category_stats']) | |
| if category_chart: | |
| story.append(category_chart) | |
| story.append(Spacer(1, 24)) | |
| # Contributor Type Distribution | |
| story.append(Paragraph("Contributor Type Distribution", self.styles['SectionHeader'])) | |
| contributor_chart = self._create_contributor_chart(data['contributor_stats']) | |
| if contributor_chart: | |
| story.append(contributor_chart) | |
| story.append(PageBreak()) | |
| # Breakdown Table | |
| story.append(Paragraph("Category Breakdown by Contributor Type", self.styles['SectionHeader'])) | |
| breakdown_table = self._create_breakdown_table(data['breakdown'], data['contributor_types']) | |
| story.append(breakdown_table) | |
| story.append(Spacer(1, 24)) | |
| # Map | |
| if data['geotagged_submissions']: | |
| story.append(PageBreak()) | |
| story.append(Paragraph("Geographic Distribution", self.styles['SectionHeader'])) | |
| map_image = self._create_map(data['geotagged_submissions'], data['categories']) | |
| if map_image: | |
| story.append(map_image) | |
| # Build PDF | |
| doc.build(story) | |
| return buffer | |
| def _create_summary_stats(self, data): | |
| """Create summary statistics section""" | |
| elements = [] | |
| total_items = sum(count for _, count in data['category_stats']) | |
| total_submissions = len(data['submissions']) | |
| total_geotagged = len(data['geotagged_submissions']) | |
| # Create metrics table | |
| metrics_data = [ | |
| ['Total Submissions', str(total_submissions)], | |
| ['Total Items Analyzed', str(total_items)], | |
| ['Geotagged Items', str(total_geotagged)], | |
| ['Categories', str(len([c for c, count in data['category_stats'] if count > 0]))] | |
| ] | |
| metrics_table = Table(metrics_data, colWidths=[3*inch, 2*inch]) | |
| metrics_table.setStyle(TableStyle([ | |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
| ('FONTNAME', (1, 0), (1, -1), 'Helvetica'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 12), | |
| ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#2c3e50')), | |
| ('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#3498db')), | |
| ('ALIGN', (1, 0), (1, -1), 'RIGHT'), | |
| ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 12), | |
| ])) | |
| elements.append(metrics_table) | |
| return elements | |
| def _create_category_chart(self, category_stats): | |
| """Create category distribution pie chart using matplotlib""" | |
| if not category_stats: | |
| return None | |
| try: | |
| # Prepare data | |
| labels = [cat for cat, _ in category_stats] | |
| values = [count for _, count in category_stats] | |
| # Create matplotlib figure | |
| fig, ax = plt.subplots(figsize=(6, 5)) | |
| colors_list = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'] | |
| wedges, texts, autotexts = ax.pie(values, labels=labels, autopct='%1.1f%%', | |
| colors=colors_list[:len(labels)], | |
| startangle=90) | |
| # Make percentage text more readable | |
| for autotext in autotexts: | |
| autotext.set_color('white') | |
| autotext.set_fontsize(10) | |
| autotext.set_weight('bold') | |
| ax.set_title('Category Distribution', fontsize=14, fontweight='bold') | |
| # Convert to image | |
| img_buffer = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(img_buffer, format='png', dpi=150, bbox_inches='tight') | |
| plt.close(fig) | |
| img_buffer.seek(0) | |
| img = Image(img_buffer, width=5*inch, height=4*inch) | |
| return img | |
| except Exception as e: | |
| print(f"Error creating category chart: {e}") | |
| return None | |
| def _create_contributor_chart(self, contributor_stats): | |
| """Create contributor type bar chart using matplotlib""" | |
| if not contributor_stats: | |
| return None | |
| try: | |
| # Prepare data | |
| types = [ctype for ctype, _ in contributor_stats] | |
| counts = [count for _, count in contributor_stats] | |
| # Create matplotlib figure | |
| fig, ax = plt.subplots(figsize=(6, 4)) | |
| bars = ax.bar(types, counts, color='#3498db', edgecolor='#2980b9', linewidth=1.5) | |
| # Add value labels on bars | |
| for bar in bars: | |
| height = bar.get_height() | |
| ax.text(bar.get_x() + bar.get_width()/2., height, | |
| f'{int(height)}', | |
| ha='center', va='bottom', fontsize=10, fontweight='bold') | |
| ax.set_xlabel('Contributor Type', fontsize=11, fontweight='bold') | |
| ax.set_ylabel('Count', fontsize=11, fontweight='bold') | |
| ax.set_title('Submissions by Contributor Type', fontsize=14, fontweight='bold') | |
| ax.grid(axis='y', alpha=0.3) | |
| plt.xticks(rotation=45, ha='right') | |
| # Convert to image | |
| img_buffer = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(img_buffer, format='png', dpi=150, bbox_inches='tight') | |
| plt.close(fig) | |
| img_buffer.seek(0) | |
| img = Image(img_buffer, width=5*inch, height=3.5*inch) | |
| return img | |
| except Exception as e: | |
| print(f"Error creating contributor chart: {e}") | |
| return None | |
| def _create_breakdown_table(self, breakdown, contributor_types): | |
| """Create category breakdown table""" | |
| # Prepare table data | |
| headers = ['Category'] + [ct['label'] for ct in contributor_types] | |
| data = [headers] | |
| for category, counts in breakdown.items(): | |
| row = [category] | |
| for ct in contributor_types: | |
| row.append(str(counts.get(ct['value'], 0))) | |
| data.append(row) | |
| # Calculate column widths | |
| num_cols = len(headers) | |
| col_width = 6.5 * inch / num_cols | |
| table = Table(data, colWidths=[col_width] * num_cols) | |
| table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498db')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'CENTER'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 10), | |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.grey), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#ecf0f1')]) | |
| ])) | |
| return table | |
| def _create_map(self, geotagged_submissions, categories): | |
| """Create geographic distribution map with real OpenStreetMap tiles""" | |
| if not geotagged_submissions: | |
| return None | |
| try: | |
| # Prepare data | |
| lats = [s.latitude for s in geotagged_submissions] | |
| lons = [s.longitude for s in geotagged_submissions] | |
| cats = [s.category for s in geotagged_submissions] | |
| # Create matplotlib figure | |
| fig, ax = plt.subplots(figsize=(10, 8)) | |
| # Color map for categories | |
| category_colors = { | |
| 'Vision': '#3498db', | |
| 'Problem': '#e74c3c', | |
| 'Objectives': '#2ecc71', | |
| 'Directives': '#f39c12', | |
| 'Values': '#9b59b6', | |
| 'Actions': '#1abc9c' | |
| } | |
| # Plot points by category | |
| for category in set(cats): | |
| cat_lats = [lat for lat, cat in zip(lats, cats) if cat == category] | |
| cat_lons = [lon for lon, cat in zip(lons, cats) if cat == category] | |
| color = category_colors.get(category, '#95a5a6') | |
| ax.scatter(cat_lons, cat_lats, c=color, label=category, | |
| s=150, alpha=0.8, edgecolors='white', linewidths=2, zorder=5) | |
| # Add OpenStreetMap basemap if contextily is available | |
| if HAS_CONTEXTILY: | |
| try: | |
| # Add map tiles | |
| cx.add_basemap(ax, crs='EPSG:4326', source=cx.providers.OpenStreetMap.Mapnik, | |
| attribution=False, alpha=0.8) | |
| except Exception as e: | |
| print(f"Could not add basemap: {e}") | |
| # Fallback to grid | |
| ax.grid(True, alpha=0.3) | |
| else: | |
| # Fallback: simple grid | |
| ax.grid(True, alpha=0.3) | |
| ax.set_xlabel('Longitude', fontsize=12, fontweight='bold') | |
| ax.set_ylabel('Latitude', fontsize=12, fontweight='bold') | |
| ax.set_title('Geographic Distribution of Submissions', | |
| fontsize=16, fontweight='bold', pad=20) | |
| # Legend outside plot area | |
| ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), | |
| fontsize=10, frameon=True, fancybox=True, shadow=True) | |
| # Add attribution text if using OpenStreetMap | |
| if HAS_CONTEXTILY: | |
| fig.text(0.99, 0.01, '© OpenStreetMap contributors', | |
| ha='right', va='bottom', fontsize=7, style='italic', alpha=0.7) | |
| # Convert to image | |
| img_buffer = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(img_buffer, format='png', dpi=200, bbox_inches='tight') | |
| plt.close(fig) | |
| img_buffer.seek(0) | |
| img = Image(img_buffer, width=7*inch, height=5.5*inch) | |
| return img | |
| except Exception as e: | |
| print(f"Error creating map: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return None | |