I am simulating a click on an Excel Slicer using VBA but have run into serious performance problems.
The user clicks on a column graph with dates on the X-axis. When clicking on a column, the corresponding date is selected in a slicer containing the list of dates. The list will continue to grow with time.
The only way (to my knowledge) to set slicer selection for non-OLAP data sources (my case) is to set selected = true individually for each slicer item. As a recalculation is triggered on each setting this is very slow for slicers with many items.
Small code example showing the problem:
On Error GoTo Err_Handler:
Dim SC As SlicerCache
Set SC = ActiveWorkbook.SlicerCaches("Slicer_DATE")
Dim SI As SlicerItem
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
For Each SI In SC.SlicerItems
SI.Selected = True
Next
Err_Handler:
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Similar questions have been asked before:
Selecting multiple slicer items at once without a refresh
Pivot Slicer Update To Slow, Can I pause all functions until slicer update is complete?
There the suggestion is either:
Application.EnableEvents = false
or
Application.Calculation = xlCalculationManual
UPDATE: I also notice that despite turning off events and calculation, all pivot tables are in fact recalculating!
For me, neither of these options work and do not improve the performance. Calculation is indeed postponed and no events are triggered. Still, each iteration of the selected=true takes around 1.5 seconds. In total the operation takes around 5 minutes to complete.
My slicer is connected to 23 pivot tables (!) in multiple sheets. The underlying data (MS Access DB connection) is around 60,000 rows with ~20 variables which is not that much.
Any help is appreciated.
PivotTables have a ManualUpdate property which can be set to True. Setting this property for all your pivots might help speed up your code.
Please try adding this right above where your update slicer code is:
Dim PT As PivotTable
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
For Each ws In wb.Sheets
For Each PT In ws.PivotTables
PT.ManualUpdate = True
Next PT
Next ws
And then add this after you've updated the slicer:
For Each ws In wb.Sheets
For Each PT In ws.PivotTables
PT.ManualUpdate = False
Next PT
Next ws
For more information:
Speed up pivot table filtering VBA code
Turn Off PT Calc
MSDN: ManulaUpdate
Hope that helps!
What you need to do is Duplicate the Slicer and
Duplicate the field as a slicer AND a report filter
see http://www.powerpivotpro.com/2010/12/another-way-to-get-and-use-slicer-values-in-formulas/
Then use the CurrentPage property to select the item:
Private Sub SelectPivotItem(FieldName As String, Itemname As String)
Dim PT As PivotTable, PTF As PivotField, PTI As PivotItem
Set PT = shtInt.PivotTables("PivotTable1")
Set PTF = PT.PivotFields(FieldName)
PTF.ClearAllFilters
PTF.CurrentPage = Itemname
End Sub
This helps massively with performance. The same approach applies to selecting multiple PivotItems in VBA.
Set the pivot tables to ManualUpdate.
Hide all the sheets that contain pivot tables affected by the Slicer while you're doing the multiple slicer selections.
Don't forget to set the pivot tables to not ManualUpdate when done, and set the sheets visible afterwards if you need to
It's very strange because even with Application.ScreenUpdating = False this still helps enormously.
I got my PivotItems selection macro down from 3 minutes to 3 seconds by doing this. Quite a stark difference.
Unfortunately for Slicers this is still slow, around 30s. I'm looking to switch back to doing this with VBA and PivotItems, and avoiding the use of Slicers completely, unless I can find another way to improve performance.
Related
I have an Excel file that comprises a sheet "DATA", which contains data extracted using a Power Query, and three other sheets containing pivot tables that are all dependent on these extracted data in "DATA".
I wantto create a VBA that (1) automatically refreshes/reruns the Power Query specified above when the Excel file is opened and (2) subsequently updates the three pivot tables to mirror the updated data.
I have made several attempts and looked for similar questions on stackoverflow, but without succeeding:
While it was possible for me to refresh the data by refreshing the connection, the Pivot tables were not updated accordingly.
One VBA code that, among others, did not yield the desired results (i.e. subsequently updating the Pivot table) is:
Sub test()
Dim ws As Worksheet
Dim pt As PivotTable
'ActiveWorkbook.RefreshAll 'make sure the refresh in bg property is false for all connections
ActiveWorkbook.Connections("Name of Query").Refresh
For Each ws In ActiveWorkbook.Worksheets
For Each pt In ws.PivotTables
pt.RefreshTable
Next pt
Next ws
End Sub
Thank you!
You must be sure that "background refresh" is set to false:
Sub Refresh_All_Data_Connections()
Dim objConnection, bBackground
For Each objConnection In ThisWorkbook.Connections
'Get current background-refresh value
bBackground = objConnection.OLEDBConnection.BackgroundQuery
'Temporarily disable background-refresh
objConnection.OLEDBConnection.BackgroundQuery = False
'Refresh this connection
objConnection.refresh
'Set background-refresh value back to original value
objConnection.OLEDBConnection.BackgroundQuery = bBackground
Next
ThisWorkbook.RefreshAll
End Sub
Update: added refreshall to refresh all pivot tables.
Rereading your question if you just want to refresh a specific query (assuming sh is set):
sh.ListObjects("Name of Table").QueryTable.refresh BackgroundQuery:=False
ThisWorkbook.RefreshAll
I'm trying to auto-filter my pivot table based on the most recent date in my data and I came across the following code. However, when I try to execute it, I get a
run-time error (1004 - Unable to set the Visible property of the PivotItem class)
on the line: pfiPivFldItem.Visible = False Can someone help me with this?
Dim pfiPivFldItem As PivotItem
Dim dtmDate As Date
With Worksheets("Sheet2").PivotTables("Campaigns")
.PivotCache.Refresh
.ClearAllFilters
With .RowRange
dtmDate = Evaluate("MAX(IF(ISNUMBER(" & .Address(0, 0) & ")," & .Address(0, 0) & ",))")
End With
For Each pfiPivFldItem In .PivotFields("Date").PivotItems
If pfiPivFldItem.Value = "(blank)" Then
pfiPivFldItem.Visible = False
Else
pfiPivFldItem.Visible = (CDate(pfiPivFldItem.Value) = CLng(dtmDate))
End If
Next pfiPivFldItem
This is a common error I see when folk are trying to iterate over PivotItems and change visibility. It happens because you must leave one item visible at all times. The workaround is to set the first item to visible, do the loop, and then check whether the first item should be visible or not. Of course, this won't help you if the thing you're looking for isn't in the PivotTable, which might be the case here. It may well be that (CDate(pfiPivFldItem.Value) will never equal CLng(dtmDate)) due to the issue/bug I outlined at http://dailydoseofexcel.com/archives/2013/11/09/a-date-with-pivotitems/
There's some other things you want to do when filtering PivotTables, such as switch the PivotTable .ManualUpdate property to TRUE while you loop the PivotTable, or otherwise the PT will try to update after each and every change.
Suggest you check out my answer at Filter pivot table 1004 error as the code comments show how and why to do some of this stuff. Also suggest you check out my blogpost at http://dailydoseofexcel.com/archives/2013/11/14/filtering-pivots-based-on-external-ranges/ that takes an indepth look at bottlenecks and workarounds when iterating over PivotItems.
Note that the quickest way to filter a field on one item is to make it a PageField, and set the .PageField value to the PivotItem concerned. That's near instantaneous, and makes a massive difference if you have thousands of items in the PivotField that you're iterating over.
Very novice user here, just getting my feet wet. Please excuse this if it's an idiotic question - I'm quite sure what I'm doing yet.
I'm creating a userform with VBA and Excel. I've been successful in learning how to pull information from a pivot table so far, but I've run into a hitch.
When run my userform from another sheet it can't find the pivot table. Here's the code I'm using.
Set PT = ActiveSheet.PivotTables(1)
Works perfectly when I'm one that sheet, but I envision a scenario where I have multiple pivots on different sheets and will want to call info and cross-reference the data.
I thought maybe Set PT = ActiveWorkbook.PivotTables(1) would work, but of course it doesn't. Obviously I don't yet quite understand how to use a pivot table variable.
Does anyone know how I might go about doing this? Thanks so much.
If you open UserForm, workbook with UserForm open will be the active one. If I understand correctly, you want to refer to 1st object of PivotTables collection on a different sheet. To do so, define path of the file with pivot table, example below:
Dim path as string
Dim wbk as workbook
Dim PT As PivotTable
path= "C:\folder1\folder2\yourfile.extension"
set wbk = workbooks.open(path)
Set PT = wbk.worksheets("Sheet1").PivotTables(1)
'do your actions, save the file if you want to keep changes
wbk.close
set wbk = nothing
Alternatively work on all Pivot Tables in your worksheet, instead of:
Set PT = wbk.worksheets("Sheet1").PivotTables(1)
You can use a loop through collection, this example refreshes tables:
For Each PT In wbk.worksheets("Sheet1").PivotTables
PT.RefreshTable
Next PT
I have two tables, one with a list of cities( we'll call this City List), and another with data points that correspond with those cities ( Call this The Data Table). The Data Table, is connected to a Select query that I built in MS SQL Server. This Select query/ Data Table has a single Where clause in which I have substituted the SQL criteria and replaced a ? in order to make it a parameter when connected to Excel.
Now that I have that out of the way, I'll explain what I'm trying to accomplish. I want to loop through the City List and for each city in the list, update The Data Table to reflect the data points for the city. Ultimately, I would like to loop through and each time The Data Table is refreshed, it saves a copy of the workbook with that specific table.
I have posted my current code down below, but my issue is that the table never refreshes with the current data that corresponds with the city that is currently selected via the loop. With that being said, if I hit the escape key to break out of the VBA macro, the table will then refresh with whatever the last city was before I stopped the macro.
Code:
Sub Macro1()
Dim WS As Worksheet
Dim WS2 As Worksheet
Dim CT As Variant
Dim MSG As String
Set WS = Sheets("Sheet1")
Set WS2 = Sheets("Sheet2")
CT = Range("A1").CurrentRegion
For i = 2 To UBound(CT, 1)
MSG = ""
For J = 1 To UBound(CT, 2)
WS.Range("$D$2").Value = CT(i, J) //Places the city into Cell $D$2 which is where The Data Table looks to for the parameter.
Exit For
Next J
ActiveWorkbook.Connections("Query from Database").Refresh
WS2.ListObjects(1).Refresh
Next i
End Sub
It's almost as though the macro is running too fast for the table to catch up and refresh. I've tried adding some wait times into the code, in hopes that it would give it enough time to allow the table to refresh, but that had no affect. I have also turned off Background Refresh, and that doesn't seem to do anything either. Right now it just loops through the city table, and with each city it shows that the query is refreshing, but after the query is finished refreshing, it goes onto the next city without ever updating The Data Table. HELP!
There are a couple of things I think you need to do -- maybe you've already done them.
When you set up your parameter/bind variable (which you have done), point it to a specific cell. Then, within your SQL Server query, make sure the parameter is bound to that range every time:
Forgive me if I'm overstating the obvious, but for those that don't know you get to this dialog by right-clicking the table and selecting Table->Parameters.
From there, as you iterate through your main table (the one with the cities in it), you can just take the value from each row in that table and update the cell with the binding parameter.
Something like this should work:
Sub RefreshAllCities()
Dim ws1, ws2 As Worksheet
Dim loCities, loDataTable As ListObject
Dim lr As ListRow
Set ws1 = ActiveWorkbook.Worksheets("Sheet1")
Set ws2 = ActiveWorkbook.Worksheets("Sheet2")
Set loCities = ws1.ListObjects("CityList")
Set loDataTable = ws2.ListObjects("DataTable")
' get rid of those pesky warnings to overwrite files
Application.DisplayAlerts = False
For Each lr In loCities.ListRows
ws2.Cells(1, 2).Value = lr.Range(1, 1).Value
loDataTable.QueryTable.Refresh BackgroundQuery:=False
ActiveWorkbook.SaveAs Filename:="C:\temp\" & lr.Range(1, 1).Value & ".xlsx", _
FileFormat:= xlOpenXMLWorkbook
Next lr
Application.DisplayAlerts = True
End Sub
I assume you wanted .xlsx files in this example. This will clobber any embedded VBA, which is actually a nice bonus, as the recipients of the filtered datasets won't have to be exposed to that. If you want xlsm or xlsb, that's easy enough to change.
By default Excel will "Enable Background Refresh" which will allow Excel to move on and continue execution before the query refresh is actually finished. Some people have been able to get it to work by calling .Refresh twice but it's pretty much random/arbitrary timing.
There should be an Excel option to uncheck in the Data Tables properties or you might be able to update the BackgroundQuery = False property from VBA through a reference to it
If you disable background refreshing then your code will sit and wait for the refresh to complete before moving on.
I have lots of data that is pulled by a press of a button from SQL query through connections in Excel. Then I compose few simple calculations to get results. I also have 4 graphs that are based off that data.
I run into an issue where the code will take few minutes to execute. I believe it is due to the fact that while data is being updated, graphs are updated as well. I ran into that conclusion after removing graphs, it was significantly faster.
Is there a way to speed up this process a bit? Can I pause the graphing and resume it after all the data have been updated?
Thank you!
Have you considered offsetting the chart area in vba and switching back at the end of the code?
Here's how you can Select the Chart Area in VBA.
For example. If you want to chart data in Range A1:A10 then you can do the following
Charts(1).SetSourceData Source:=Sheets(1).Range("B1:B10")
your logic
Charts(1).SetSourceData Source:=Sheets(1).Range("A1:A10")
This "Aims" the chart at a different range so that it doesn't try to recompute the graph after each cell change. Once your logic is complete, then "Aim" it back at the correct range.
I suggest using
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
And perhaps also
Application.EnableEvents = False
Before the query,
And reversing it after the query
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
The way I did it was to clear all series in all the graphs at the beginning. Formatting stays the same on each graph.I created a sub that I call in the main program from where I am sending each chart variable name declared as ListObject and assigned a chart name.
Private Sub DeleteOldData(CurrentChart As ChartObject)
Dim s As Long
With CurrentChart
For s = .Chart.SeriesCollection.Count To 1 Step -1
.Chart.SeriesCollection(s).Delete
Next s
End With
End Sub
Then, I update the data. After, I re-assign all the series back the graph through another sub.
Private Sub AddData(CurrentChart As ChartObject, table1 As ListObject, table2 As ListObject, series1 As String, series2 As String)
CurrentChart.Chart.SeriesCollection.NewSeries
CurrentChart.Chart.FullSeriesCollection(1).Name = series1
CurrentChart.Chart.FullSeriesCollection(1).Values = table1.ListColumns(2).DataBodyRange
CurrentChart.Chart.FullSeriesCollection(1).XValues = table1.ListColumns(1).DataBodyRange
CurrentChart.Chart.SeriesCollection.NewSeries
CurrentChart.Chart.FullSeriesCollection(2).Name = series2
CurrentChart.Chart.FullSeriesCollection(2).Values = table2.ListColumns(2).DataBodyRange
CurrentChart.Chart.FullSeriesCollection(2).XValues = table2.ListColumns(1).DataBodyRange
CurrentChart.Chart.FullSeriesCollection(2).Select
DoEvents
End Sub
table1 and table2 are the tables from where I am pulling the data. I had to add 2 graphs on 1 chart from 2 different tables. Series1 and Series2 are the names for the series. I declared them before calling each sub.