general6 min read

How to Download and Embed AI-Generated Charts

Retrieve chart PNGs from DataStoryBot's file proxy, embed them in HTML, React, or email. Practical guide to working with AI-generated chart files.

By DataStoryBot Team

How to Download and Embed AI-Generated Charts

DataStoryBot's analysis produces chart images as PNG files stored in the container. The analysis response gives you file IDs. This article shows how to retrieve those files, embed them in different contexts (HTML pages, React apps, emails, PDFs), and handle the container lifecycle so your charts don't disappear.

The Chart File Lifecycle

When DataStoryBot generates charts during analysis, the files live in the container:

Upload CSV → Container created (20-min TTL)
   → /analyze → Charts generated (file IDs in response)
   → /refine → More charts generated
   → 20 minutes → Container expires → Charts deleted

Critical point: Chart files are ephemeral. They exist only while the container is alive. If you need the charts beyond the 20-minute window, download them immediately after the analysis completes.

Downloading Charts

The refine response includes chart file IDs:

{
  "narrative": "...",
  "charts": [
    {
      "fileId": "file-chart001",
      "caption": "Monthly revenue trend with year-over-year comparison"
    },
    {
      "fileId": "file-chart002",
      "caption": "Revenue by product category (bar chart)"
    }
  ]
}

Download with curl

CONTAINER_ID="ctr_abc123"

curl -X GET "https://datastory.bot/api/files/$CONTAINER_ID/file-chart001" \
  --output revenue_trend.png

curl -X GET "https://datastory.bot/api/files/$CONTAINER_ID/file-chart002" \
  --output revenue_by_category.png

Download with Python

import requests

BASE_URL = "https://datastory.bot/api"

def download_charts(container_id, charts, output_dir="/tmp"):
    """Download all charts from a refine response."""
    paths = []
    for i, chart in enumerate(charts):
        response = requests.get(
            f"{BASE_URL}/files/{container_id}/{chart['fileId']}"
        )
        response.raise_for_status()

        # Generate a clean filename from the caption
        slug = chart["caption"][:50].lower()
        slug = "".join(c if c.isalnum() or c == " " else "" for c in slug)
        slug = slug.strip().replace(" ", "_")
        filename = f"{output_dir}/{slug}_{i+1}.png"

        with open(filename, "wb") as f:
            f.write(response.content)

        paths.append({
            "path": filename,
            "caption": chart["caption"],
            "size": len(response.content)
        })

    return paths

Download with JavaScript

async function downloadCharts(containerId, charts, outputDir = "/tmp") {
  const results = [];

  for (let i = 0; i < charts.length; i++) {
    const res = await fetch(
      `https://datastory.bot/api/files/${containerId}/${charts[i].fileId}`
    );
    const buffer = await res.arrayBuffer();
    const filename = `${outputDir}/chart_${i + 1}.png`;

    const fs = require("fs");
    fs.writeFileSync(filename, Buffer.from(buffer));

    results.push({ path: filename, caption: charts[i].caption });
  }

  return results;
}

Embedding in HTML

Once downloaded, charts are standard PNG files. Embed them like any image.

Static HTML

<figure>
  <img
    src="./charts/revenue_trend.png"
    alt="Monthly revenue trend showing 23% growth quarter-over-quarter"
    width="800"
    loading="lazy"
  />
  <figcaption>Monthly revenue trend with year-over-year comparison</figcaption>
</figure>

Always use the chart's caption from the API response as the alt text — it's descriptive and accessibility-friendly.

Base64 Embedding (Self-Contained HTML)

For emails and single-file reports, embed the image data directly:

import base64

def chart_to_base64_img(chart_path, caption):
    """Convert a chart file to a base64 HTML img tag."""
    with open(chart_path, "rb") as f:
        b64 = base64.b64encode(f.read()).decode()
    return f'<img src="data:image/png;base64,{b64}" alt="{caption}" style="max-width: 100%;">'
<img
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
  alt="Monthly revenue trend"
  style="max-width: 100%;"
/>

Base64 images increase HTML size by ~33% (the encoding overhead), but the HTML is fully self-contained — no external file references to break.

Embedding in React

From a URL (server-side proxy)

If your backend downloads the charts and serves them:

function DataStoryChart({ src, caption }) {
  return (
    <figure className="my-6">
      <img
        src={src}
        alt={caption}
        className="w-full rounded-lg shadow-md"
        loading="lazy"
      />
      <figcaption className="mt-2 text-sm text-gray-500 text-center">
        {caption}
      </figcaption>
    </figure>
  );
}

