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.
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 →