Copy/Clone an Excel Chart with EPPlus - c#

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;
}
}

Related

Update Chart data in PowerPoint

I am creating PowerPoint by loading template and updating the data and saving as a new copy. I am using GemBox.Presentation and GemBox.SpreadSheet for that. I can access the chart data using spreadsheet however I am not sure how to update it.
Here is how i access chart from the template slide.
var chart = ((GraphicFrame)currentSlide.Content.Drawings[0]).Chart;
var stackedColumn = (GemBox.Spreadsheet.Charts.ColumnChart)chart.ExcelChart;
I have tried to update the data by converting to list but it still did not update.
foreach (var item in data)
{
var seriesItem = data;
var valueList = item.Values.Cast<int>().Select(x => x).ToList();
// values to update in chart
List<int> myValues = new List<int>(new int[] { 1, 2});
for (int v = 0; v < valueList.Count(); v++)
{
valueList[v] = myValues[v];
}
}
I also saw this example https://www.gemboxsoftware.com/presentation/examples/powerpoint-charts/412 to update chart however this adds the series but not updates the current data.
If you can please share if there is any support available for this component to perform to update chart data that would be appreciated. Thanks
One way is to use one of the ChartSeries.SetValues methods, as mentioned in the previous answer.
Another way is to update the underlining Excel cell.
For instance, in that example you linked to, you could do this:
var lineChart = (LineChart)chart.ExcelChart;
var lineSeries = lineChart.Series[0];
// Get series source cells.
string[] cellsReference = lineSeries.ValuesReference.Split('!');
var cells = lineChart.Worksheet.Parent.Worksheets
.First(w => w.Name == cellsReference[0].Trim('\''))
.Cells.GetSubrange(cellsReference[1]);
// Update cells with doubled values.
foreach (var cell in cells)
cell.SetValue(cell.DoubleValue * 2);
I have managed to update the data on the spreadsheet that is associated with powerpoint charts.
Class named ChartSeries contains method named SetValues() that will set the chart series values.
There are two ways you can use this method as it takes IEnumerable and Object [] as parameters as shown below.
public void SetValues(IEnumerable values)
public void SetValues(params object[] values)
And here is how i have access that value and updated it.
foreach (var item in data)
{
var seriesItem = data;
foreach (var value in seriesItem)
{
if (value.DisplayName == "[Open]")
{
value.SetValues(50,25);
}
}
}

How to add to Text Label For excel Charts using Open xml or EPPLUS

