🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

VBA Chart in Excel — Create a Chart in Code and Why It Plots the Wrong Data

|

VBA Chart in Excel — Create a Chart in Code and Why It Plots the Wrong Data

TL;DR — A chart does not store data — it stores a reference to a Range. Each series' .Values is a formula like =Sheet1!$B$2:$B$13, so the chart is only ever as correct as that reference. Create an embedded chart with ws.ChartObjects.Add(left, top, width, height), get its .Chart, set .ChartType, and bind data in one shot with .SetSourceData Source:=Range(...). The two things that bite: embedded charts live in ChartObjects (not Charts, which are chart sheets), and re-running the macro stacks duplicate charts unless you delete the old ones first.

Dim co As ChartObject, ch As Chart

Set co = Worksheets("Report").ChartObjects.Add( _
    Left:=300, Top:=20, Width:=420, Height:=260)
Set ch = co.Chart                                   ' the ChartObject is the frame; .Chart is the chart
ch.ChartType = xlColumnClustered
ch.SetSourceData Source:=Worksheets("Data").Range("A1:B13")   ' bind the whole range at once
ch.HasTitle = True
ch.ChartTitle.Text = "Monthly Sales"

Charting in VBA looks easy until the second run, when you find two charts, or the first run, when the bars plot the wrong column. Both come from the same misunderstanding, and the fix for both is the same idea: a chart does not contain your data — it points at it. Every series holds a reference to a range, written as a formula. Move the data, insert a row, rebuild the source in a different place, and the chart keeps pointing where it was told, faithfully drawing the wrong cells. Once you hold "a chart is a view bound to a Range," the API stops being fiddly and the two classic bugs become obvious.

What you'll learn

  • The mental model — a chart holds a reference to a Range, not a copy of the data
  • The two containers: embedded ChartObjects versus chart sheets in Charts
  • Creating a chart and binding data in one shot with SetSourceData
  • Adding and controlling series by hand with SeriesCollection
  • The duplicate-chart trap on re-run, and how to clear old charts
  • Binding the source to a Table so the chart tracks the data

The mental model: a chart points at a Range

When you chart a range, Excel does not copy the numbers into the chart. It stores, for each series, a formula that references the range — the same way a cell formula references other cells. You can read it back:

Debug.Print ch.SeriesCollection(1).Formula
' =SERIES("Sales",Data!$A$2:$A$13,Data!$B$2:$B$13,1)

That SERIES(...) formula is the whole truth about the chart: a name, an X-values reference, a Y-values reference. The chart is a live view over those references. This is why "my chart shows the wrong data" is so common — the chart is not wrong, its reference is pointing at cells that no longer hold what you think. And it is why the durable fix, at the end of this guide, is to make the reference track the data instead of freezing at $B$2:$B$13.

The two containers: ChartObjects versus chart sheets

Excel has two homes for charts, and confusing them is the first thing that trips a chart macro. An embedded chart floats on a worksheet and lives in that sheet's ChartObjects collection; a chart sheet is a whole tab that is nothing but a chart, and lives in the workbook's Charts collection:

' Embedded: a ChartObject is the frame/container; its .Chart is the chart itself
Dim co As ChartObject
Set co = Worksheets("Report").ChartObjects.Add(300, 20, 420, 260)
co.Chart.ChartType = xlLine        ' set properties on .Chart, not on co

' Chart sheet: a tab of its own
Dim ch As Chart
Set ch = ThisWorkbook.Charts.Add

The subtlety that costs people an hour: with an embedded chart, ChartObjects.Add gives you the frame (a ChartObject), and the chart you actually format is co.Chart. Reaching for Charts(1) when your chart is embedded raises an error, because Charts only holds chart sheets. Most reporting lives on a worksheet, so you will use ChartObjects far more often — just remember the two-layer frame/chart split.

Creating a chart and binding data in one shot

The clean pattern is: add the frame, grab .Chart, set the type, and bind the whole source with SetSourceData in a single call:

Dim co As ChartObject, ch As Chart
Set co = Worksheets("Report").ChartObjects.Add(300, 20, 420, 260)
Set ch = co.Chart
ch.ChartType = xlColumnClustered
ch.SetSourceData Source:=Worksheets("Data").Range("A1:B13")

SetSourceData hands Excel the whole block — headers, categories, and values — and lets it work out the series, exactly like selecting the range and pressing the chart button. This is almost always what you want; it is fewer lines and fewer mistakes than building series one at a time. Include the header row and the category column in the range and the chart labels itself. Set .ChartType before or after — common values are xlColumnClustered, xlLine, xlPie, and xlXYScatter.

Adding and controlling series by hand

When you need precise control — one series at a time, values from a non-contiguous place, a custom name — add series through SeriesCollection:

Dim s As Series
Set s = ch.SeriesCollection.NewSeries
s.Name = "2026"
s.XValues = Worksheets("Data").Range("A2:A13")
s.Values = Worksheets("Data").Range("B2:B13")

Each series has three parts that map straight onto that SERIES(...) formula: .Name, .XValues (the category labels), and .Values (the numbers). Add a second series with another NewSeries; remove one with ch.SeriesCollection(2).Delete. Setting .Values to a range is the same as writing the reference into the series formula — which is the mechanism behind the wrong-data bug, and the reason binding to a stable source matters.

The duplicate-chart trap, and clearing old charts

Here is the bug that produces twelve charts after twelve runs: ChartObjects.Add always adds a new one. It never reuses the chart from last time, so an unattended macro quietly stacks copies. Clear the sheet's existing charts before you build:

Dim co As ChartObject
For Each co In Worksheets("Report").ChartObjects
    co.Delete                          ' remove last run's charts first
Next co
' ...now build the fresh chart

Loop ChartObjects and Delete each, then create your chart from a clean slate — the same collection discipline you would use for any repeated build. If you would rather keep and update one specific chart, name it (co.Name = "chtSales"), look it up by that name next run, and call SetSourceData again instead of adding a new frame. Either way, decide deliberately: build fresh or update in place, never blindly add.

Binding the source to a Table so the chart tracks the data

Now the durable fix for the wrong-data bug. Because a series is a reference, a chart bound to A1:B13 freezes at thirteen rows — add a month and the new point is outside the chart. Bind the source to a Table instead, and the reference grows with the data:

' The chart's source is a Table's range -> new rows appear automatically
ch.SetSourceData Source:=Worksheets("Data").ListObjects("tblSales").Range

When the source is a ListObject, adding a row extends the Table, the Table extends the range the series references, and the chart redraws with the new point — no code, no re-SetSourceData. This is the same move that keeps a pivot current, and it is why a Table is the foundation worth building first: point your pivots and charts at it once and both self-update. To hand a chart off as an image, co.Chart.Export "C:\Reports\sales.png" writes a PNG straight to disk.

How ExcelMaster helps

The chart mistakes that reach a finished report are quiet: the second chart nobody deleted, the series still pointing at last quarter's range, the bars plotting the wrong column because SetSourceData got a block that had shifted. Each one renders cleanly — it just shows the wrong picture.

ExcelMaster builds charts like someone who has been burned by all three. Ask it to "chart monthly sales by region," and it clears the old ChartObjects before it draws, binds the source to a Table so new rows appear on their own, uses SetSourceData for the whole block instead of hand-wiring series, and formats through co.Chart rather than tripping on the frame/chart split. You describe the picture you want; it wires the reference so the chart keeps telling the truth as the data grows.

Frequently asked questions

How do I create a chart in VBA?

Add an embedded chart with ChartObjects.Add, then set its type and data through .Chart: Set co = ws.ChartObjects.Add(300, 20, 420, 260), Set ch = co.Chart, ch.ChartType = xlColumnClustered, ch.SetSourceData Source:=Range("A1:B13"). The ChartObject is the frame and its .Chart is the chart you format. SetSourceData binds the whole range in one call.

Why does my VBA chart plot the wrong data?

Because a chart holds a reference to a range, not a copy of it — each series' .Values is a formula like =Sheet1!$B$2:$B$13. If the data moves, rows are inserted, or the source is rebuilt elsewhere, the reference still points at the old cells and the chart draws them. Re-bind with SetSourceData, or point the source at a Table so the reference tracks the data.

What is the difference between ChartObjects and Charts in VBA?

ChartObjects is a worksheet's collection of embedded charts that float on the sheet; each ChartObject is a frame whose .Chart is the actual chart. Charts is the workbook's collection of chart sheets — tabs that are nothing but a chart. Reaching for Charts(1) when your chart is embedded raises an error; use ws.ChartObjects(1).Chart instead.

How do I add a series to a chart in VBA?

Use SeriesCollection.NewSeries, then set its parts: Set s = ch.SeriesCollection.NewSeries, s.Name = "2026", s.XValues = Range("A2:A13"), s.Values = Range("B2:B13"). Add more with another NewSeries; remove one with ch.SeriesCollection(2).Delete. For a simple chart, SetSourceData on the whole range is easier than adding series by hand.

How do I stop VBA from creating duplicate charts?

ChartObjects.Add always adds a new chart, so re-running a macro stacks copies. Before building, delete the existing ones: For Each co In ws.ChartObjects: co.Delete: Next co. If you want to keep and refresh a specific chart, name it (co.Name = "chtSales"), find it by that name on the next run, and call SetSourceData again instead of adding a new frame.

Tested in

Tested in: Excel 365 (Windows 11), VBA 7.1 — last verified 2026-09-02.

Related guides: VBA Pivot Table · VBA Table · VBA Range · VBA Collection · VBA Worksheet