2

I am using EPPlus to create an Excel workbook, and I have created a pivot table.

var pivotTable1 = sheet.PivotTables.Add(sheet.Cells["M1"], sourcePivotData, "MyPivotTable");

pivotTable1.ColumnFields.Add("col4");
pivotTable1.RowFields.Add(pivotTable1.Fields["Col1"]);
pivotTable1.RowFields.Add(pivotTable1.Fields["Col2"]);
pivotTable1.RowFields.Add(pivotTable1.Fields["Col3"]);

var dataField = pivotTable1.DataFields.Add(pivotTable1.Fields["Amount"]);

I now want to get a reference to the pivot table OUTPUT data as a Range. I know the upper left cell, but given the size of the pivot table in terms of rows and column is dynamic based on the underlying data source, I can't figure out a way to dynamically figure out the last row and last column in the pivot table output range and I can't find any property of the pivot table object that gives me this.

In addition, it doesn't look like pivot table output data shows up in the

worksheet.Cells[]

property of the sheet, so I can't figure it out through that mechanism.

Is there a way to figure out the range of a pivot table output data using EPPlus?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
leora
  • 188,729
  • 360
  • 878
  • 1,366
  • Would this help you any further? https://epplussoftware.com/docs/5.8/api/OfficeOpenXml.Table.PivotTable.ExcelPivotTableRowColumnFieldCollection.html It's supposed to have a .Count possibility and considering you know the upper right cell, you should know the last column and beginning position to have your full range. – Notus_Panda Dec 15 '22 at 09:20
  • @Notus_Panda - it doesn't seem to help as this collection stores the column pivots and the row pivots (not the actual data results output) so the count is not giving you the size of the results – leora Dec 15 '22 at 11:36
  • Does `worksheet.Dimension.Address` work? https://stackoverflow.com/questions/4777128/c-sharp-epplus-openxml-count-rows – Notus_Panda Dec 15 '22 at 12:32
  • Check [PivotTable.TableRange1 Property](https://learn.microsoft.com/en-us/dotnet/api/microsoft.office.interop.excel.pivottable.tablerange1?view=excel-pia). This is a range object of the whole Pivot Table including columns and rows and grand totals. I can't post an answer because i don't know c# but in VBA, as example, `PivotTables("nameofpivottable").TableRange1` will return a range object referenced to all cells of the Pivot Table. Maybe something like `pivotTable1.TableRange1` may work but not tested – Foxfire And Burns And Burns Dec 15 '22 at 14:26
  • @FoxfireAndBurnsAndBurns - i am not using Microsoft.Excel.Interop because i am wring this code on a mac. .i am using EPPlus library that doesn't have TableRange1 property – leora Dec 16 '22 at 03:19
  • @leora is there some reason that `var pivotTableLocation = pivotTable1.Address;` won't work? [Address](https://epplussoftware.com/docs/5.8/api/OfficeOpenXml.Table.PivotTable.ExcelPivotTable.html#OfficeOpenXml_Table_PivotTable_ExcelPivotTable_Address) returns the location as the type [ExcelAddressBase](https://epplussoftware.com/docs/5.8/api/OfficeOpenXml.ExcelAddressBase.html#constructors), which can be formulated as a range. – joeschwa Dec 17 '22 at 07:56
  • @joeschwa - Address only gives me a reference to the top left cell of the pivot table. It doesn't give me the size in Rows or Columns to be able to capture the range of the entire pivot table data – leora Dec 17 '22 at 09:44

3 Answers3

2

I do see a GitHub issue matching this. So until they fix this / release a new feature, you can't do anything without a hacky solution.

It looks like with EPPlus (at least till 6.1), you can only create the schema definition of "how" the pivot table should be generated, and not the actual table itself. At least this is how the default config / setting is. I am not some EPPlus expert. But I don't see any settings / config to instruct EPPlus to generate the data alongside schema.

That's the reason you can't see any data with worksheet.Cells, and also the reason why the underlying XML file doesn't have rowItems and colItems node.

The actual table is generated only when you open it in some .xlsx reader (like Libre or Excel). It then evaluates / generates the Pivot table based on the definition generated by EPPlus (your code). And if at this point if you save the file, it will then copy the data to the XML files as well.

Once you reach this stage, you should be able to access the data using worksheet.Cells as usual, and even get the range using pivotTable.Address property ( or maybe the below function if Address property doesn't work).

This might not sound like a solution, but, as a hacky solution, you can do the following steps to generate, and read the underlying data.

  1. Create the pivot table as usual
  2. Open the file using some Excel application (from code)
  3. Send Ctrl + S command to the process. (This will save the generated pivot table to the underlying XML file.)
  4. Read the Excel sheet from your code again.

You can refer to this for the hacky part.

Or maybe use another library which actually generates the data, and not just the definition.

using OfficeOpenXml;
using OfficeOpenXml.Table.PivotTable;
using System.Text;

public static class ExcelExtensions
{
    // https://stackoverflow.com/a/182924/15066006
    static int ToColumnNumber(this string colAdress)
    {
        int[] digits = new int[colAdress.Length];
        for (int i = 0; i < colAdress.Length; ++i)
        {
            digits[i] = Convert.ToInt32(colAdress[i]) - 64;
        }
        int mul = 1; int res = 0;
        for (int pos = digits.Length - 1; pos >= 0; --pos)
        {
            res += digits[pos] * mul;
            mul *= 26;
        }
        return res;
    }

    // https://stackoverflow.com/a/2652855/15066006
    static string ToColumnAdress(this int col)
    {
        if (col <= 26)
        {
            return Convert.ToChar(col + 64).ToString();
        }
        int div = col / 26;
        int mod = col % 26;
        if (mod == 0) { mod = 26; div--; }
        return ToColumnAdress(div) + ToColumnAdress(mod);
    }

    public static ExcelRange? GetExcelRange(this ExcelPivotTable pivotTable)
    {
        var locationTag = pivotTable.PivotTableXml.GetElementsByTagName("location")[0];
        var baseLoc = locationTag?.Attributes?.GetNamedItem("ref")?.Value ?? string.Empty;

        if (baseLoc.Contains(':'))
            return pivotTable.WorkSheet.Cells[baseLoc];

        var firstHeaderValue = locationTag?.Attributes?.GetNamedItem("firstHeaderRow")?.Value ?? string.Empty;
        var firstDataRowValue = locationTag?.Attributes?.GetNamedItem("firstDataRow")?.Value ?? string.Empty;
        var firstDataColValue = locationTag?.Attributes?.GetNamedItem("firstDataCol")?.Value ?? string.Empty;

        var firstHeaderRow = firstHeaderValue is null ? 1 : Int32.Parse(firstHeaderValue);
        var firstDataRow = firstDataRowValue is null ? 1 : Int32.Parse(firstDataRowValue);
        var firstDataCol = firstDataColValue is null ? 1 : Int32.Parse(firstDataColValue);

        var headers = firstHeaderRow - 1;
        var colItems = pivotTable.GetColumnItems();
        var rowItems = pivotTable.GetRowItems();
        var totalRows = firstDataRow + headers + rowItems;
        var totalColumns = firstDataCol + colItems;

        string columnLetter; int currentRowNumber; int i = 0;
        var columnLetterBuffer = new StringBuilder();
        while (Char.IsLetter(baseLoc[i]))
        {
            columnLetterBuffer.Append(baseLoc[i]);
            ++i;
        }
        columnLetter = columnLetterBuffer.ToString();
        currentRowNumber = int.Parse(baseLoc[i..]);

        var currentColNumber = columnLetter.ToColumnNumber();
        var lastColNumber = currentColNumber + totalColumns - 1;
        var lastRowNumber = currentRowNumber + totalRows;
        var bottomRightCell = $"{lastColNumber.ToColumnAdress()}{lastRowNumber}";
        return pivotTable.WorkSheet.Cells[$"{baseLoc}:{bottomRightCell}"];
    }

        public static int GetRowItems(this ExcelPivotTable pivotTable)
    {
        var rowItems = pivotTable.PivotTableXml.GetElementsByTagName("rowItems")[0];

        if (rowItems is null)
        {
            Console.WriteLine("Data hasn't been generated yet. Open it in some excel application to actually generate the pivot table first");
            return 0;
        }
        else
        {
            var attrValue = rowItems?.Attributes?.GetNamedItem("count")?.Value;
            return attrValue is null ? 0 : Int32.Parse(attrValue);
        }
    }

    public static int GetColumnItems(this ExcelPivotTable pivotTable)
    {
        var colItems = pivotTable.PivotTableXml.GetElementsByTagName("colItems")[0];

        if (colItems is null)
        {
            Console.WriteLine("Data hasn't been generated yet. Open it in some excel application to actually generate the pivot table first");
            return 0;
        }
        else
        {
            var attrValue = colItems?.Attributes?.GetNamedItem("count")?.Value;
            return attrValue is null ? 0 : Int32.Parse(attrValue);
        }
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gliesefire
  • 616
  • 5
  • 12
  • thanks for the answer but this code above only returns the top left most cell in the pivot table (which i already know) and does not return the full pivot table output range. – leora Dec 19 '22 at 09:25
  • That's weird. If you have a .xlsx file, this should work. Can you please provide a MRE? And before that, are you creating the workbook programmatically, or reading it from disk? – gliesefire Dec 19 '22 at 11:42
  • @leora Can you try now, see if that helps. If not, I am out of ideas here :D. – gliesefire Dec 19 '22 at 20:06
  • i am programmatically creating the workbook from scratch. Also i just tried again using the latest code and still only get a string with the top left cell. – leora Dec 21 '22 at 13:56
  • Looks like I was still looking at your previous solution. So i am programmatically creating the workbook from scratch. Also i just tried the new solution you put at the top of your answer and I do get a range now but it doesn't cover any of the data content part of the pivot table so the range isn't correct. . i am going to try to debug and hope i can tweak your code to get it to work – leora Dec 21 '22 at 14:08
  • If it is giving you a range, but no data content, then it probably means that the pivot table component hasn't been saved yet. You should call `package.Save()` method before calling the above function. That will generate the data components, and in turn give you the correct range as well. – gliesefire Dec 21 '22 at 18:19
  • Thank you as after I call package. Save() I now get a range! So this solution is close but 1 issue. in my example, the pivottable range is M1:Z89 but your function returns M1:Z69. i have isolated this down to the fact that, as per my question, i have 3 Rowfields in my pivot (Col1, Col2 & Col3) and the pivotTable.GetRowItems() only seems to return the lowest level of the pivot (Col3) and so I get 69 rows that have data but ignores the 20 rows coming from the Col1 & Col2 buckets. Do you know if those rows can be added to pivotTable.GetRowItems as i think that would make this the perfect – leora Dec 22 '22 at 12:08
  • @leora I have updated the answer with my findings. The nutshell being "EPPlus doesn't generate data alongside the pivot table schema definition. So you will have to rely on other libraries, or external application to read / count the underlying data." – gliesefire Dec 22 '22 at 21:02
  • thanks for your effort. I was looking for a way to provide feedback to EPPlus on their website to fix this properly but don't see the ability to send feedback unfortunately – leora Dec 23 '22 at 01:42
  • You can try raising a question / enhancement request in their official github repo. https://github.com/EPPlusSoftware/EPPlus. It is a known issue https://github.com/EPPlusSoftware/EPPlus/issues/397#issuecomment-847575935 – gliesefire Dec 23 '22 at 05:01
0
using (ExcelPackage excelPackage = new ExcelPackage(new FileInfo("Workbook.xlsx")))
{
    // Get the worksheet that contains the pivot table
    ExcelWorksheet worksheet = excelPackage.Workbook.Worksheets["Sheet1"];

    // Get the pivot table
    ExcelPivotTable pivotTable = worksheet.PivotTables["PivotTable1"];

    // Get the range of the pivot table
    ExcelRange pivotTableRange = pivotTable.CacheDefinition.SourceRange;

    // Get the last row of the pivot table range
    int lastRow = pivotTableRange.End.Row;

    // Get the last column of the pivot table range
    int lastColumn = pivotTableRange.End.Column;
}

By using the End property of the ExcelRange object and the Row and Column properties of the resulting ExcelRange, you can dynamically figure out the last row and last column of the range of the pivot table.

  • 1
    Thanks for the answer but I am not looking for the range of the Source pivot table data. I am looking for the range of the Pivottable output – leora Dec 19 '22 at 09:22
-1

Use something like this:

var range = pivotTable1.CacheDefinition.SourceRange;
user16930239
  • 6,319
  • 2
  • 9
  • 33
  • Wouldn't that give the sourcedata range instead of the range of the pivottable? As mentioned in documentation https://epplussoftware.com/docs/5.8/api/OfficeOpenXml.Table.PivotTable.ExcelPivotCacheDefinition.html – Notus_Panda Dec 15 '22 at 09:25
  • I don't need the range of the source data.. I am looking for the range of the pivot table data – leora Dec 15 '22 at 11:13