function AnalysisReport({ report }) {
  return (
    <article>
      <div
        className="prose"
        dangerouslySetInnerHTML={{
          __html: markdownToHtml(report.narrative),
        }}
      />
      {report.charts.map((chart, i) => (
        <DataStoryChart
          key={i}
          src={`/api/charts/${report.containerId}/${chart.fileId}`}
          caption={chart.caption}
        />
      ))}
    </article>
  );
}

From Base64 (client-side)

If you're passing chart data directly to the frontend:

function Base64Chart({ base64Data, caption }) {
  return (
    <figure className="my-6">
      <img
        src={`data:image/png;base64,${base64Data}`}
        alt={caption}
        className="w-full rounded-lg"
      />
      <figcaption className="mt-2 text-sm text-gray-500 text-center">
        {caption}
      </figcaption>
    </figure>
  );
}

Embedding in Emails

Email clients have notoriously inconsistent image support. Two approaches:

CID Attachments (Most Compatible)

Attach the chart as a MIME part and reference it with a cid: URL:

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

msg = MIMEMultipart("related")
msg["Subject"] = "Weekly Data Report"

# HTML body referencing the chart by CID
html = """
<h2>Revenue Trend</h2>
<img src="cid:chart1" alt="Revenue trend" style="max-width: 600px;">
<p>Revenue grew 23% quarter-over-quarter...</p>
"""
msg.attach(MIMEText(html, "html"))

# Attach the chart image
with open("revenue_trend.png", "rb") as f:
    img = MIMEImage(f.read())
    img.add_header("Content-ID", "<chart1>")
    img.add_header("Content-Disposition", "inline", filename="revenue_trend.png")
    msg.attach(img)

CID attachments work in Outlook, Gmail, Apple Mail, and most corporate email clients.

Hosted URL

Upload the chart to S3, R2, or any CDN, then reference the public URL:

import boto3

s3 = boto3.client("s3")
s3.upload_file(
    "revenue_trend.png",
    "report-assets",
    "charts/2026-03-24/revenue_trend.png",
    ExtraArgs={
        "ContentType": "image/png",
        "CacheControl": "max-age=31536000"
    }
)

chart_url = "https://report-assets.s3.amazonaws.com/charts/2026-03-24/revenue_trend.png"

Hosted URLs are simpler but require the recipient to have internet access to view them. Some corporate email filters block external images by default.

Resizing and Optimization

DataStoryBot charts are generated at 150+ DPI — high quality but large file sizes (30-100 KB per chart). For web embedding, you may want to optimize:

from PIL import Image

def optimize_chart(input_path, output_path, max_width=800):
    """Resize and compress a chart for web use."""
    img = Image.open(input_path)

    # Resize if wider than max_width
    if img.width > max_width:
        ratio = max_width / img.width
        new_size = (max_width, int(img.height * ratio))
        img = img.resize(new_size, Image.LANCZOS)

    # Save with optimization
    img.save(output_path, "PNG", optimize=True)

    return output_path

For email, keep charts under 600px wide — most email clients cap the content area at that width.

Persistent Storage Pattern

Since container files are ephemeral, production applications should download and store charts immediately:

def analyze_and_persist(csv_path, steering=None):
    """Analyze CSV and persist all outputs to durable storage."""
    # Run analysis
    container_id, report = run_analysis(csv_path, steering)

    # Download and store charts immediately
    stored_charts = []
    for chart in report.get("charts", []):
        # Download from container
        response = requests.get(
            f"{BASE_URL}/files/{container_id}/{chart['fileId']}"
        )

        # Store durably (S3, R2, local filesystem, database)
        key = f"analyses/{container_id}/{chart['fileId']}.png"
        store_to_s3(key, response.content)

        stored_charts.append({
            "url": f"https://your-cdn.com/{key}",
            "caption": chart["caption"]
        })

    return {
        "narrative": report["narrative"],
        "charts": stored_charts
    }

Never rely on the container being alive when the user wants to view the chart. Download immediately, store permanently.

What to Read Next

For the chart generation fundamentals, see how to generate charts from CSV data automatically.

For chart styling and dark/light theme options, read about generating multiple chart types from a single dataset.

For building the complete analysis-to-report pipeline, see PDF data reports from AI.

For the API reference covering the files endpoint, see the DataStoryBot API reference.

Ready to find your data story?

Upload a CSV and DataStoryBot will uncover the narrative in seconds.

Try DataStoryBot →