# How to Generate a PDF Report with Charts from GEO‑SEO Claude

> Generate a PDF report with charts from GEO-SEO Claude. Extend the Flask web app to aggregate data, create matplotlib visualizations, and compile into a ReportLab PDF streamed to your browser.

- Repository: [Zubair Trabzada/geo-seo-claude](https://github.com/zubair-trabzada/geo-seo-claude)
- Tags: how-to-guide
- Published: 2026-09-08

---

**You can generate a PDF report with charts from GEO‑SEO Claude by extending the Flask web application with a new route that aggregates prospect data, creates a matplotlib visualization, and compiles both into a ReportLab PDF streamed directly to the browser.**

GEO‑SEO Claude stores prospect data in a local JSON file at `~/.geo-prospects/prospects.json` and serves it through a lightweight Flask web UI located in [`scripts/webapp/app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/scripts/webapp/app.py). To create visual reports, you will add server-side logic that reuses existing data helpers, generates charts in-memory, and delivers the final PDF without writing temporary files to disk.

## Installation Requirements

Before implementing the reporting feature, install the required visualization and PDF libraries into your project environment.

```bash
pip install matplotlib reportlab

```

These packages integrate cleanly with the existing Flask application and require no external services or API keys.

## Step 1: Load Prospect Data

The application already provides a `load_prospects()` helper in [`scripts/webapp/app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/scripts/webapp/app.py) that reads and parses the local JSON storage. Reuse this function to ensure your report always uses the current dataset.

As implemented in the source code, `load_prospects()` handles file I/O and returns a list of prospect dictionaries. This keeps your new reporting code consistent with existing routes like `/prospect/<pid>/pdf` found at lines 92‑108 in [`app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/app.py).

## Step 2: Aggregate Statistics

Compute summary metrics using the same logic as the existing `crm_stats()` function (defined at lines 58‑72 in [`scripts/webapp/app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/scripts/webapp/app.py)). This ensures your report text matches the statistics displayed elsewhere in the UI.

Typical aggregations include total prospect count, average `geo_score`, and status groupings. Process these values in pure Python before passing them to the PDF generator.

## Step 3: Generate the Chart

Create a chart visualization using **matplotlib** and render it to a `BytesIO` buffer. This avoids filesystem clutter and keeps the workflow stateless.

The following function counts prospects by geo-score tiers and returns PNG bytes:

```python
import io
import matplotlib.pyplot as plt

def build_chart(prospects: list[dict]) -> bytes:
    """Return PNG bytes of a prospect distribution chart."""
    tiers = {"good": 0, "moderate": 0, "poor": 0, "critical": 0}
    for p in prospects:
        score = p.get("geo_score", 0)
        if score >= 80:
            tiers["good"] += 1
        elif score >= 60:
            tiers["moderate"] += 1
        elif score >= 40:
            tiers["poor"] += 1
        else:
            tiers["critical"] += 1

    fig, ax = plt.subplots(figsize=(6, 4))
    ax.bar(tiers.keys(), tiers.values(), color="#4c8bf5")
    ax.set_title("Prospect Geo‑Score Distribution")
    ax.set_ylabel("Count")
    plt.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png")
    plt.close(fig)
    buf.seek(0)
    return buf.read()

```

## Step 4: Compose the PDF Document

Use **ReportLab** to layout text, statistics, and the embedded chart in a single PDF document. The following function accepts the PNG bytes and a statistics dictionary, then returns the complete PDF as bytes:

```python
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader

def create_pdf(chart_png: bytes, stats: dict) -> bytes:
    """Return PDF bytes containing text stats and the chart image."""
    buf = io.BytesIO()
    c = canvas.Canvas(buf, pagesize=A4)
    width, height = A4

    # Header

    c.setFont("Helvetica-Bold", 16)
    c.drawString(40, height - 50, "GEO‑SEO Claude – Prospect Summary")

    # Stats table

    c.setFont("Helvetica", 12)
    y = height - 90
    for key, value in stats.items():
        c.drawString(40, y, f"{key.capitalize()}: {value}")
        y -= 20

    # Embed chart

    img = ImageReader(io.BytesIO(chart_png))
    img_width, img_height = img.getSize()
    max_width = 500
    scale = min(max_width / img_width, 1)
    img_width *= scale
    img_height *= scale
    c.drawImage(img, 40, y - img_height - 20, 
                width=img_width, height=img_height)

    c.showPage()
    c.save()
    buf.seek(0)
    return buf.read()

```

## Step 5: Register the Flask Route

Add a new endpoint to [`scripts/webapp/app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/scripts/webapp/app.py) that orchestrates the data loading, chart generation, and PDF compilation. Use Flask’s `send_file` utility to stream the result, following the same pattern as the existing individual prospect PDF route.

```python
from flask import send_file, abort

@app.route("/report/pdf")
def download_report():
    """Generate and download a full prospect report with charts."""
    prospects = load_prospects()
    if not prospects:
        abort(404, description="No prospect data available")

    chart_png = build_chart(prospects)
    stats = crm_stats(prospects)
    pdf_bytes = create_pdf(chart_png, stats)

    return send_file(
        io.BytesIO(pdf_bytes),
        as_attachment=True,
        download_name="geo_seo_report.pdf",
        mimetype="application/pdf",
    )

```

## Integrating with the Web Interface

Expose the new functionality by adding a download link or button in your HTML templates. Because the PDF is generated on the server and streamed back, the client only needs a standard anchor tag:

```html
<a href="/report/pdf" class="btn btn-primary">Download PDF Report</a>

```

Clicking this link triggers the full pipeline—data aggregation, chart rendering, and PDF compilation—and immediately prompts the browser to save `geo_seo_report.pdf`.

## Summary

- **Data source**: GEO‑SEO Claude stores prospects in `~/.geo-prospects/prospects.json`, accessed via `load_prospects()` in [`scripts/webapp/app.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/scripts/webapp/app.py).
- **Charting**: Use **matplotlib** to generate visualizations and buffer them as PNG bytes in memory.
- **PDF creation**: Use **ReportLab** to embed text, statistics, and images into a single PDF document without temporary files.
- **Delivery**: Extend the Flask app with a `/report/pdf` route that streams the final document using `send_file`, matching the architecture of existing download endpoints.
- **Dependencies**: Requires only `matplotlib` and `reportlab`, maintaining the project’s "no-external-services" design philosophy.

## Frequently Asked Questions

### Do I need external APIs to generate PDFs in GEO‑SEO Claude?

No. The solution relies entirely on local Python libraries. **ReportLab** and **matplotlib** process everything server-side using the existing prospect JSON file, so no third-party PDF services or cloud APIs are required.

### Can I customize the chart colors to match my brand?

Yes. Modify the `color` parameter in the `ax.bar()` call within the `build_chart()` function. You can also import color values from [`white-label/brand_config.py`](https://github.com/zubair-trabzada/geo-seo-claude/blob/main/white-label/brand_config.py) if you have defined brand-specific palettes elsewhere in the project.

### Is it possible to add multiple charts to the same report?

Yes. Extend the `create_pdf()` function to accept additional image buffers and call `c.drawImage()` for each chart at different vertical positions on the page. You can also create new pages with `c.showPage()` before adding the next visualization.

### How do I troubleshoot if the PDF generation fails?

First, verify that `~/.geo-prospects/prospects.json` exists and contains valid data, as the route calls `abort(404)` when the prospect list is empty. Next, check that `matplotlib` and `reportlab` are installed in the same Python environment running the Flask server. Finally, inspect the server logs for specific errors from `build_chart()` or `create_pdf()`, which usually indicate data formatting issues or memory constraints when processing large prospect lists.