I am using EPPlus to create a pivot table and now I am trying to use ConditionalFormatting on the pivot table and it simply doesn't seem to work. I have taken the example from Three color scale example and am trying to apply it on the cells of the pivot table:
var rng = worksheet.Cells["N3:Y88"]; // (This is the output of the pivot table)
var cfRule = rng.Worksheet.ConditionalFormatting.AddThreeColorScale(rng);
cfRule.LowValue.Color = ColorTranslator.FromHtml("#FF63BE7B");
cfRule.MiddleValue.Color = ColorTranslator.FromHtml("#FFFFEB84");
cfRule.MiddleValue.Type = eExcelConditionalFormattingValueObjectType.Percentile;
cfRule.MiddleValue.Value = 50;
cfRule.HighValue.Color = ColorTranslator.FromHtml("#FFF8696B");
And there isn't any impact. I use the same code on a set of regular cells and it works fine. Does EPPlus support the ability to conditionalformat a pivot table?
I found the answer in Pivot Table support in Conditional Formatting #399.
public static void AddConditionalFormattingToPivotTable(ExcelPivotTable pivotTable)
{
var worksheetXml = pivotTable.WorkSheet.WorksheetXml;
var element = worksheetXml.GetElementsByTagName("conditionalFormatting")[0];
((XmlElement)element).SetAttribute("pivot", "1");
}
Related
I am working on a piece of code to generate a pivot table in Excel.
This is the code:
using (XL.XLWorkbook workbook = new XL.XLWorkbook(sourceFile))
{
var outSheet = workbook.Worksheets.Add("output table");
outSheet.Cell(1, 1).InsertTable(dt, "out table", true);
var datarange = outSheet.RangeUsed();
var pivotSheet = workbook.Worksheets.Add("PivotTable");
var pivotTable = pivotSheet.PivotTables.AddNew("Pivot Table", pivotSheet.Cell(3, 1), datarange);
pivotTable.ReportFilters.Add("Filter1");
pivotTable.ReportFilters.Add("Filter2");
pivotTable.RowLabels.Add("RLabel");
pivotTable.ColumnLabels.Add("CLabel");
pivotTable.Values.Add("Value").SummaryFormula = XL.XLPivotSummary.Sum;
workbook.SaveAs(#"C:\Temp\Test.xlsx");
}
How would I go about to filter the values in "Filter1"?
For example, selecting only the values for "Unknown" and "Gcom".
In Excel the Pivot filter looks like this:
Excel Pivot Table Report Filter
I have checked all the ClosedXML documentation for pivots, the ReportFilters functionality is not mentioned.
Source code wiki example
Please advise, is this functionality even available?
Any advice/help is much appreciated.
Not sure when the functionality was added, but I got it to work with the following additions to your code:
using (XL.XLWorkbook workbook = new XL.XLWorkbook(sourceFile))
{
var outSheet = workbook.Worksheets.Add("output table");
outSheet.Cell(1, 1).InsertTable(dt, "out table", true);
var datarange = outSheet.RangeUsed();
var pivotSheet = workbook.Worksheets.Add("PivotTable");
var pivotTable = pivotSheet.PivotTables.AddNew("Pivot Table", pivotSheet.Cell(3, 1), datarange);
// I was not sure how to retrieve the filter after adding, but found Add() returns it for you.
var filter1 = pivotTable.ReportFilters.Add("Filter1");
// Now add your filter selection.
filter1.AddSelectedValue("Unknown");
filter1.AddSelectedValue("GCom");
pivotTable.ReportFilters.Add("Filter2");
pivotTable.RowLabels.Add("RLabel");
pivotTable.ColumnLabels.Add("CLabel");
pivotTable.Values.Add("Value").SummaryFormula = XL.XLPivotSummary.Sum;
workbook.SaveAs(#"C:\Temp\Test.xlsx");
}
I'm using EPPlus in my project and I know you can copy an existing Shape however there is no method for copying an existing Chart.
I have setup a workbook with template charts that I need to duplicate and update the series to point to different datatable/sets.
I can populate the data no worries and can create new charts, but then need to size and position and style. It would be a lot easier to just clone the chart template and modify series and position to simplify the code.
Currently I use this approach:
// wb is an ExcelWorkbook
ExcelWorksheet ws = wb.Worksheets[sheetIdx];
ExcelChart chart = (ExcelChart)ws.Drawings[0];
ExcelChart cc = ws.Drawings.AddChart("Chart " + (i + 2), eChartType.ColumnClustered);
// invoke methods that will position and size new chart
// copy starting chart xml so will have identical styling, series, legend etc
var xml = XDocument.Parse(chart.ChartXml.InnerXml);
XNamespace nsC = "http://schemas.openxmlformats.org/drawingml/2006/chart";
XNamespace nsA = "http://schemas.openxmlformats.org/drawingml/2006/main";
// modify xml to update Category, Title and Values formulas
var fs = xml.Descendants(nsC + "f");
foreach (var f in fs)
{
f.Value = ws.Cells[f.Value].Offset(chartNumRows + 1, 0).FullAddressAbsolute;
}
// set new chart xml to modified xml.
cc.ChartXml.InnerXml = xml.ToString();
Which works, but there are several drawbacks.
1) The chart.series of the clone (cc in my example) has not been set, peeking at the code this is because it is only done during the object construction. If I could get this property to update then I would be able to easily resolve the second issue
2) I need to remove all series and add new ones and because the series property isn't initialised properly this is harder than it should be.
Any help getting properties to initialise in the chart or a better method of cloning the original would be greatly appreciated!
It seems like there is no built-in functionality for this and an other reload methods I could come up with required too many changes to the EPPlus source, so until I find a better solution I have added the following method to EPPlus\Drawings\ExcelDrawings.cs
public ExcelChart CloneChart(ExcelChart SourceChart, String NewName)
{
// Create clone
var tempClone = this.AddChart(NewName, SourceChart.ChartType, null);
tempClone.ChartXml.InnerXml = SourceChart.ChartXml.InnerXml;
// Reload clone
using (tempClone.Part.Stream = new MemoryStream())
{
// Create chart object using temps package and xml
var chartXmlBytes = Encoding.ASCII.GetBytes(tempClone.ChartXml.OuterXml);
tempClone.Part.Stream.Write(chartXmlBytes, 0, chartXmlBytes.Length);
var finalClone = ExcelChart.GetChart(this, tempClone.TopNode);
// Remove old from collection
var index = _drawingNames[tempClone.Name];
var draw = _drawings[index];
for (int i = index + 1; i < _drawings.Count; i++)
_drawingNames[_drawings[i].Name]--;
_drawingNames.Remove(draw.Name);
_drawings.Remove(draw);
// Add new to collection
finalClone.Name = tempClone.Name;
_drawings.Add(finalClone);
_drawingNames.Add(finalClone.Name, _drawings.Count - 1);
// Done
return finalClone;
}
}
I have a small program where you can select some database tables and create a excel file with all values for each table and thats my solution to create the excel file.
foreach (var selectedDatabase in this.lstSourceDatabaseTables.SelectedItems)
{
//creates a new worksheet foreach selected table
foreach (TableRetrieverItem databaseTable in tableItems.FindAll(e => e.TableName.Equals(selectedDatabase)))
{
_xlWorksheet = (Excel.Worksheet) xlApp.Worksheets.Add();
_xlWorksheet.Name = databaseTable.TableName.Length > 31 ? databaseTable.TableName.Substring(0, 31): databaseTable.TableName;
_xlWorksheet.Cells[1, 1] = string.Format("{0}.{1}", databaseTable.TableOwner,databaseTable.TableName);
ColumnRetriever retrieveColumn = new ColumnRetriever(SourceConnectionString);
IEnumerable<ColumnRetrieverItem> dbColumns = retrieveColumn.RetrieveColumns(databaseTable.TableName);
var results = retrieveColumn.GetValues(databaseTable.TableName);
int i = 1;
(result is a result.Item3 is a List<List<string>> which contains all values from a table and for each row is a new list inserted)
for (int j = 0; j < results.Item3.Count(); j++)
{
int tmp = 1;
foreach (var value in results.Item3[j])
{
_xlWorksheet.Cells[j + 3, tmp] = value;
tmp++;
}
}
}
}
It works but when you have a table with 5.000 or more values it will take such a long time.
Does someone maybe know a better solution to add the List List string per row than my for foreach solution ?
I utilize the GetExcelColumnName function in my code sample to convert from column count to the excel column name.
The whole idea is, that it's very slow to write excel cells one by one. So instead precompute the whole table of values and then assign the result in a single operation. In order to assign values to a two dimensional range, use a two dimensional array of values:
var rows = results.Item3.Count;
var cols = results.Item3.Max(x => x.Count);
object[,] values = new object[rows, cols];
// TODO: initialize values from results content
// get the appropriate range
Range range = w.Range["A3", GetExcelColumnName(cols) + (rows + 2)];
// assign all values at once
range.Value = values;
Maybe you need to change some details about the used index ranges - can't test my code right now.
As I see, youd didn't do profiling. I recomend to do profiling first (for example dotTrace) and see what parts of your code actualy causing performance issues.
In my practice there is rare cases (almost no such cases) when code executes slower than database requests, even if code is realy awfull in algorithmic terms.
First, I recomend to fill up your excel not by columns, but by rows. If your table has many columns this will cause multiple round trips to database - it is great impact to performance.
Second, write to excel in batches - by rows. Think of excel files as mini-databases, with same 'batch is faster than one by one' principles.
I'm trying to design a table that has 3 additional tables in the last cell. Like this.
I've managed to get the first nested table into row 4, but my second nested table is going into cell(1,1) of the first table.
var wordApplication = new Word.Application();
wordApplication.Visible = true;
var wordDocument = wordApplication.Documents.Add();
var docRange = wordDocument.Range();
docRange.Tables.Add(docRange, 4, 1);
var mainTable = wordDocument.Tables[1];
mainTable.set_Style("Table Grid");
mainTable.Borders.Enable = 0;
mainTable.PreferredWidthType = Word.WdPreferredWidthType.wdPreferredWidthPercent;
mainTable.PreferredWidth = 100;
docRange.Collapse(Word.WdCollapseDirection.wdCollapseStart);
var phoneRange = mainTable.Cell(4, 1).Range;
phoneRange.Collapse(Word.WdCollapseDirection.wdCollapseStart);
phoneRange.Tables.Add(phoneRange, 3, 2);
var phoneTable = mainTable.Cell(4, 1).Tables[1];
phoneTable.set_Style("Table Grid");
phoneTable.Borders.Enable = 0;
phoneTable.AutoFitBehavior(Word.WdAutoFitBehavior.wdAutoFitContent);
phoneTable.Rows.RelativeHorizontalPosition = Word.WdRelativeHorizontalPosition.wdRelativeHorizontalPositionMargin;
phoneRange.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
I've tried collapsing the range, adding in a paragraph then collapsing the range again. No luck. I found this post and many similar ones, but I must be missing something.
Thanks for your time.
It usually helps in situation like these to add a line in your code: phoneRange.Select(); and having code execution end with that. Take a look at where the Range actually is. Now you can test using the keyboard where the Range needs to be in order to insert the next table successfully.
Since you say phoneRange selects outside the third row, rather than working with phoneRange try setting a new Range object to phoneTable.Range then collapse it to its end-point.
I am trying to create a table with a header. I want this header to be repeated for each new page that the table takes. How can I do this in C# and OpenXml Wordprocessing?
DocumentFormat.OpenXml.Packaging.WordprocessingDocument internalDoc =
DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Open(stream, true);
var tables = wordDoc.MainDocumentPart.Document.Descendants<SdtBlock>().Where
( r => r.SdtProperties.GetFirstChild<Tag>().Val.Value.StartsWith(DATA_TABLE_TAG));
Table table = tables.Descendants<Table>().Single();
//Here can I set some property to repeat the header of the table?
As Chris said, an instance of the TableHeader class is what you need. It needs to be appended to the header row's TableRowProperties:
var row = table.GetFirstChild<TableRow>();
if (row.TableRowProperties == null)
row.TableRowProperties = new TableRowProperties();
row.TableRowProperties.AppendChild(new TableHeader());
For anyone who is looking for the same issue:
The code below must be applied to the header Row, as TablePropertiesRow
TableRowProperties tblHeaderRowProps = new TableRowProperties(
new CantSplit() { Val = OnOffOnlyValues.On },
new TableHeader() { Val = OnOffOnlyValues.On }
);
tblHeaderRow.AppendChild<TableRowProperties>(tblHeaderRowProps);
Deww!!
I think this is what you're looking for. If you apply that element to a particular row, it will behave the way you're describing.
To create header for every table in a page.
You need to create multiple body's and append to document.
If you want to create new header to every table, you need to append every table to new body then apply page break.
Finally, append all bodies to document.
Then you finally have your result in created document.
If any doubts reply to me.
Regards,
Balaji