Making Data Charts With LLMs in November 2026: Start From the Question, Check Every Number Against the Data, and Label Axes Honestly
Asking a language model for a chart is one of the fastest ways to go from a CSV file to something you can show a colleague. You describe what you want, the model writes twenty lines of matplotlib or Vega-Lite, and a picture appears. The trouble is that a chart can look polished and still be wrong. The model may aggregate the wrong column, silently drop rows with missing values, sort categories alphabetically instead of by size, or start the y-axis at a number that turns a 2% change into a cliff. Nobody notices, because a clean chart looks trustworthy. This guide shows a workflow that keeps the speed and removes most of those failure modes, using Python, pandas, and matplotlib as the running example.
The short version: decide what question the chart answers before you ask for code, give the model your real column names and a small sample, compute the numbers in a separate step you can check, and review the finished chart against a short checklist before it leaves your machine.
Playfair drew some of the first line charts more than two centuries ago. The basics still apply: a clear question, labeled axes, and numbers you can trace back to a source. Image: Playfair TimeSeries by William Playfair, Public domain, via Wikimedia Commons.
Step 1: Write down the question the chart answers
"Make a chart of our sales data" is not a request a model can get right, because there are dozens of reasonable charts in that file. Before you open the chat, write one sentence that names the comparison you care about. For example:
- "Did weekly signups change after the pricing page update on March 3?"
- "Which five product categories brought in the most revenue last quarter?"
- "How is response time distributed, and how long is the slow tail?"
Each question points to a chart type. A change over time is a line chart. A ranking of a few categories is a sorted horizontal bar chart. A distribution is a histogram or box plot. If you state the question, you can ask the model to choose the chart type and explain why, and you can disagree with its reasoning instead of arguing about colors later.
Step 2: Show the model your real columns, not a description of them
Models guess column names when you do not give them. A guessed df["revenue"] against a file whose column is actually net_amount_usd fails loudly, which is fine. The dangerous case is when a guess happens to match a column that means something else, such as amount that includes refunds. Paste the output of a few quick commands instead:
import pandas as pd
df = pd.read_csv("orders.csv")
print(df.dtypes)
print(df.head(5).to_string())
print(df.isna().sum())
print(len(df))
That gives the model the exact names, the types (so it knows a date column was read as text and needs parsing), how many values are missing, and the row count. Strip or replace anything sensitive, such as customer emails, before pasting. Five rows are enough; the model does not need the whole file, and you should not send it one you are not allowed to share.
Then add the definitions only you know: "Revenue means net_amount_usd where status == 'paid'. Weeks start on Monday. Exclude test accounts, which have is_internal == True." These business rules are where most wrong charts come from, and a model cannot infer them from the data.
Step 3: Split the work into "compute" and "draw"
Ask for two separate pieces of code: one that turns the raw data into a small summary table, and one that draws that table. A prompt like this works well:
Write a function summarize(df) that returns a DataFrame with one row
per week (Monday start) and columns week_start and paid_revenue,
following the rules above. Then write plot_summary(summary) that draws
a line chart from that table only. Do not do any filtering or
aggregation inside plot_summary.
The split matters because the summary table is something you can inspect. You can print it, sort it, and compare it to numbers you already trust. When the aggregation is buried inside plotting code, the only output is a picture, and you cannot easily check a picture.
Step 4: Check every number against the data
Before you look at the chart at all, verify the summary table. A few checks catch most problems:
- Totals match. The sum of the summary column should equal the same total computed directly from the raw data with the same filters. If you have a figure from another source, such as a finance report, compare against it too.
- Row counts are explained. Print how many rows each filter removed. If excluding test accounts dropped 40% of your orders, that is worth knowing before you publish anything.
- Missing values were handled on purpose. pandas skips missing values in sums and means by default. That is often what you want, but ask the model to state how it treated them, and decide whether a week with no data should appear as zero or as a gap.
- Spot-check one row by hand. Pick one week, filter the raw data yourself, and confirm the number.
You can turn the first check into an assertion so it runs every time:
paid = df[(df.status == "paid") & (~df.is_internal)]
summary = summarize(df)
assert abs(summary.paid_revenue.sum() - paid.net_amount_usd.sum()) < 0.01
Step 5: Label axes honestly
Once the numbers are right, the remaining risk is a chart that tells a different story than the data. Review the generated plotting code for these issues, or ask the model to review it against this list:
- Bar charts start at zero. Bar length represents quantity, so a truncated axis exaggerates differences. Line charts can use a tighter range when the change is the point, but say so in the axis label or caption.
- Axis labels include units. "Revenue (USD, thousands)" beats "Revenue". "Latency (ms)" beats "Time".
- The title states the finding or the question, not just the dataset name. "Weekly paid revenue, Jan to Jun" is the minimum; add the date range and any filters a reader needs.
- Categories are sorted by value unless they have a natural order like months or sizes.
- Dates are real dates. If the date column is still text, matplotlib will space the points evenly and sort them as strings, which hides gaps. Parse with
pd.to_datetimefirst. - Colors are readable for people with color vision deficiency. Avoid relying on red against green alone; a colorblind-friendly palette such as matplotlib's default tab10 or viridis is a safe start.
- Dual y-axes are avoided. Two lines on different scales can be made to look correlated by choosing the scales. Two stacked charts with a shared x-axis are usually clearer.
Minard's chart of Napoleon's 1812 campaign packs several variables into one picture, and every one of them is labeled. Image: Minard by Charles Minard, Public domain, via Wikimedia Commons.
Step 6: Make the chart reproducible
A chart you made once in a notebook will be requested again next month. Ask the model to package the code as a script that takes the input file and output path as arguments and saves the figure with a fixed size and resolution, for example fig.savefig(out, dpi=150, bbox_inches="tight"). Keep the summary table as a CSV next to the image. When someone asks "where did this number come from?", you can point at a file instead of trying to remember what you typed into a chat window.
Record the version of the data too: the file name, the date it was exported, and the filters. A one-line caption such as "Source: orders export, 2026-11-09; paid orders only; internal accounts excluded" saves a long conversation later.
A worked example prompt
Putting it together, a complete first message to the model looks like this:
Question: Did weekly paid revenue change after the pricing page
update on 2026-03-03?
Data (output of df.dtypes, df.head(), df.isna().sum(), len(df)):
[paste]
Rules: revenue = net_amount_usd where status == "paid";
exclude is_internal == True; weeks start Monday.
Please:
1. Suggest a chart type and say why.
2. Write summarize(df) returning week_start, paid_revenue.
Print how many rows each filter removes.
3. Write plot_summary(summary, out_path) using matplotlib.
Label units, mark 2026-03-03 with a vertical line and a text label,
and do no aggregation in this function.
4. List any assumptions you made about missing values or dates.
The last item is the most useful one. Models are generally good at listing their assumptions when asked and much less reliable at volunteering them.
Common mistakes to watch for
- Averaging averages. If the data already holds daily averages, a weekly mean of those values is not the weekly average unless every day had the same volume. Ask for weighted means or recompute from raw rows.
- Counting rows instead of entities. "Customers per month" computed with
count()on an orders table counts orders. Usenunique()on the customer ID. - Time zone drift. Timestamps stored in UTC and grouped by day can move late-evening events into the next day for your local market. Convert to the right time zone before grouping.
- Partial periods. The current week or month is incomplete, so it usually looks like a drop. Exclude it or mark it clearly.
- Invented annotations. If you ask the model to "add insights", it may write a caption claiming a trend the data does not support. Write the caption yourself after you have checked the numbers.
When to skip the model
For a one-off look at a single column, df["col"].plot() or your spreadsheet's built-in chart is faster than writing a prompt. The model earns its place when the chart needs several transformation steps, a specific style, or a reusable script. Even then, treat its code like a colleague's first draft: useful, quick, and checked before anyone else sees the result.
The pattern is the same as most good LLM workflows. You bring the question, the real data shape, and the business rules. The model brings speed at writing code. A separate verification step makes sure the picture matches the numbers.
Comments
Post a Comment