11

Using Excel 2010, I have written some VBA to copy selected sheets from a Master workbook to a Client workbook. The code works just fine to copy the data sheet which has data & PivotTable(s) associated with the data, and the chart sheet with one or more PivotCharts to the new workbook.

The issue is that in the destination workbook, the charts are no longer PivotCharts, they're regular Charts and their Source Data range is blank. The Source Data for the Master PivotChart is filled in, but greyed out so it's not editable.

The issue appears immediately upon copying the worksheet from one workbook to the other (on this line: XLMaster.Sheets(SlideRS.Fields(2).Value).Copy After:=XLClinic.Sheets(XLClinic.Sheets.Count)), though I'll include the code for the call Subs. By the time it gets to those lines, the chart is already broken

The question is three part:

  1. Can I prevent the PivotChart being converted into a regular Chart in the copy? i.e. is there a flag/setting for .Copy that I've missed?
  2. If not, can I update the chart to be a PivotChart again by something along the lines of cchart.Chart.PivotLayout.PivotTable = mchart.Chart.PivotLayout.PivotTable?
  3. Failing either of these, what's the best way to create the PivotChart from scratch in the copied worksheet?

NOTES:

  1. These are standard Excel PivotTables, I'm not using PowerPivot. I'm open to that, though, if it will fix the issue. Having just done a little reading on PowerPivot, I don't think it will help me, but, again, I'm open to suggestion.
  2. In response to Jens' comment, the raw data and Pivot Tables are on one sheet, and the Pivot Charts are on a second sheet.

Here's the code. It is working great for everything except copying a sheet with a PivotChart intact as a PivotChart.

  While Not SlideRS.EOF                                                   'loop through all the supporting data sheets for this graph sheet
    If SlideRS.Fields(1) <> SlideRS.Fields(2) Then                        'the worksheet depends on something else, copy it first
      If InStr(1, UsedSlides, SlideRS.Fields(2)) = 0 Then                 'if the depended upon slide is not in the list of UsedSlides, then add it
        Form_Master.ProcessStatus.Value = "Processing: " & ClinicName & "  Slide: " & SlideRS!SlideName & "    Worksheet: " & SlideRS.Fields(2).Value
        XLMaster.Sheets(SlideRS.Fields(2).Value).Copy After:=XLClinic.Sheets(XLClinic.Sheets.Count)
        Set NewSheet = XLClinic.Sheets(XLClinic.Sheets.Count)
        UsedSlides = UsedSlides & "," & NewSheet.Name
        UpdateCharts XLMaster.Sheets(SlideRS.Fields(2).Value), NewSheet
        ProcessDataSheet NewSheet, NewXLName
        Set NewSheet = Nothing
      End If
    End If
    SlideRS.MoveNext                                                      'move to the next record of supporting Data sheets
  Wend

Here is the code for UpdateCharts. Its purpose is to copy the colors from the Master to the Client worksheet, since Excel seems to like assigning random colors to the new charts

Private Sub UpdateCharts(ByRef Master As Worksheet, ByRef Clinic As Worksheet)

Dim MChart As Excel.ChartObject
Dim CChart As Excel.ChartObject
Dim Ser As Excel.Series
Dim pnt As Excel.point
Dim i As Integer
Dim Color() As Long
Dim ColorWheel As ChartColors

  Set ColorWheel = New ChartColors
  For Each MChart In Master.ChartObjects
    For Each CChart In Clinic.ChartObjects
      If CChart.Name = MChart.Name Then
        If CChart.Chart.ChartType = xlPie Or _
           CChart.Chart.ChartType = xl3DPie Or _
           CChart.Chart.ChartType = xl3DPieExploded Or _
           CChart.Chart.ChartType = xlPieExploded Or _
           CChart.Chart.ChartType = xlPieOfPie Then
          If InStr(1, CChart.Name, "ColorWheel") Then                  'this pie chart needs to have pre-defined colors assigned
            i = 1
            For Each pnt In CChart.Chart.SeriesCollection(1).Points
              pnt.Format.Fill.ForeColor.RGB = ColorWheel.GetRGB("Pie" & i)
              i = i + 1
            Next
          Else                                                      'just copy the colors from XLMaster
            'collect the colors for each point in the SINGLE series in the MASTER pie chart
            i = 0
            For Each Ser In MChart.Chart.SeriesCollection
              For Each pnt In Ser.Points
                ReDim Preserve Color(i)
                Color(i) = pnt.Format.Fill.ForeColor.RGB
                i = i + 1
              Next 'point
            Next 'series
            'take that collection of colors and apply them to the CLINIC pie chart points
            i = 0
            For Each Ser In CChart.Chart.SeriesCollection
              For Each pnt In Ser.Points
                pnt.Format.Fill.ForeColor.RGB = Color(i)
                i = i + 1
              Next 'point
            Next 'series
          End If
        Else
          'get the series colors from the MASTER
          i = 0
          For Each Ser In MChart.Chart.SeriesCollection
            ReDim Preserve Color(i)
            Color(i) = Ser.Interior.Color
            i = i + 1
          Next 'series
          'assign them to the CLINIC
          i = 0
          For Each Ser In CChart.Chart.SeriesCollection
            Ser.Interior.Color = Color(i)
            i = i + 1
          Next 'series
        End If 'pie chart
      End If 'clinic chart = master chart
    Next 'clinic chart
  Next 'master chart
  Set ColorWheel = Nothing

