An AI will happily write you a chart in ten seconds. The trouble comes three weeks later, when someone asks you to regenerate it with October's data and the code no longer matches the image, or when a colleague notices that your "surging revenue" chart is nine nearly flat bars with a cropped axis. This post is about reproducible matplotlib charts from AI prompts: how to ask for scripts you can rerun, and a review checklist, partly automated, that catches misleading charts before they leave your desk.
Everything labelled "ran" below was run on 2026-10-09 with Python 3.13, matplotlib 3.11.2 and pandas 3.0.5. Images can't be embedded in this post, so I describe each chart in words and give measured numbers. The data is the synthetic 5,000-order file from the spreadsheet vs Python comparison.
What should a reproducible-chart prompt contain?
Most chart prompts say what to plot and stop. That gets you a chart, not a rerunnable one. This is an untested template (I wrote the script below by hand to check the idea; I did not run this prompt on a live model):
Write one Python script that makes this chart from a file, using matplotlib and pandas.
Input: [path to CSV], columns [list with types]. Known cleaning needs: [duplicates, currency text, date formats].
Chart: [type] of [metric] by [dimension], for [period]. Units: [INR millions].
Requirements:
1. Take the input path and output folder as command-line arguments. No hard-coded paths.
2. Compute every plotted number inside the script. No numbers typed in by hand.
3. Use matplotlib.use("Agg") and set figure size, font family and dpi explicitly.
4. Nothing random and nothing that reads today's date.
5. Save the PNG and also write the plotted data to chart_data.csv in the same folder.
6. Bar charts start at zero. Title names the metric, the period and any filter. Both axes labelled with units.
7. Print the plotted numbers to the terminal.
Then list the three assumptions you made about the data.
The last line matters most. Asking the model to list its assumptions surfaces things like "I treated all rows as paid orders", which is exactly what a reviewer needs to see. Requirement 5 gives you a diffable artefact: if a rerun produces a different chart_data.csv, the data changed. If only the PNG changed, the rendering did.
What does the script look like, and does it rerun identically?
The data step applies the same cleaning rules as the earlier post (de-duplicate, strip ₹ and commas, parse two date formats) and then sums by month in millions of rupees. The part that makes the chart is below (an excerpt of the script I ran):
import matplotlib
matplotlib.use("Agg") # no display needed, same backend every run
import matplotlib.pyplot as plt
def make(honest: bool, path: str):
plt.rcParams.update({"font.family": "DejaVu Sans", "figure.dpi": 100})
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(monthly.index, monthly.values, color="#3b6ea5")
if honest:
ax.set_ylim(0, monthly.max() * 1.1)
ax.set_title("Monthly revenue, Jan-Sep 2026 (paid and unpaid orders, de-duplicated)")
else:
lo = monthly.min() * 0.97
ax.set_ylim(lo, monthly.max() * 1.01)
ax.set_title("Revenue is surging")
ax.set_ylabel("Revenue (INR millions)")
ax.set_xlabel("Month")
ax.tick_params(axis="x", rotation=45)
fig.tight_layout()
fig.savefig(path, metadata={"Software": None}) # drop version string from PNG
return fig, ax
The same function draws both the honest chart and a deliberately misleading one, so you can see what changed. The monthly totals the script printed:
month
2026-01 5.55
2026-02 5.44
2026-03 5.93
2026-04 5.91
2026-05 5.32
2026-06 5.71
2026-07 5.26
2026-08 5.53
2026-09 5.32
I ran the script twice into two separate folders and compared the SHA-256 of each PNG. The first 16 characters matched both times: c275ac184191cfd1 for the honest chart and 4198af18abc50ba0 for the misleading one. So the rerun was byte-identical on this machine. That is a statement about one machine and one library version, not a guarantee. Pin your versions in a requirements.txt and, if you care about exact pixels, record the matplotlib version beside the hash.
What do the two charts look like?
The honest chart. Nine blue vertical bars, one per month from 2026-01 to 2026-09. The y-axis runs from 0 to about 6.5 and is labelled "Revenue (INR millions)". Every bar reaches between roughly 5.3 and 5.9, so the eye reads them as nearly equal, with a slight dip in May and again in July and September. The title gives the metric, the period and the filter. ("Paid and unpaid" is the title I used because the synthetic file has no status column, so every order counts; with real data, write whichever filter you actually applied.)
The misleading chart. Same data, same colour. The y-axis runs from about 5.1 to 5.95, and the title says "Revenue is surging". March and April tower over the others; July's bar is a stub. A reader sees a collapse and a spike, not nine months in a 13 percent band. Nothing in the data changed. Two lines of axis code and one headline did all of that.
The numbers behind it: the tallest bar (March, 5.93) is 1.13 times the shortest (July, 5.26). On the cropped axis, the tallest bar is drawn 5.25 times taller than the shortest. That gap between 1.13x and 5.25x is what a truncated bar-chart axis does to your reader. Also, "surging" is false: September is lower than January.
Can you check for misleading charts automatically?
Partly. Some failures are visible in the figure object, so a function can flag them. This is the audit I ran on both charts:
def audit(ax, name):
problems = []
bars = [p for p in ax.patches]
lo, hi = ax.get_ylim()
if bars and lo > 0:
tallest, shortest = max(b.get_height() for b in bars), min(b.get_height() for b in bars)
drawn_ratio = (tallest - lo) / (shortest - lo)
true_ratio = tallest / shortest
problems.append(f"bar chart y-axis starts at {lo:.1f}, not 0: drawn ratio {drawn_ratio:.2f}x vs true ratio {true_ratio:.2f}x")
if not ax.get_ylabel():
problems.append("no y-axis label (units missing)")
if not ax.get_xlabel():
problems.append("no x-axis label")
title = ax.get_title()
if not any(ch.isdigit() for ch in title):
problems.append(f"title '{title}' names no period; say what is measured and over which dates")
print(f"{name}: {'OK' if not problems else 'FLAGS'}")
for p in problems:
print(" -", p)
Output:
honest.png: OK
misleading.png: FLAGS
- bar chart y-axis starts at 5.1, not 0: drawn ratio 5.25x vs true ratio 1.13x
- title 'Revenue is surging' names no period; say what is measured and over which dates
Be clear about what that proves. It caught a truncated bar axis and a title with no period. It cannot tell you that "surging" is false, that an undisclosed filter removed refunds, or that you used a pie chart for fourteen categories. The title check is crude: a title with any digit passes. Treat the audit as a smoke test that fails loudly on the cheapest mistakes. Extend it for your house rules, such as a maximum number of series or a required source note.
What belongs on a review checklist for AI-made charts?
Use this before any chart is pasted into a deck or sent to a client. Items 1 to 4 can be automated as above. The rest need a person.
- Axis starts at zero for bars. Lines can crop the axis, bars cannot, because bar length encodes the value.
- Units and scale on both axes. "INR millions" beats "Revenue".
- Title says what, when and which subset. Not a conclusion. Put the conclusion in the sentence beside the chart, where you can defend it.
- Plotted numbers saved next to the image. Open
chart_data.csvand check two values by hand against the source. - Row counts reconcile. Raw rows, rows after cleaning, rows plotted. A script that silently drops rows with bad dates is a classic way to lose a month.
- Filters and exclusions stated. Refunds, test orders, one region only. If the model chose them, you need to know.
- Chart type fits the question. Comparison across categories, bars. Change over time, lines. Part of a whole with few parts, a single stacked bar or a table. Avoid dual y-axes, which let you imply any correlation you like.
- Colour is not the only carrier of meaning. Add direct labels or different markers if the chart might be printed in black and white or seen by colour-blind readers.
- One message per chart. If you need a paragraph to explain it, split it.
- Rerun test. Run the script on a clean folder and compare the data CSV, and the PNG hash where you can.
For wording that helps a model reason about the data before it charts it, see prompting for data analysis. If the numbers come from a database, build a text-to-SQL analyst agent shows how a model can fetch them, and the safe-query guardrail in AI SQL assistant schema prompts keeps that step read-only.
A second prompt: let the model review the chart script
After the script runs, ask for a critique instead of a rewrite. This is an untested template:
Here is a matplotlib script and the printed numbers it produced.
Do not rewrite it. List, in order of risk, anything that could make the chart misleading or irreproducible:
axis ranges, missing units, undisclosed filters, hidden randomness, silent row drops, chart-type mismatch.
For each, quote the line of code and say what a reader would wrongly conclude.
A model is decent at spotting ax.set_ylim(lo, ...) in code. It is poor at knowing that your finance team excludes refunds from "revenue", so keep that decision with a person.
What I did not test
No live model wrote the script. Both the prompt templates and the checklist are untested as a model workflow, although the script, the audit and the hash comparison were run. I only checked bar charts; line, scatter and dual-axis charts need their own audit rules. The reproducibility claim holds for one machine, one set of fonts and matplotlib 3.11.2. And the data is synthetic and flat by construction, which is why "surging" was so easy to disprove. Your data will be less obliging.