I am completely new to Excel automation in C#
Actually I am came across some of API for Excel generation in C# .net like CLOSED XML , EEPLUS , and spreadsheetlight by vincent ,Open XML by Microsoft , Interop excel by Microsoft
According My Study
CLOSED XML -- No charts supported
EEPLUS - Charts supported
Spread light- Very easy to use and Charts also supported
Open XML- complex hard to work
I was completely fine with Spread light light was good API , but i am not able to find a solution how to add label inside a Chart
I hope any one in stack overflow came across with same problem.
I need to add label like text inside chart like for example Company in chart.
Please let me know how to find solution any one this Free API
Thanks
Ranjith
You can add at title via Epplus but positioning will require XML editing:
[TestMethod]
public void Chart_Manual_Title_Test()
{
//http://stackoverflow.com/questions/37304860/how-to-add-to-text-label-for-excel-charts-using-open-xml-or-epplus
//Throw in some data
var datatable = new DataTable("tblData");
datatable.Columns.AddRange(new[] { new DataColumn("Col1", typeof(int)), new DataColumn("Col2", typeof(int)), new DataColumn("Col3", typeof(object)) });
for (var i = 0; i < 10; i++)
{
var row = datatable.NewRow();
row[0] = i;
row[1] = i * 10;
row[2] = Path.GetRandomFileName();
datatable.Rows.Add(row);
}
//Create a test file
var fileInfo = new FileInfo(#"c:\temp\Chart_Manual_Title_Test.xlsx");
if (fileInfo.Exists)
fileInfo.Delete();
using (var pck = new ExcelPackage(fileInfo))
{
var workbook = pck.Workbook;
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cells.LoadFromDataTable(datatable, true);
var chart = worksheet.Drawings.AddChart("chart test", eChartType.XYScatter);
var series = chart.Series.Add(worksheet.Cells["B2:B11"], worksheet.Cells["A2:A11"]);
chart.Title.Text = "XYZ Corp";
//Add custom layout
var chartXml = chart.ChartXml;
var nsm = new XmlNamespaceManager(chartXml.NameTable);
var nsuri = chartXml.DocumentElement.NamespaceURI;
nsm.AddNamespace("c", nsuri);
nsm.AddNamespace("a", "http://schemas.openxmlformats.org/drawingml/2006/main");
//Set the title overlay
var overlayNode = chartXml.SelectSingleNode("c:chartSpace/c:chart/c:title/c:overlay", nsm);
overlayNode.Attributes["val"].Value = "1";
//Set the font size
var defRPrNode = chartXml.SelectSingleNode("c:chartSpace/c:chart/c:title/c:tx/c:rich/a:p/a:pPr/a:defRPr", nsm);
defRPrNode.Attributes["sz"].Value = "1200";
//Get the title layout and add the manual section
var layoutNode = chartXml.SelectSingleNode("c:chartSpace/c:chart/c:title/c:layout", nsm);
var manualLayoutNode = chartXml.CreateElement("c:manualLayout", nsuri);
layoutNode.AppendChild(manualLayoutNode);
//Add coordinates
var xModeNode = chartXml.CreateElement("c:xMode", nsuri);
var attrib = chartXml.CreateAttribute("val");
attrib.Value = "edge";
xModeNode.Attributes.Append(attrib);
manualLayoutNode.AppendChild(xModeNode);
var yModeNode = chartXml.CreateElement("c:yMode", nsuri);
attrib = chartXml.CreateAttribute("val");
attrib.Value = "edge";
yModeNode.Attributes.Append(attrib);
manualLayoutNode.AppendChild(yModeNode);
var xNode = chartXml.CreateElement("c:x", nsuri);
attrib = chartXml.CreateAttribute("val");
attrib.Value = "0.9";
xNode.Attributes.Append(attrib);
manualLayoutNode.AppendChild(xNode);
var yNode = chartXml.CreateElement("c:y", nsuri);
attrib = chartXml.CreateAttribute("val");
attrib.Value = "0.95";
yNode.Attributes.Append(attrib);
manualLayoutNode.AppendChild(yNode);
pck.Save();
}
}
Which gives you this in the output:
RESPONSE TO COMMENTS
Ok, thats a little tougher. The right way would be to use a relSizeAnchor which can be placed inside the chart and moved/sized with it. But that you would have to do from scratch (or at best another library). If you activate a chart in excel and do an Insert > Text Box to see what that looks like.
Another option would be to fake it by using an unused title like say an Axis Title and moving it similar to how I did the chart title.
But the easiest option would be to simply add a shape. The draw back is if you move the chart it will not move with it:
var tb1 = worksheet.Drawings.AddShape("tb1", eShapeStyle.Rect);
tb1.Text = "ABC Company";
tb1.SetPosition(1, 0, 2, 0);
tb1.SetSize(200, 20);
tb1.Font.Color = Color.Black;
tb1.TextAlignment = eTextAlignment.Center;
tb1.Fill.Color = Color.LightYellow;
tb1.Fill.Style = eFillStyle.SolidFill;
tb1.Border.Fill.Color = Color.Red;
gives this as the output when combined with above:
Essential XlsIO can add textboxes to Excel charts.
Example code
//Accessing the chart of the worksheet
IChartShape shape = workbook.Worksheets[0].Charts[0];
//Adding textbox to chart shape
shape.TextBoxes.AddTextBox(1,1, 100,200);
//Setting position for textbox
shape.TextBoxes[0].Top = 900;
shape.TextBoxes[0].Left = 750;
//Adding text to textbox
shape.TextBoxes[0].Text = "New textbox";
The whole suite of controls is available for free (commercial applications also) through the community license program if you qualify (less than 1 million US Dollars in revenue). The community license is the full product with no limitations or watermarks.
Note: I work for Syncfusion.
If the chart already exists in your template and you are just adding data, then you can read the contents of the box from a cell on the page. Then from C# you just need to write to the cell. This lets you do any formatting or positioning you want in Excel.

C# Nested Tables in Word

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.

Setting DefinedNames in OpenXML to set Print Titles for all worksheets in Excel

My end aim is to programatically set Excel's "Print Titles" value of Page Setup for all worksheets in my document.
Initially I tried looking at using the SpreadsheetPrintingParts object (based on this question) - however, that requires generating a base 64 string, which seemingly has to come from an exisiting file. (I am generating my spreadsheet from scratch.)
This post then taught me that I could set "Print_Titles" as a defined name on the row I needed instead. I've been attempting to do that programatically, but this seems to corrupt all my files.
My code:
var definedNamesCol = new DefinedNames(); //Create the collection
var definedName = new DefinedName() { Name = "_xlnm.Print_Titles", Text = "\'SheetName\'!$2:$2", LocalSheetId = (UInt32) (_nextSheetId - 1) }; // Create a new range
definedNamesCol.Append(definedName); // Add it to the collection
_workbookPart.Workbook.Append(definedNamesCol);
I've also looked with the OpenXML productivity tool which suggests: (Essentially identical)
DefinedNames definedNames1 = new DefinedNames();
DefinedName definedName1 = new DefinedName(){ Name = "_xlnm.Print_Titles", LocalSheetId = (UInt32Value)0U };
definedName1.Text = "\'SheetName\'!$2:$2";
definedNames1.Append(definedName1)
I've also tried setting the Xlm property on DefinedName but the file then opens with an error that it contains Macro's in a Macro-Free file, which isn't what I think I want to do.
A (simplified) version of what I'm generating in workbook.xml:
<?xml version="1.0" encoding="utf-8"?>
<x:workbook xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<x:sheets>
<x:sheet name="ABBEY" sheetId="1" r:id="R2f5447238bc94fa4" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />
</x:sheets>
<x:definedNames>
<x:definedName name="_xlnm.Print_Titles" localSheetId="0">'SheetName'!$2:$2</x:definedName>
</x:definedNames>
</x:workbook>
Is there a better way to approach the problem? Or is my intention right, and it's a misunderstanding of the method somewhere else?
The above code was within a CreateWorksheet method, so was being called for each sheet. In the resulting workbook.xml file, this was then creating multiple definedNames objects, when there should only be a single definedNames object containing multiple definedNames.
I resolved the problem using this code:
var definedName = new DefinedName() { Name = "_xlnm.Print_Titles", Text = "\'Sheet Name\'!$2:$2", LocalSheetId = (UInt32) (_nextSheetId - 1) }; // Create a new range
if (_workbookPart.Workbook.DefinedNames == null)
{
var definedNamesCol = new DefinedNames();
_workbookPart.Workbook.Append(definedNamesCol);
}
_workbookPart.Workbook.DefinedNames.Append(definedName); // Add it to the collection

How to Define a PDF Outline Using MigraDoc

I noticed when using MigraDoc that if I add a paragraph with any of the heading styles (e.g., "Heading1"), an entry is automatically placed in the document outline. My question is, how can I add entries in the document outline without showing the text in the document? Here is an example of my code:
var document = new Document();
var section = document.AddSection();
// The following line adds an entry to the document outline, but it also
// adds a line of text to the current section. How can I add an
// entry to the document outline without adding any text to the page?
var paragraph = section.AddParagraph("TOC Level 1", "Heading1");
I used a hack: added white text on white ground with a font size of 0.001 or so to get outlines that are actually invisible to the user.
For a perfect solution, mix PDFsharp and MigraDoc code. The hack works for me and is much easier to implement.
I realized after reading ThomasH's answer that I am already mixing PDFSharp and MigraDoc code. Since I am utilizing a PdfDocumentRenderer, I was able to add a custom outline to the PdfDocument property of that renderer. Here is an example of what I ended up doing to create a custom outline:
var document = new Document();
// Populate the MigraDoc document here
...
// Render the document
var renderer = new PdfDocumentRenderer(false, PdfFontEmbedding.Always)
{
Document = document
};
renderer.RenderDocument();
// Create the custom outline
var pdfSharpDoc = renderer.PdfDocument;
var rootEntry = pdfSharpDoc.Outlines.Add(
"Level 1 Header", pdfSharpDoc.Pages[0]);
rootEntry.Outlines.Add("Level 2 Header", pdfSharpDoc.Pages[1]);
// Etc.
// Save the document
pdfSharpDoc.Save(outputStream);
I've got a method that is slightly less hacked. Here's the basic method:
1) Add a bookmark, save into a list that bookmark field object and the name of the outline entry. Do not set a paragraph .OutlineLevel (or set as bodytext)
// Defined previously
List<dynamic> Bookmarks = new List<dynamic>();
// In your bookmarking method, P is a Paragraph already created somewhere
Bookmarks.Add(new { Bookmark = P.AddBookmark("C1"), Name = "Chapter 1", Depth = 0 });
2) At the end of your Migradoc layout, before rendering, prepare the pages
pdfwriter.PrepareRenderPages();
3) Build a dictionary of the Bookmark's parent's parent (This will be a paragraph) and pages (pages will be initialized to -1)
var Pages = Bookmarks.Select(x=> ((BookmarkField)x).Bookmark.Parent.Parent).ToDictionary(x=>x, x=>-1);
4) Now fill in those pages by iterating through the objects on each page, finding the match
for (int i = 0; i < pdfwriter.PageCount; i++)
foreach (var s in pdfwriter.DocumentRenderer.GetDocumentObjectsFromPage(i).Where(x=> Pages.ContainsKey(x))
Pages[s] = i-1;
5) You've now got a dictionary of Bookmark's parent's parents to page numbers, with this you can add your outlines directly into the PDFSharp document. This also iterates down the depth-tree, so you can have nested outlines
foreach(dynamic d in Bookmarks)
{
var o = pdfwriter.PdfDocument.Outlines;
for(int i=0;i<d.Depth;i++)
o = o.Last().Outlines;
BookmarkField BK = d.Bookmark;
int PageNumber = Pages[BK.Parent.Parent];
o.Add(d.Name, pdfwriter.PdfDocument.Pages[PageNumber], true, PdfOutlineStyle.Regular);
}

Categories

Resources