End Sub

Here is the ProcessDataSheet() code. This will update the data on the sheet based on one or more SQL queries embedded in the sheet.

Private Sub ProcessDataSheet(ByRef NewSheet As Excel.Worksheet, ByRef NewXLName As String)

Const InstCountRow As Integer = 1
Const InstCountCol As Integer = 2
Const InstDataCol As Integer = 2
Const InstCol As Integer = 3
Const ClinicNameParm As String = "{ClinicName}"
Const LikeClinicName As String = "{LikeClinicName}"
Const StartDateParm As String = "{StartDate}"
Const EndDateParm As String = "{EndDate}"
Const LocIDParm As String = "{ClinicLoc}"

Dim Data As New ADODB.Recordset
Dim InstCount As Integer
Dim SQLString As String
Dim Inst As Integer
Dim pt As Excel.PivotTable
Dim Rng As Excel.Range
Dim Formula As String
Dim SChar As Integer
Dim EChar As Integer
Dim Bracket As Integer
Dim TabName As String
Dim RowCol() As String

  'loop through all the instructions on the page and update the appropriate data tables
  InstCount = NewSheet.Cells(InstCountRow, InstCountCol)
  For Inst = 1 To InstCount
    RowCol = Split(NewSheet.Cells(InstCountRow + Inst, InstDataCol), ",")
    SQLString = NewSheet.Cells(InstCountRow + Inst, InstCol)
    SQLString = Replace(SQLString, """", "'")
    If InStr(1, SQLString, LikeClinicName) > 0 Then
      SQLString = Replace(SQLString, LikeClinicName, "'" & ClinicSystoc & "%'")
    Else
      SQLString = Replace(SQLString, ClinicNameParm, "'" & ClinicSystoc & "'")
    End If
    SQLString = Replace(SQLString, LocIDParm, "'" & ClinicLocID & "%'")
    SQLString = Replace(SQLString, StartDateParm, "#" & StartDate & "#")
    SQLString = Replace(SQLString, EndDateParm, "#" & EndDate & "#")
    Data.Open Source:=SQLString, ActiveConnection:=CurrentProject.Connection
    If Not Data.EOF And Not Data.BOF Then
      NewSheet.Cells(CInt(RowCol(0)), CInt(RowCol(1))).CopyFromRecordset Data
    End If
    Data.Close
  Next

  'search for all external sheet refrences and truncate them so it points to *this* worksheet
  Set Rng = NewSheet.Range(NewSheet.Cells.Address).Find(What:=XLMasterFileName, LookIn:=xlFormulas, Lookat:=xlPart, MatchCase:=False)
  While Not Rng Is Nothing
    Formula = Rng.Cells(1, 1).Formula
    If InStr(1, Formula, "'") > 0 Then
      SChar = InStr(1, Formula, "'")
      EChar = InStr(SChar + 1, Formula, "'")
      Bracket = InStr(1, Formula, "]")
      TabName = Mid(Formula, Bracket + 1, EChar - Bracket - 1)
      Rng.Replace What:=Mid(Formula, SChar, EChar - SChar + 1), replacement:=TabName, Lookat:=xlPart
    End If
    Set Rng = NewSheet.Range(NewSheet.Cells.Address).Find(What:=XLMasterFileName, LookIn:=xlFormulas, Lookat:=xlPart, MatchCase:=False)
  Wend
  Set Rng = Nothing

  'fix all the pivot table data sources so they point to *this* spreadsheet
  'TODO: add a filter in here to remove blanks
  'NOTE: do I want to add for all pivots, or only selected ones?
  For Each pt In NewSheet.PivotTables
    Formula = pt.PivotCache.SourceData
    Bracket = InStr(1, Formula, "!")
    Formula = Right(Formula, Len(Formula) - Bracket)
    pt.ChangePivotCache XLClinic.PivotCaches.Create(SourceType:=xlDatabase, SourceData:=Formula)
  Next

  SaveNewXL NewXLName         'yes, save the spreadsheet every single time so that the links in the PPT can be updated to point to it.  Sigh...

End Sub

UPDATE

Based on R3uK's suggestion, I've added a call to the beginning of the UpdateCharts Sub, here:

  If Master.ChartObjects.Count > 0 Then
    Set ColorWheel = New ChartColors      'only do this if we need to
  End If
  For Each MChart In Master.ChartObjects
    If Not MChart.Chart.PivotLayout Is Nothing Then

      'Re-copy just the pivot chart from Master to Clinic
      CopyPivotChart PivotItemsList, MChart, CChart, Clinic

    End If

With CopyPivotChart here:

Private Sub CopydPivotChart(ByVal PivotItemsList As PivotTableItems, ByVal MChart As Excel.ChartObject, ByRef CChart As Excel.ChartObject, ByRef Clinic As Worksheet)

Dim TChart As Excel.ChartObject

'Breakpoint 1
  For Each TChart In Clinic.ChartObjects
    If TChart.Name = MChart.Name Then
      TChart.Delete
    End If
  Next

  MChart.Chart.ChartArea.Copy
'Breakpoint 2
  Clinic.PasteSpecial Format:="Microsoft Office Drawing Object", Link:=False, DisplayAsIcon:=False
  Clinic.PasteSpecial Format:=2

End Sub

When I run that code, I now get

Run-time error '1004': Method 'PasteSpecial' of object'_Worksheet' failed

at the line following Breakpoint 2.

Now, if I skip the For Each loop at Breakpoint 1 (manually drag the execution point below the loop), and manually delete the chart from Worksheet Clinic, then the code executes just fine.

Community
  • 1
  • 1
FreeMan
  • 5,660
  • 1
  • 27
  • 53
  • I can't seem to replicate your problem. When I copy a sheet containing a PivotChart to a new workbook with `Excel.ActiveWorkbook.Sheets("SheetX").Copy` it copies the source data as well, and remains a pivotchart. Am I missing something here? – Jens Apr 16 '15 at 09:00
  • Excellent question, @Jens, seems I left out an important and obvious (but only to me) detail. `Source.Sheets("Sheet1")` contains the raw data and the pivot _Tables_, `Source.Sheets("Sheet2")` contains the pivot _Charts_. The middle `While` loop in `ProcessDataSheet()` is used to update the PivotTables on "Sheet1" so they point to `Destination.("Sheets1")`, and that works just fine. – FreeMan Apr 16 '15 at 10:15
  • Hi FreeMan...are you able to upload a sample somewhere and then supply a link to your sample? – Davesexcel Apr 18 '15 at 16:22
  • This is all health info, @Davesexcel, so it would take a lot of recreating data to make a sample that won't violate [HIPPA](http://www.hhs.gov/ocr/privacy/). I'll take a look on Monday to see if I can create something... – FreeMan Apr 18 '15 at 19:08

4 Answers4

1

1 . In my humble experience of copying Pivot Chart, I didn't copy the Sheet but the Chart :

Sheets("Graph1").ActiveChart.ChartArea.Copy
ActiveSheet.PasteSpecial Format:="Objet Dessin Microsoft Office", _
    Link:=True, DisplayAsIcon:=False

Have you tried to create an empty page and to paste the Chart in it? You probably will have to change the format which is in French, but that should do the trick!

2 . No clue....

3 . For creating an Pivot Table from scratch, I have no magical tricks, but I use this as a template :

Sub Create_DCT(ByVal Source_Table_Name As String, ByVal DCT_Sheet_Name As String, ByVal DCT_Name As String)


    DeleteAndAddSheet DCT_Sheet_Name

    ActiveWorkbook.PivotCaches.Create(SourceType:=xlDatabase, _
        SourceData:=Source_Table_Name, _
        Version:=xlPivotTableVersion14). _
        CreatePivotTable _
        TableDestination:=DCT_Sheet_Name & "!R3C1", _
        TableName:=DCT_Name, _
        DefaultVersion:=xlPivotTableVersion14


End Sub

Sub Add_Fields_DCT(ByVal DCT_Sheet_Name As String, ByVal DCT_Name As String)
    Dim Ws As Worksheet
    Set Ws = Worksheets(DCT_Sheet_Name)

    'Organized filters
    With Ws.PivotTables(DCT_Name).PivotFields("Cluster")
        .Orientation = xlPageField
        .Position = 1
    End With
    With Ws.PivotTables(DCT_Name).PivotFields("Region")
        .Orientation = xlPageField
        .Position = 2
    End With
    With Ws.PivotTables(DCT_Name).PivotFields("Account")
        .Orientation = xlPageField
        .Position = 3

    'Organized rows
    With Ws.PivotTables(DCT_Name).PivotFields("Family")
        .Orientation = xlRowField
        .Position = 1
    End With
    With Ws.PivotTables(DCT_Name).PivotFields("Sub_family")
        .Orientation = xlRowField
        .Position = 2
    End With
    With Ws.PivotTables(DCT_Name).PivotFields("Invoice_Country")
        .Orientation = xlRowField
        .Position = 3
    End With
    With Ws.PivotTables(DCT_Name).PivotFields("Product")
        .Orientation = xlRowField
        .Position = 4
    End With

    'Columns : none
'    With Ws.PivotTables(DCT_Name).PivotFields("Family")
'        .Orientation = xlColumnField
'        .Position = 1
'    End With


'Data fields (adding, modifying, formatting and compacting)
    'Data fiels : Adding
    'With Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name)
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Quantity"), "Total Qty", xlSum
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Quantity"), "Avg Qty", xlAverage
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Quantity"), "Qty of Orders", xlCount
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("TotalAmountEUR"), "TO (€)", xlSum
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("UPL"), "Avg UPL", xlAverage
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Discount"), "Avg Discount", xlAverage
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Discount"), "Min Discount", xlMin
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("Discount"), "Max Discount", xlMax
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("PVU"), "Min PVU", xlMin
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("PVU"), "Max PVU", xlMax
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("(PVU-PRI)/PVU"), "Gross Margin", xlAverage
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("(PVU-TC)/PVU"), "Net Margin", xlAverage
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("PVU-PRI"), "Gross Profit (€)", xlSum
        Ws.PivotTables(DCT_Name).AddDataField Ws.PivotTables(DCT_Name).PivotFields("PVU-TC"), "Net Profit (€)", xlSum
    'End With

    'Data fiels : Modifying
'    With Ws.PivotTables(DCT_Name).PivotFields("Somme de Quantity")
'        .Caption = "Moyenne de Quantity"
'        .Function = xlAverage
'    End With



    'Data formatting
    With ActiveSheet.PivotTables(DCT_Name)
        .PivotFields("Total Qty").NumberFormat = "# ##0"
        .PivotFields("Avg Qty").NumberFormat = "# ##0,#"
        .PivotFields("Qty of Orders").NumberFormat = "# ##0"
        .PivotFields("TO (€)").NumberFormat = "# ##0 €"
        .PivotFields("Avg UPL").NumberFormat = "# ##0 €"
        .PivotFields("Avg Discount").NumberFormat = "0,0%"
        .PivotFields("Min Discount").NumberFormat = "0,0%"
        .PivotFields("Max Discount").NumberFormat = "0,0%"
        .PivotFields("Min PVU").NumberFormat = "# ##0 €"
        .PivotFields("Max PVU").NumberFormat = "# ##0 €"
        .PivotFields("Gross Margin").NumberFormat = "0,0%"
        .PivotFields("Net Margin").NumberFormat = "0,0%"
        .PivotFields("Gross Profit (€)").NumberFormat = "# ##0 €"
        .PivotFields("Net Profit (€)").NumberFormat = "# ##0 €"
    End With


'Compact row fields to minimum
For Each PivIt In ActiveSheet.PivotTables(DCT_Name).PivotFields("Sub_family").PivotItems
    PivIt.DrillTo "Invoice_Country"
Next PivIt

For Each PivIt In ActiveSheet.PivotTables(DCT_Name).PivotFields("Family").PivotItems
    PivIt.DrillTo "Sub_family"
Next PivIt

For Each PivIt In ActiveSheet.PivotTables(DCT_Name).PivotFields("Family").PivotItems
    PivIt.DrillTo "Family"
Next PivIt

End Sub

And my custom fucntion DeleteAndAddSheet :

Public Function DeleteAndAddSheet(ByVal SheetName As String) As Worksheet

For Each aShe In Sheets
    If aShe.Name <> SheetName Then
    Else
        Application.DisplayAlerts = False
        aShe.Delete
        Application.DisplayAlerts = True
        Exit For
    End If
Next aShe

Sheets.Add After:=Sheets(Sheets.Count)
Sheets(Sheets.Count).Name = SheetName

Set DeleteAndAddSheet = ThisWorkbook.Worksheets(Worksheets.Count)

End Function

Hope it will help you!

R3uK
  • 14,417
  • 7
  • 43
  • 77
  • OK, this has been simmering for a while. Tried your suggestion #1. I like it, _however_ I can't guarantee that all charts on the sheet will be PivotCharts, and many sheets have some data that is used for charts, or as in the the PPT that they eventually get copied to, so... My goal is to 1) Copy the sheet, 2) _Delete_ the PivotChart, if it exists, 3) copy the PivotChart from the master. This works great if I do step 2 by hand (in debug). It fails with `err 1004 Method PasteSpecial of object _Worksheet failed` if I let the code loop through all the charts to delete from the dest Sheet by name. – FreeMan Apr 21 '15 at 17:39
  • A little something (time to sleep here ;) ), have you tried `ByVal Clinic` in `CopydPivotChart` ? I'll take a good look into it in few hours as it is so close! – R3uK Apr 21 '15 at 22:10
  • It has to be `ByRef`. `Clinic` is the destination worksheet - it's where the new charts need to go! – FreeMan Apr 21 '15 at 23:35
  • I'm giving the bounty here because I believe that suggestion #1 is the right path. There are just some minor issues to be worked out & I'll be good to go. Thanks for your help, R3uK!! – FreeMan Apr 22 '15 at 17:53
  • Thx @FreeMan. I'll probably do something similar soon, so I'll keep proposing some ideas (even stupid ones probably). You don't seem to use `ByVal PivotItemsList As PivotTableItems` and `ByRef CChart As Excel.ChartObject` in your `CopyPivotChart`, get rid of it maybe? As you pass the chart as reference and yet you delete it at `Breakpoint 1` without direct reference. Maybe even try to do both in both orders `TChart.Delete` and `CChart.Delete` or just `CChart.Delete`. Or have you tried to paste in an empty chart or a range? – R3uK Apr 22 '15 at 19:08
  • Yeah, that's part of a refactor of some more significant code that I'm looking to replace with your simpler suggestion. I'll have to look into that some more, maybe that's where my issue is. – FreeMan Apr 22 '15 at 19:22
  • 1
    A fresh answer to this reminded me to follow up. I've posted my working result as the accepted answer. Don't know if you've managed to get your task completed with a minimum of head-desk interfacing, but my solution may work for you, and I wanted to draw your attention to it. Hope all is well, and thanks again for your help! – FreeMan Dec 04 '15 at 13:59
1

i suggest creating a second workbook linked to the source data sheet, then create matching pivot tables in the second sheet (both are essentially using the same data to populate) -- I'm not sure on the next point, does the client need to have a linked version of the charts or is this purely for auto reporting?

If it is for auto reporting then I'd suggest a new approach entirely, using the current workbook you have - create a macro to run when required ontime, daily/weekly/monthly etc and send the charts (from the sheets you select only) as a pdf - i have some sample code for this if required :)

DCX
  • 102
  • 5
1

I resolved the issue by using the .XLTM macro-enabled template as an actual template!

Instead of opening the template file then copying worksheets that are needed from the template to a new workbook, I now open the .XLTM, remove the sheets that aren't needed for a particular client's report. This has completely eliminated the need to copy sheets, charts and graphs, and removed all the errors created by attempting to do so.

This doesn't specifically resolve the issue of how to copy the pivot chart without losing its pivot chartiness, but it resolved the bigger picture issue of how do I make this happen (I did say I was open to alternate suggestions).

FreeMan
  • 5,660
  • 1
  • 27
  • 53
  • Thx for your input, and indeed, alternate methods are often the bests!^^ Especially when it requires less heavy lifting! – R3uK Dec 04 '15 at 14:33
0

If you copy a sheet with data and a sheet with a chart linked to that data, the copied chart will not link to the data on the copied sheet unless the sheets are copied together in one operation. It looks like your code first copies the sheet with the pivot table, and then separately it copies the sheet with the chart (chart sheet or worksheet with embedded chart, doesn't matter). The chart loses its link to the pivot table in the original workbook, and becomes a regular chart with hard-coded values.

Rewrite your code to copy both sheets in one operation. Then do the adjustments of the pivot table and the chart in the new workbook.

Jon Peltier
  • 5,895
  • 1
  • 27
  • 27
  • 1
    Thanks for your thoughts, Jon. I resolved this a while ago, though, but you did prompt me to post what ended up being my working solution. – FreeMan Dec 04 '15 at 13:56