Dynamic Chart using Pivot Table and VBA
by Jon Peltier
Peltier Technical Services, Inc., Copyright © 2009.
Licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.
In Using Pivot Table Data for a Chart with a Dual Category Axis, I showed how to make a Chart with a Dual Category Axis using a pivot table to properly arrange the source data. I generally prefer using a regular chart, because pivot charts are pretty inflexible when it comes to formatting. Unfortunately, a regular chart doesn’t know when the pivot table has been resized by a refresh. In Dynamic Chart using Pivot Table and Range Names I showed how to use dynamic ranges to allow a regular chart to update when the pivot table changes. However, that technique only accommodates a change in the number of points in a series, not the number of series in the chart. To allow for changing height and width of a pivot table, you either need to revert to a pivot chart, or use a VBA procedure, which I describe below.
This example begins with the same data range, pivot table, and chart as the previous ones.
| A | B | C | D | |
| 1 | Main Category | Category | Sub Category | Defects |
| 2 | Mechanical | Mechanical | Gear | 11 |
| 3 | Mechanical | Mechanical | Bearing | 8 |
| 4 | Mechanical | Mechanical | Motor | 3 |
| 5 | Electrical | Electrical | Switch | 19 |
| 6 | Electrical | Electrical | Plug | 12 |
| 7 | Electrical | Electrical | Cord | 11 |
| 8 | Electrical | Electrical | Fuse | 3 |
| 9 | Electrical | Electrical | Bulb | 2 |
| 10 | Hydraulic | Hydraulic | Pump | 4 |
| 11 | Hydraulic | Hydraulic | Leak | 3 |
| 12 | Hydraulic | Hydraulic | Seals | 1 |


I’ve highlighted some sections of the pivot table above to correlate them with pivot table ranges in the VBA object model.
| Color | Range |
| Blue | DataBodyRange |
| Green | ColumnRange |
| Purple | RowRange |
| Yellow | DataLabelRange |
The VBA procedure is not too complicated. It should be run whenever the pivot table has been refreshed. It can be called from a button, or from a Worksheet_Calculate event procedure. Whatever works best in a particular application. Paste this code into a regular code module:
Sub UpdateChartFromPivot()
Dim rCategories As Range
Dim rValues As Range
Dim rSeriesNames As Range
Dim pt As PivotTable
Dim cht As Chart
Dim iSeries As Long
Dim nSeries As Long
' Define the Pivot Table
'Set pt = ActiveSheet.PivotTables(1)
Set pt = ActiveSheet.PivotTables("PT_ChartSource")
' Define the Ranges
Set rValues = pt.DataBodyRange
With pt.RowRange
Set rCategories = .Offset(1).Resize(.Rows.Count - 1)
End With
Set rSeriesNames = pt.ColumnRange.Rows(2)
' Define the Chart
'Set cht = ActiveSheet.ChartObjects(1).Chart
Set cht = ActiveSheet.ChartObjects("chtPivotData").Chart
' How Many Series?
nSeries = rSeriesNames.Columns.Count
' Remove or Add Series until Chart Has Correct Number
Select Case cht.SeriesCollection.Count - nSeries
Case Is > 0
' too many: remove excess series
For iSeries = nSeries To cht.SeriesCollection.Count + 1
cht.SeriesCollection(iSeries).Delete
Next
Case Is < 0
' too few: add sufficient series
For iSeries = cht.SeriesCollection.Count + 1 To nSeries
cht.SeriesCollection.NewSeries
Next
Case Else
' just right
End Select
' Populate Each Series
For iSeries = 1 To nSeries
With cht.SeriesCollection(iSeries)
.Name = rSeriesNames.Columns(iSeries)
.Values = rValues.Columns(iSeries)
.XValues = rCategories
.Border.LineStyle = xlNone
End With
Next
End Sub
Let’s add some rows to the pivot table source data:
| A | B | C | D | |
| 1 | Main Category | Category | Sub Category | Defects |
| 2 | Mechanical | Mechanical | Gear | 11 |
| 3 | Mechanical | Mechanical | Bearing | 8 |
| 4 | Mechanical | Mechanical | Motor | 3 |
| 5 | Electrical | Electrical | Switch | 19 |
| 6 | Electrical | Electrical | Plug | 12 |
| 7 | Electrical | Electrical | Cord | 11 |
| 8 | Electrical | Electrical | Fuse | 3 |
| 9 | Electrical | Electrical | Bulb | 2 |
| 10 | Hydraulic | Hydraulic | Pump | 4 |
| 11 | Hydraulic | Hydraulic | Leak | 3 |
| 12 | Hydraulic | Hydraulic | Seals | 1 |
| 13 | Environmental | Environmental | Emissions | 5 |
| 14 | Environmental | Environmental | Noise | 6 |
| 15 | Environmental | Environmental | Thermal | 2 |
Now update the pivot table:

And finally, run the VBA procedure to update the chart:

The VBA procedure can be run from the Macros dialog (press Alt+F8 or navigate the menu/ribbon), from a button you’ve placed on the sheet (see Assign a Macro to a Button or Shape and Assign a Macro to an ActiveX Control), from a menu item (see Assign a Macro to a Toolbar or Menu), or from a worksheet event procedure.
In Excel 2003 (and maybe 2002) you can use the Worksheet_PivotTableUpdate event procedure. Select the sheet with the pivot table and chart, right click on the sheet tab, and choose View Code. This opens the code module for the active worksheet. From the left dropdown in the module window, choose Worksheet, and from the right dropdown, choose PivotTableUpdate. The shell for the event procedure is entered into the module for you. Insert the procedure name as shown below, and you’re ready to go:
Private Sub Worksheet_PivotTableUpdate(ByVal Target As PivotTable)
UpdateChartFromPivot
End Sub
If you’re using an older version of Excel, there is no PivotTableUpdate event. What I do in this case is place a formula in a cell outside the pivot table (say, M1) with a formula like:
=SUM(A1:J20)
where the range A1:J20 includes the pivot table. This formula ensures that a calculation occurs when the pivot table refreshes. Then I use the Worksheet_Calculate event procedure to kick off the chart update procedure. As above, select the sheet with the pivot table and chart, right click on the sheet tab, and choose View Code. This opens the code module for the active worksheet. From the left dropdown in the module window, choose Worksheet, and from the right dropdown, choose Calculate. The shell for the event procedure is entered into the module for you. Insert the procedure name as shown below, and you’re ready to go:
Private Sub Worksheet_Calculate()
UpdateChartFromPivot
End Sub
Fourth in a series
- Chart with a Dual Category Axis
- Using Pivot Table Data for a Chart with a Dual Category Axis
- Dynamic Chart using Pivot Table and Range Names
- Dynamic Chart using Pivot Table and VBA
Possibly Related Posts:
- Dynamic Chart using Pivot Table and Range Names
- Using Pivot Table Data for a Chart with a Dual Category Axis
- Update Regular Chart when Pivot Table Updates
- Chart with a Dual Category Axis
- Pivot Table Conditional Formatting with VBA
- Extract Chart Data
- VBA to Split Data Range into Multiple Chart Series
- Referencing Pivot Table Ranges in VBA
- VBA Conditional Formatting of Charts by Series Name
- How To: Assign a Macro to an ActiveX Control
Posted: Friday, April 25th, 2008 under Dynamic Charts.
Comments: 30
Comments
Comment from Peder Schmedling
Time: Saturday, April 26, 2008, 9:42 am
Hey!
Way to go, very nice solution.
I love this blog, keep up the good work.
Comment from Jon Peltier
Time: Saturday, April 26, 2008, 12:29 pm
Peder -
I told you it was on the way! Actually I was writing it as your previous comment came in. The nice thing about the blog is I can write about smaller parts of a problem, and post a bit at a time, like chapters in a serial novel. With the regular web site, I felt I had to write the whole novel at once, so I often just never got around to it.
Comment from Subhash Tiwari
Time: Friday, June 6, 2008, 11:29 am
Hi Team,
It’s nice to see the effort you people doing to help the other person as well a new comer.
I have a query…I’m working on a data range where pivot item value need to change dynamically. Once I record the Marco. It’s given a static value..
Code is: –
With ActiveSheet.PivotTables(”PivotTable2″).PivotFields(”CLOSED_DTTM”)
.PivotItems(”6/4/2008″).Visible = False
End With
Here “6/4/2008″ is a current date & every day this value gets changes.
Please help me how can I change it dynamically in the VBA code
Comment from Jon Peltier
Time: Friday, June 6, 2008, 12:12 pm
Subhash -
This might be as easy as using today’s data in the proper format:
.PivotItems(Format(Now, "m/d/yyyy")).Visible = False
Comment from Subhash Tiwari
Time: Monday, June 9, 2008, 9:20 am
Hi Jon ,
Thank for your advice. However it’s not working :-(
Even I tried Today & Date function as well…but in all cases curser stop at this line of codes & every time I need to put the values manually :-(
Comment from Jon Peltier
Time: Monday, June 9, 2008, 9:41 am
1. Note that the straight quotes in my line of code have been converted by WordPress into curly quotes.
2. Make sure that Format(Now, “m/d/yyyy”) exactly matches the pivot item label you need to hide. In the Immediate Window, type this and press Enter:
?Format(Now, “m/d/yyyy”)
Comment from Subhash Tiwari
Time: Monday, June 9, 2008, 11:52 am
Thanks a lot Jon :-)..now code is working fine
Comment from Felix Marusic
Time: Friday, January 16, 2009, 10:25 am
John,
I’m trying apply a dynamic chart title which will should change when the data is either filtered or sorted, but have been unable to do so using a macro or via the interface. Not sure of the VBA syntax needed to pull this off.
Comment from Jon Peltier
Time: Friday, January 16, 2009, 10:44 am
Have you tried the Worksheet_Calculate and Worksheet_PivotTableUpdate event procedures? These are on the worksheet’s code module. Easiest way to get there is to right click on the worksheet tab and select View Code.
Comment from Felix Marusic
Time: Friday, January 16, 2009, 5:30 pm
Jon,
Thanks I’ll look into it.
Felix
Comment from Doug Glancy
Time: Tuesday, March 3, 2009, 8:10 pm
Jon, I’m doing something similar with an addin that updates a workbook on its Worksheet_PivotTableUpdate event. It works great except that some, to all, of my charts (pie charts!) keep reverting to Pivot Charts. It seems like this didn’t happen when my code was in the workbook itself instead of in an addin. Also, the workbooks are actually created from templates if that makes any difference, and the pivot tables are based on external queries to Access. I can kind of prevent it if I put a Stop in the code and select a sheet, but only kind of, and putting in code to activate sheets doesn’t help. I can’t really see a pattern to which charts revert and which don’t.
I know, from you, that you can accidentally turn a regular chart into a pivot chart by hand, so I assume this is related.
Comment from Doug Glancy
Time: Tuesday, March 3, 2009, 9:54 pm
I got it. Looked at your code in this post some more and that gave me the answer. I was using the SetSourceData method (I think I got that from the macro recorder, but can’t swear to it). When I switched to SeriesCollection.XValues and .Values my charts started behaving. Thanks for your most helpful site!
Comment from Jon Peltier
Time: Tuesday, March 3, 2009, 10:06 pm
Doug -
Glad you got it sorted out. When SetSourceData overlaps with a pivot table, the chart is converted to a pivot chart. Inconvenient, but setting the X and Y values of each series maintains the independence of the chart. I’m working on a replacement chart source data dialog which bypasses SetSourceData in favor of series-by-series definitions.
Comment from Doug Glancy
Time: Tuesday, March 3, 2009, 10:32 pm
Jon, that sounds like a good project. I’ve got a nice pivot chart class that passes back references to items in a field and the like. It’s been a lot of fun to put together. Thanks again.
Comment from NMullis
Time: Tuesday, May 12, 2009, 4:07 pm
Jon, I have a workbook with several pivot charts. The charts are all Line chart types where each of the series lines represents a retailer and each of the 8 retailers may or may not be in the resulting pivot chart. On each chart I need there to be consistency among each of the retailer colors, that is, to see Retailer A always in yellow, Retailer B always in blue, Retailer C always in green, etc. If Retailer A is not represented on a given chart then another retailer should not be yellow.
To add to the complexity, I would like a way to update all pivot charts (12) at one time. Is this possible?
Thank you,
N
Comment from Jon Peltier
Time: Tuesday, May 12, 2009, 4:40 pm
For the consistent formatting, you could run a macro like that in VBA Conditional Formatting of Charts by Series Name after refreshing the pivot tables.
To refresh all of the pivot tables:
Sub RefreshAllPivots()
Dim pt As PivotTable
For Each pt In ActiveSheet.PivotTables
pt.RefreshTable
Next
End Sub
 :
Comment from NMullis
Time: Wednesday, May 13, 2009, 11:05 am
That is awesome, thank you! I had to adjust for having a line chart style and then all worked perfectly.
Thanks again! N
Comment from jeff weir
Time: Sunday, July 19, 2009, 5:36 am
Just a thought inspired by NMullis’ formatting issue: Instead of using VBA, you could use the Getpivotdata function to pull series data outside of the pivottable into a ‘parking space’, and then use dynamic named ranges to reference that parking space, so the size of the chart series array always matches the size of the pivot table columns. That way, the formatting of your different series wouldn’t change.
ALso, if you switced the order of the fields around in the Values field (or Data field as its called in excel 2003) – or take one data field out out entirely – it doesn’t affect your graph…because the data returned to the ‘parking area’ is specified by a cannibalised GETPIVOTDATA function, which doesn’t care which row or column the data is in, as it references the pivot cache, not the pivot table.
Whereas if you were using a dynamic range with the OFFSET function that referenced a particular row where the data originally was, then suddenly you’d have the wrong data in your chart, as the data you wanted when you set up the range would have moved.
By ‘cannabalise’, I mean a stripped-back GetPivotData function like this:
=GETPIVOTDATA(”Sum of Revenues”,Sheet1!$B$2,”Client 1″,A2)
…where all other field row references have been stripped out.
You’d enter this in Cell B2 of the parking area, and copy down. Cell A2 would just point to the Field row names in the pivottable itself.
If the structure of the pivottable was likely to change (eg if you added a field to the ‘Column Labels’/'Column Fields’) then you’d have an issue, as your GETPIVOTDATA formulas would return errors. But you could probably build a contingency around this in to your GETPIVOTDATA formula using an IF statement or two.
Any thoughts?
Comment from jeff weir
Time: Sunday, July 19, 2009, 5:53 am
Just realised I didn’t explain something fully. Where I said:
“If the structure of the pivottable was likely to change (eg if you added a field to the ‘Column Labels’/’Column Fields’) then you’d have an issue, as your GETPIVOTDATA formulas would return errors. But you could probably build a contingency around this in to your GETPIVOTDATA formula using an IF statement or two.”
…I meant say take the case where you want to compare sales in your pivottabel in different years, and you drag a ‘Year’ field to the ‘Column Labels’/’Column Fields’. Suddenly the GETPIVOTDATA formula posted above wouldn’t work, because it doesn’t have a ‘Year’ reference in it, and Excel doesn’t know what year you are interested in.
Comment from Jon Peltier
Time: Sunday, July 19, 2009, 10:16 am
Jeff -
GETPIVOTDATA can certainly be used to update a worksheet range (chart data source) when a pivot table is refreshed. I always seem to get myself messed up when I use GETPIVOTDATA, but that’s probably because I don’t use it much.
The VBA approach here doesn’t need to know the number of defect categories (columns in the pivot data range and series) or defect subcategories (rows in the pivot data range and X axis categories), nor the names of these items. Note how I added the “Environmental” category, and the code needed no adjustment. It’s simple, it just divides the pivot data range into columns as long as the data range, and in fact you could modify it slightly to work on a non-pivot-table range.
GETPIVOTDATA would need to rely on named ranges that reference the pivot table, it would need some formulaic approach to extract the categories to use as its arguments, and you would need to make sure you had enough columns and rows of GETPIVOTDATA formulas to handle any added data (and these would have to be ignored by the chart if they were unfilled).
I neglected to note that I sorted both row area fields (Main Category and Sub Category) in decreasing order of Sum of Defects, so the highest bars in the chart stay to the left. I guess you could write funky formulas to get this result from the GETPIVOTDATA function, but my head doesn’t hurt as much if I use VBA.
If you want the series formats to be tied to a series name, as they can be using GETPIVOTDATA, you can always use VBA Conditional Formatting of Charts by Series Name to accomplish this. This would make sure that Hydraulic stayed blue even when Environmental was added higher in the series plot order in the last chart. This could be called at the end of the chart update procedure presented above.
Comment from Jon Peltier
Time: Sunday, July 19, 2009, 10:40 am
Another nice thing the VBA approach did without modification. If the Sub Category field is removed from the rows area of the pivot table, the chart updates very nicely, with a summary by main category without the second tier of category labels.

Comment from jeff weir
Time: Wednesday, July 22, 2009, 6:18 am
I’ve just had a play with this, and found it VERY cool indeed. I’ve been using GETPIVOTDATA functions and parking spaces as per my earlier comment above. I now see the error of my ways….
Only thing is that my data has individual products in my one and only row field, and months of the year in the column field, with sum of revenue in the data field. So the layout is similar to the picture you posted in your last comment, except that every square in the matrix has data in it. i.e. something like this:
Jan Feb March
Product 1 10 12 12
Product 2 5 6 8
Product 3 15 13 2
Product 4 8 7 6
I’m trying to work out how to amend your code so that I can plot each product against time on seperate line graphs called ‘Product 1 graph’, Product 2 graph’, etc.
I suspect I just need to switch a few things in your code around, and add a loop so it updates every product chart. But I’m struggling….still learning this VBA beast, not to mention proper pt diction.
Any chance you could help steer me in the right direction? Of course, I realise it would be some miracle if you actually have ANY free time between earning a living and answering the zillions of requests you get like this :-)
Meanwhile I’ll keep chipping away to see if I can crack it. ANd crack it I must…this cuts down on SOOOO much complexity in the way I currently do things.
Comment from Jon Peltier
Time: Wednesday, July 22, 2009, 9:28 am
Jeff -
Is it as simple as you want to plot each row as a series, with the months as your category labels, and the product names as series names? I’ve gotten into the habit of always putting my X values in the row fields, with my series in columns. I always have to stop and think when the data is arranged differently.
I have a post waiting to be written which will help describe how to program pivot tables,
Comment from Jeff Weir
Time: Wednesday, July 22, 2009, 6:54 pm
Exactly. I’ve taken your advice, and put X values in the row fields, with series in columns
I’ve had a go at modifying your code so it just plots one product (i.e. I filtered the pivottable to 1 product).
I hardcoded the macro so it just dealt with one series on the chart, and got it to plot the series just fine. Bu I couldn’t work out how to change your macro so that I got dates for x axis names. THen in playing around trying to fix that, I somehow mucked up the macro entirely.
I’m gonna park it for now, and continue with my existing spagetti of getpivotdata formulas as I’ve got to deliver this tomorrow.
Cheers
Comment from NMullis
Time: Tuesday, August 25, 2009, 3:43 pm
Jon, I have one more question. How can I apply this code to all charts in my workbook?
Sub ColorBySeriesName()
Dim rPatterns As Range
Dim iSeries As Long
Dim rSeries As Range
Dim iColorIndex As Long
Dim chtTemp As ChartObject
Set rPatterns = ActiveSheet.Range("BC1:BD8")
For Each chtTemp In ActiveSheet.ChartObjects
With chtTemp.Chart
For iSeries = 1 To .SeriesCollection.Count
Set rSeries = rPatterns.Find(What:=.SeriesCollection(iSeries).Name)
If Not rSeries Is Nothing Then
iColorIndex = rSeries.Interior.ColorIndex
With .SeriesCollection(iSeries)
.Border.ColorIndex = iColorIndex
.Border.Weight = xlMedium
.MarkerForegroundColorIndex = iColorIndex
.MarkerBackgroundColorIndex = iColorIndex
.MarkerStyle = xlTriangle
End With
End If
Next
End With
Next
End Sub
Thank you! Your assistance has been invaluable!
N
Comment from Jon Peltier
Time: Thursday, August 27, 2009, 1:17 pm
Sub ColorBySeriesName()
Dim rPatterns As Range
Dim iSeries As Long
Dim rSeries As Range
Dim iColorIndex As Long
Dim chtTemp As ChartObject
Dim wsTemp As Worksheet
Set rPatterns = ActiveSheet.Range("BC1:BD8")
For Each wsTemp In ActiveWorkbook.Worksheets
For Each chtTemp In wsTemp.ChartObjects
With chtTemp.Chart
For iSeries = 1 To .SeriesCollection.Count
Set rSeries = rPatterns.Find(What:=.SeriesCollection(iSeries).Name)
If Not rSeries Is Nothing Then
iColorIndex = rSeries.Interior.ColorIndex
With .SeriesCollection(iSeries)
.Border.ColorIndex = iColorIndex
.Border.Weight = xlMedium
.MarkerForegroundColorIndex = iColorIndex
.MarkerBackgroundColorIndex = iColorIndex
.MarkerStyle = xlTriangle
End With
End If
Next
End With
Next
Next
End Sub
Comment from Barb Reinhardt
Time: Friday, September 11, 2009, 2:51 pm
Jon,
I’d like to add the range “ChtLabel” as the XVal and “Loc_13″ as the Value and have it display in the series definition. Also, when I use your method, the “name” is not listed in the series formula. It looks something like this
=SERIES(,’Sheet1′!$C$1:$AB$1,’Sheet1′!$C$14:$AB$14,13)
and I want
=SERIES(”MyName”,’Sheet1′!ChtLabel,’Sheet1′!Loc_13,13)
How would I go about doing that?
Thanks a bunch,
Barb
Comment from Jon Peltier
Time: Friday, September 11, 2009, 10:57 pm
Hi Barb -
You can simply select the series and edit the series formula in the formula bar. Alternatively (but less directly), you can open the source data dialog and change the definitions of the series name, X values, and Y values in the dialog.
Excel 2007 had problems at first when the series formula was edited directly, but SP1 or SP2 fixed that.
Comment from Barb Reinhardt
Time: Saturday, September 12, 2009, 6:08 am
I’m not sure you understood the question, but I’ve figured out most of it. I still need to define the name of a chart series programmatically. When I use something like this:
With cht.SeriesCollection(iSeries)
.Name = rSeriesNames.Columns(iSeries)
.Values = rValues.Columns(iSeries)
.XValues = rCategories
End With
There is a name tied to the series in the chart that I want, but it’s not displayed in the series formula, nor is it displayed when I edit the series. It looks like
=SERIES(,Sheet1!B1:AC1,Sheet1!B2:AC2,1) ‘Don’t have the series right in front of me.
I want something like this
=Series(Sheet1!A1,…
Thanks again,
Barb
Comment from Jon Peltier
Time: Saturday, September 12, 2009, 9:58 am
Barb -
Oh, you wanted to do this programmatically. I didn’t understand why you were having that problem.
Try using the address rather than the range object:
.Name = “=” & rSeriesNames.Columns(iSeries).Address( , , xlR1C1, True)
















Write a comment
I welcome comments from my readers. If you have an opinion on this post, if you have a question or if there is anything to add, I want to hear from you. Whether you agree or disagree, please join the discussion.
If you want to include an image in your comment, post it on your own site or on one of the many free image sharing sites, and include a link in your comment. I'll download your image and insert the necessary html to display the image inline.
Read the PTS Blog Comment Policy.