Chart Accessibility: Alt Text and Captions from AI
How DataStoryBot generates captions for every chart, and how to use those captions as alt text for accessible data visualization.
Chart Accessibility: Alt Text and Captions from AI
Every chart DataStoryBot generates comes with a caption — a natural-language description of what the chart shows, written at the time the AI constructs the visualization. That caption isn't decorative. It's the mechanism by which your charts become accessible to screen reader users, users on slow connections where images fail to load, and users in any context where the PNG cannot be rendered.
This article covers why chart accessibility matters, how DataStoryBot's captions map directly to WCAG requirements, how to extract them from API responses, and how to wire them correctly into HTML and React.
Why Chart Accessibility Is Not Optional
WCAG 2.1 and the Legal Baseline
WCAG 2.1 Success Criterion 1.1.1 (Non-text Content) requires that every image conveying information has a text alternative that serves the equivalent purpose. Charts are explicitly in scope. The criterion makes no exception for "complex" images — if the chart communicates data, that data must be available as text.
For products subject to ADA Title III (US), Section 508 (US federal), EN 301 549 (EU), or AODA (Canada), inaccessible charts are a compliance failure. Class action suits over inaccessible web content have been filed against retailers, banks, universities, and SaaS companies. Courts have increasingly ruled that WCAG 2.1 AA is the applicable standard.
Even outside regulated industries, accessibility is a correctness issue. A chart with no alt text is broken for screen reader users the same way a broken API endpoint is broken for programmatic consumers.
Who Uses Screen Readers
The common assumption is that screen reader users are blind. The actual population is broader: users with low vision who supplement screen magnification with audio, users with cognitive disabilities who process text more easily than images, users with temporary impairments (eye surgery, migraine), and users on audio-first devices. The WebAIM Screen Reader User Survey reports roughly 7% of respondents use screen readers "always or often" during web browsing — a meaningful slice of any user base.
Image Loading Failures
Alt text isn't only for screen readers. It's the fallback for:
- Images blocked by corporate firewalls or content filters
- Images that fail to load due to network errors
- Email clients with external image loading disabled (the majority of enterprise email clients default to blocking images)
- Browser configurations that disable images for bandwidth reasons
If your embedded chart has no alt text and the image fails to load, the user sees a broken image icon. If it has alt text, they see the description.
How DataStoryBot Captions Work
When DataStoryBot's code interpreter generates a chart, the AI writes the caption in the same step it writes the visualization code. The caption describes what the chart actually shows — the data pattern, the notable finding, the axis variables — not just the chart type.
The API response includes captions in the charts array:
{
"narrative": "Revenue grew 31% year-over-year...",
"charts": [
{
"fileId": "file-chart001",
"caption": "Line chart showing monthly revenue from Jan–Dec 2025. Revenue rises steadily from $1.2M in January to $3.8M in December, with a notable acceleration in Q4 driven by holiday sales."
},
{
"fileId": "file-chart002",
"caption": "Bar chart comparing revenue by product category. Electronics leads at $12.4M, followed by Apparel ($8.1M) and Home Goods ($5.2M). Electronics is the only category showing year-over-year growth."
}
]
}
These captions do more than label the chart type. They describe the data pattern ("rises steadily"), call out the notable finding ("acceleration in Q4"), and provide the scale ("$1.2M in January to $3.8M in December"). That's exactly what a useful alt text needs to do.
What Makes a Good Alt Text for a Chart
WCAG's guidance on complex images recommends that alt text for a chart convey the same information a sighted user would get. That means:
- The chart type (line chart, bar chart, scatter plot)
- The axes and what they measure
- The overall trend or key finding
- Significant outliers or notable data points
- Units and scale where relevant
DataStoryBot's captions are generated with this structure in mind. The AI has seen the data, produced the chart, and describes both what the axes show and what the data pattern means. You get captions that read like a human data analyst wrote them — because the AI is performing data analysis, not just labeling chart types.
Extracting Captions from the API Response
Python
import requests
BASE_URL = "https://datastory.bot/api"
def get_accessible_charts(container_id, charts):
"""
Returns a list of dicts with fileId, caption, and the chart bytes.
The caption is ready to use as alt text.
"""
results = []
for chart in charts:
response = requests.get(
f"{BASE_URL}/files/{container_id}/{chart['fileId']}"
)
response.raise_for_status()
results.append({
"fileId": chart["fileId"],
"caption": chart["caption"], # use directly as alt text
"bytes": response.content
})
return results
def run_analysis(csv_path):
# Upload
with open(csv_path, "rb") as f:
upload = requests.post(
f"{BASE_URL}/upload",
files={"file": f}
)
container_id = upload.json()["containerId"]
# Analyze
analysis = requests.post(
f"{BASE_URL}/analyze",
json={"containerId": container_id}
)
analysis_data = analysis.json()
# Refine
refine = requests.post(
f"{BASE_URL}/refine",
json={"containerId": container_id}
)
refine_data = refine.json()
charts = get_accessible_charts(container_id, refine_data["charts"])
return {
"narrative": refine_data["narrative"],
"charts": charts
}
JavaScript / TypeScript
interface ChartResult {
fileId: string;
caption: string; // ready to use as alt text
blobUrl: string; // object URL for client-side rendering
}
async function getAccessibleCharts(
containerId: string,
charts: { fileId: string; caption: string }[]
): Promise<ChartResult[]> {
return Promise.all(
charts.map(async (chart) => {
const res = await fetch(
`https://datastory.bot/api/files/${containerId}/${chart.fileId}`
);
const blob = await res.blob();
return {
fileId: chart.fileId,
caption: chart.caption,
blobUrl: URL.createObjectURL(blob),
};
})
);
}
Embedding with Proper Alt Text in HTML
The caption field maps directly to the alt attribute. Use a <figure> + <figcaption> structure so the description is visible to sighted users as well:
<figure>
<img
src="/charts/revenue-trend.png"
alt="Line chart showing monthly revenue from Jan–Dec 2025. Revenue rises steadily from $1.2M in January to $3.8M in December, with a notable acceleration in Q4 driven by holiday sales."
width="800"
height="450"
loading="lazy"
/>
<figcaption>
Monthly revenue Jan–Dec 2025 — Q4 acceleration driven by holiday sales
</figcaption>
</figure>
A few points on this pattern:
alt vs figcaption: The alt attribute should be the full, data-complete description that a screen reader user needs to understand the chart without seeing it. The <figcaption> is visible to all users and can be a shorter, more conversational label. Both can come from the caption field — use the full string for alt, and trim or rephrase for figcaption.
width and height: Specifying dimensions prevents layout shift (CLS) as the image loads. DataStoryBot charts are typically generated at a fixed aspect ratio — check the image dimensions from the first download and hardcode them.
loading="lazy": Appropriate for charts below the fold. Use loading="eager" (the default) for above-the-fold charts.
Generating an Accessible HTML Report in Python
import base64
def build_accessible_report(narrative, charts):
"""
Build a self-contained HTML report with accessible chart embeds.
Inline the narrative markdown as HTML and embed charts with full alt text.
"""
chart_html = ""
for chart in charts:
b64 = base64.b64encode(chart["bytes"]).decode()
chart_html += f"""
<figure>
<img
src="data:image/png;base64,{b64}"
alt="{chart['caption']}"
style="max-width: 100%; height: auto;"
/>
<figcaption style="font-size: 0.875rem; color: #666; margin-top: 0.5rem;">
{chart['caption']}
</figcaption>
</figure>
"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Report</title>
</head>
<body>
<main>
<div>{narrative}</div>
{chart_html}
</main>
</body>
</html>"""
Embedding with Proper Alt Text in React
interface Chart {
fileId: string;
caption: string;
src: string; // URL or object URL
}
interface ChartFigureProps {
chart: Chart;
shortCaption?: string; // optional visible label; defaults to full caption
}
function ChartFigure({ chart, shortCaption }: ChartFigureProps) {
return (
<figure className="my-8">
<img
src={chart.src}
alt={chart.caption} // full description for screen readers
className="w-full rounded-lg"
loading="lazy"
/>
<figcaption className="mt-2 text-sm text-gray-500 text-center">
{shortCaption ?? chart.caption} {/* visible to all users */}
</figcaption>
</figure>
);
}
interface AnalysisReportProps {
narrative: string;
charts: Chart[];
}
function AnalysisReport({ narrative, charts }: AnalysisReportProps) {
return (
<article>
<div className="prose max-w-none">{narrative}</div>
{charts.map((chart) => (
<ChartFigure key={chart.fileId} chart={chart} />
))}
</article>
);
}
Handling Loading and Error States Accessibly
import { useState } from "react";
function AccessibleChartFigure({ chart }: { chart: Chart }) {
const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading");
return (
<figure
className="my-8"
aria-busy={status === "loading"}
aria-label={status === "loading" ? "Chart loading" : undefined}
>
{status === "error" ? (
// Fallback: show the description as text when image fails
<div
role="img"
aria-label={chart.caption}
className="p-4 border border-gray-200 rounded-lg bg-gray-50 text-sm text-gray-600"
>
<strong>Chart unavailable.</strong> {chart.caption}
</div>
) : (
<img
src={chart.src}
alt={chart.caption}
className={`w-full rounded-lg transition-opacity ${
status === "loading" ? "opacity-0" : "opacity-100"
}`}
onLoad={() => setStatus("loaded")}
onError={() => setStatus("error")}
loading="lazy"
/>
)}
<figcaption className="mt-2 text-sm text-gray-500 text-center">
{chart.caption}
</figcaption>
</figure>
);
}
The error state is particularly important: when the image fails to load, the component falls back to rendering the caption text in a role="img" div. Screen reader users get the description either way; sighted users get an informative message instead of a broken image icon.
Accessibility Best Practices for Data Visualization
Color Is Not the Only Encoding
DataStoryBot's dark-theme palette uses high-saturation colors that are visually distinct for most users, but color should never be the sole distinguishing attribute for data series. Ensure:
- Line charts use different dash patterns (solid, dashed, dotted) in addition to color
- Bar charts use different patterns or textures in addition to fill color
- Labels are directly on or adjacent to data series, not only in a legend
For more on chart styling decisions, see dark-theme data visualization.
Sufficient Color Contrast
WCAG 1.4.3 requires a contrast ratio of at least 4.5:1 for text, 3:1 for large text and graphical elements. Chart text (axis labels, tick marks, legends) must meet these ratios. DataStoryBot's default dark-theme palette is designed with this in mind — white or light-gray text on dark backgrounds achieves ratios well above 4.5:1.
If you're requesting custom color schemes via the steering prompt, verify contrast ratios with a tool like the WebAIM Contrast Checker before deploying.
Don't Use alt="" for Informational Charts
An empty alt attribute (alt="") signals to screen readers that an image is decorative and can be skipped. This is correct for purely decorative images (background textures, icon ornaments). It is wrong for data charts. A chart with alt="" is invisible to screen reader users — they won't know it exists, let alone what it shows.
Use alt="" only for images that add no information to the page: decorative separators, background images, icon sprites where the surrounding text already explains the action.
Provide Data Tables Alongside Complex Charts
For very complex charts — scatter plots with many data points, heatmaps, charts with more than six series — a text description alone may not convey the full dataset. Consider providing the underlying data as an HTML table:
<figure>
<img
src="/charts/category-breakdown.png"
alt="Bar chart comparing revenue by product category..."
/>
<figcaption>Revenue by product category, FY2025</figcaption>
</figure>
<details>
<summary>View data table</summary>
<table>
<caption>Revenue by product category, FY2025</caption>
<thead>
<tr><th>Category</th><th>Revenue</th><th>YoY Change</th></tr>
</thead>
<tbody>
<tr><td>Electronics</td><td>$12.4M</td><td>+18%</td></tr>
<tr><td>Apparel</td><td>$8.1M</td><td>-3%</td></tr>
<tr><td>Home Goods</td><td>$5.2M</td><td>+2%</td></tr>
</tbody>
</table>
</details>
The <details> / <summary> pattern keeps the table collapsed for sighted users who don't need it while making it fully available to screen reader users and anyone who wants the underlying numbers.
Keyboard Navigation for Interactive Charts
If you're rendering DataStoryBot charts in an interactive container (lightbox, zoom overlay, tabbed view), ensure:
- The container is reachable by keyboard (Tab key)
- Close controls are keyboard-operable (Escape key)
- Focus is trapped inside modals and returned to the trigger element on close
- Any controls (zoom in, zoom out, download) have accessible labels
The Narrative as Supplementary Accessibility Content
DataStoryBot's narrative field — the prose analysis that accompanies the charts — is itself a form of accessible content. The narrative describes findings in complete sentences, references specific data points, and explains relationships between variables. A user who cannot see the charts can read the narrative and understand the analysis.
When building your UI, treat the narrative and the charts as complementary:
- The narrative gives the analytical conclusions
- The charts show the data visually
- The alt text bridges the gap for users who cannot see the charts
Keep the narrative and charts in proximity in the DOM so screen reader users encounter the written analysis alongside the image descriptions. Don't separate them across sections or pages.
What to Read Next
For the mechanics of downloading and embedding chart files from the API, see how to download and embed AI-generated charts.
For how DataStoryBot decides which chart types to generate from a given dataset, see how to generate charts from CSV data automatically.
For the design rationale behind the default chart styling and how to request light-theme variants, see dark-theme data visualization.
Ready to find your data story?
Upload a CSV and DataStoryBot will uncover the narrative in seconds.
Try DataStoryBot →