diff --git a/src/EPPlus/CellPictures/CellPicturesManager.cs b/src/EPPlus/CellPictures/CellPicturesManager.cs index 158b5eb22..9cc2a526d 100644 --- a/src/EPPlus/CellPictures/CellPicturesManager.cs +++ b/src/EPPlus/CellPictures/CellPicturesManager.cs @@ -358,6 +358,11 @@ private void AddNewWebPicture(int row, int col, Uri imageUri, Uri addressUri, st } } + internal void ReadAndAddReference(PictureCacheKey key, uint vmId) + { + _referenceCache.Add(key, vmId); + } + private void AddReferenceToPicture(int row, int col, PictureCacheKey key, uint vmId) { var rv = _richDataStore.GetRichValue(vmId); diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs index feb80bdd6..ce0cf5296 100644 --- a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs @@ -44,7 +44,7 @@ public ExcelChartSerieDataLabel DataLabel { if (_dataLabel == null) { - if (ExcelChartDataLabelStandard.ForbiddDataLabelPosition(_chart) == false) + if (ExcelChartDataLabelStandard.IsDataLabelPositionForbidden(_chart) == false) { _dataLabel = new ExcelChartSerieDataLabel(_chart, NameSpaceManager, TopNode, SchemaNodeOrder); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs index 1b6cec48d..7e5300bfb 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs @@ -26,7 +26,7 @@ public abstract class ExcelChartDataLabel : XmlHelper, IDrawingStyle { internal ExcelChart _chart; internal string _nodeName; - private string _nsPrefix; + internal protected string NsPrefix { private set; get; } private readonly string _formatPath; private readonly string _sourceLinkedPath; @@ -35,7 +35,7 @@ internal ExcelChartDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode n { _nodeName = nodeName; _chart = chart; - _nsPrefix = nsPrefix; + NsPrefix = nsPrefix; _formatPath = $"{nsPrefix}:numFmt/@formatCode"; _sourceLinkedPath = $"{nsPrefix}:numFmt/@sourceLinked"; } @@ -157,7 +157,7 @@ public ExcelDrawingFill Fill { if (_fill == null) { - _fill = new ExcelDrawingFill(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr", SchemaNodeOrder); + _fill = new ExcelDrawingFill(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr", SchemaNodeOrder); } return _fill; } @@ -172,7 +172,7 @@ public ExcelDrawingBorder Border { if (_border == null) { - _border = new ExcelDrawingBorder(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr/a:ln", SchemaNodeOrder); + _border = new ExcelDrawingBorder(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr/a:ln", SchemaNodeOrder); } return _border; } @@ -187,7 +187,7 @@ public ExcelDrawingEffectStyle Effect { if (_effect == null) { - _effect = new ExcelDrawingEffectStyle(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr/a:effectLst", SchemaNodeOrder); + _effect = new ExcelDrawingEffectStyle(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr/a:effectLst", SchemaNodeOrder); } return _effect; } @@ -202,7 +202,7 @@ public ExcelDrawing3D ThreeD { if (_threeD == null) { - _threeD = new ExcelDrawing3D(NameSpaceManager, TopNode, $"{_nsPrefix}:spPr", SchemaNodeOrder); + _threeD = new ExcelDrawing3D(NameSpaceManager, TopNode, $"{NsPrefix}:spPr", SchemaNodeOrder); } return _threeD; } @@ -218,7 +218,7 @@ public ExcelTextFont Font { if (_font == null) { - _font = new ExcelTextFont(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder, CreateDefaultText); + _font = new ExcelTextFont(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder, CreateDefaultText); } return _font; } @@ -233,7 +233,7 @@ public ExcelDrawingTextSettings TextSettings { if (_textSettings == null) { - _textSettings = new ExcelDrawingTextSettings(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder); + _textSettings = new ExcelDrawingTextSettings(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder); } return _textSettings; } @@ -245,14 +245,14 @@ void IDrawingStyleBase.CreatespPr() private void CreateDefaultText() { - if (TopNode.SelectSingleNode($"{_nsPrefix}:txPr", NameSpaceManager) == null) + if (TopNode.SelectSingleNode($"{NsPrefix}:txPr", NameSpaceManager) == null) { - if (!ExistsNode($"{_nsPrefix}:spPr")) + if (!ExistsNode($"{NsPrefix}:spPr")) { - var spNode = CreateNode($"{_nsPrefix}:spPr"); + var spNode = CreateNode($"{NsPrefix}:spPr"); spNode.InnerXml = ""; } - var node = CreateNode($"{_nsPrefix}:txPr"); + var node = CreateNode($"{NsPrefix}:txPr"); node.InnerXml = ""; } @@ -268,7 +268,7 @@ public ExcelTextBody TextBody { if (_textBody == null) { - _textBody = new ExcelTextBody(NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, Font.CreateTopNode); + _textBody = new ExcelTextBody(NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, Font.CreateTopNode); } return _textBody; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs index 8b2b2a0a2..613500be9 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs @@ -13,7 +13,8 @@ Date Author Change using System; using System.Collections; using System.Collections.Generic; -using System.Reflection.Emit; +using System.ComponentModel; +using System.Data; using System.Xml; namespace OfficeOpenXml.Drawing.Chart @@ -31,14 +32,25 @@ internal ExcelChartDataLabelCollection(ExcelChart chart, XmlNamespaceManager ns, { SchemaNodeOrder = schemaNodeOrder; _list = new List(); - foreach (XmlNode dataLabelNode in TopNode.SelectNodes("c:dLbl", ns)) + var existingDataLabelNodes = TopNode.SelectNodes("c:dLbl", ns); + foreach (XmlNode dataLabelNode in existingDataLabelNodes) { _list.Add(new ExcelChartDataLabelItem(chart, ns, dataLabelNode, "", schemaNodeOrder)); } parentDatalabel = parent; _chart = chart; + } + + internal void InitializeDataLabelsXml() + { + if(_list.Count == 0) + { + var seriesNode = TopNode.ParentNode; + } + } + /// /// Adds a new chart label to the collection /// @@ -72,7 +84,7 @@ private ExcelChartDataLabelItem CreateDataLabel(int idx) dl.ShowLegendKey = parentDatalabel.ShowLegendKey; dl.ShowLeaderLines = true; dl.ShowValue = true; - dl.Position = eLabelPosition.Center; + dl.Position = parentDatalabel.Position; if (idx < _list.Count) { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs index 8e0d06e5c..417c05e04 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs @@ -10,6 +10,10 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Style; +using System.Collections.Generic; using System.Globalization; using System.Xml; @@ -20,10 +24,12 @@ namespace OfficeOpenXml.Drawing.Chart /// public class ExcelChartDataLabelItem : ExcelChartDataLabelStandard { + string _fontPropertiesPath = ""; internal ExcelChartDataLabelItem(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string nodeName, string[] schemaNodeOrder) : base(chart, ns, node, nodeName, schemaNodeOrder) { Layout = new ExcelLayout(NameSpaceManager, TopNode, $"c:layout","c:extLst/c:ext[1]/c15:layout", SchemaNodeOrder); + _fontPropertiesPath = $"{NsPrefix}:tx/{NsPrefix}:rich"; } /// @@ -31,6 +37,37 @@ internal ExcelChartDataLabelItem(ExcelChart chart, XmlNamespaceManager ns, XmlNo /// public ExcelLayout Layout { get; private set; } + ExcelParagraphCollection _paragraphs = null; + + /// + /// Access to text body properties + /// + private ExcelParagraphCollection ParagraphCollection + { + get + { + if (_paragraphs == null) + { + _paragraphs = new ExcelParagraphCollection(_chart, NameSpaceManager, TopNode, _fontPropertiesPath + "/a:p", SchemaNodeOrder); + } + return _paragraphs; + } + } + + /// + /// Replace datalabel text + /// + /// + public void SetText(string replacementText) + { + ParagraphCollection.Clear(); + ParagraphCollection.Add(replacementText, true); + } + + internal List> GetExistingParagraphStrings() + { + return ParagraphCollection.GetParagraphTextLists(); + } /// /// The index of an individual datalabel /// @@ -45,5 +82,7 @@ public int Index SetXmlNodeString("c:idx/@val", value.ToString(CultureInfo.InvariantCulture)); } } + + internal ExcelAddressBase SingleCellAddressFromSeries; } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs index 5552436b1..0bbe7e621 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs @@ -77,7 +77,10 @@ internal ExcelChartDataLabelStandard(ExcelChart chart, XmlNamespaceManager ns, X const string positionPath = "c:dLblPos/@val"; /// /// Position of the labels - /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns + ///
BE AWARE! For SERIES labels and all underlying labels:
+ /// Setting a position not available for this label in the Excel UI May cause a corrupt file.
+ /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns
+ /// (Same applies to most BarCharts but they allow OutEnd) ///
public override eLabelPosition Position { @@ -87,14 +90,19 @@ public override eLabelPosition Position } set { - if (ForbiddDataLabelPosition(_chart)) + if (IsDataLabelPositionForbidden(_chart)) { throw new InvalidOperationException("Can't set data label position on a 3D-chart"); } + if(_chart.ChartType == eChartType.ColumnClustered && value == eLabelPosition.Top) + { + throw new InvalidOperationException($"DataLabelPosition: '{value}' is not allowed on chart of type: '{_chart.ChartType}' \n " + + $"because it would cause a corrupt file"); + } SetXmlNodeString(positionPath, GetPosText(value)); } } - internal static bool ForbiddDataLabelPosition(ExcelChart _chart) + internal static bool IsDataLabelPositionForbidden(ExcelChart _chart) { return _chart.IsType3D() && !_chart.IsTypePie() && _chart.ChartType != eChartType.Line3D || _chart.IsTypeDoughnut(); @@ -144,7 +152,7 @@ public override bool ShowSeriesName SetXmlNodeString(showSerPath, value ? "1" : "0"); } } - const string showPerentPath = "c:showPercent/@val"; + const string showPercentPath = "c:showPercent/@val"; /// /// Show percent values /// @@ -152,11 +160,11 @@ public override bool ShowPercent { get { - return GetXmlNodeBool(showPerentPath); + return GetXmlNodeBool(showPercentPath); } set { - SetXmlNodeString(showPerentPath, value ? "1" : "0"); + SetXmlNodeString(showPercentPath, value ? "1" : "0"); } } const string showLeaderLinesPath = "c:showLeaderLines/@val"; @@ -254,5 +262,28 @@ public override string Separator } } } + + internal void AddExtFieldTableEmpty() + { + CreateNode($"{extPath}/c15:dlblFieldTable"); + } + + + internal bool ShowDatalabelsRange + { + get + { + return GetXmlNodeBool($"{extPath}/c15:showDataLabelsRange"); + } + set + { + var rangePath = $"{extPath}/c15:showDataLabelsRange"; + if(ExistsNode(rangePath) == false) + { + CreateNode(rangePath); + } + SetXmlNodeBool(rangePath+"/@val", value); + } + } } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs index ce8b6c61e..c9bfa044e 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs @@ -10,13 +10,17 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing.Interfaces; +using OfficeOpenXml.Drawing.Style.Effect; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.Style; using System; using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net; using System.Text; using System.Xml; -using OfficeOpenXml.Drawing.Interfaces; -using OfficeOpenXml.Drawing.Style.Effect; -using OfficeOpenXml.Style; namespace OfficeOpenXml.Drawing.Chart { @@ -26,9 +30,16 @@ namespace OfficeOpenXml.Drawing.Chart public sealed class ExcelChartSerieDataLabel : ExcelChartDataLabelStandard { internal ExcelChartSerieDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string[] schemaNodeOrder) - : base(chart, ns,node,"dLbls", schemaNodeOrder) + : base(chart, ns, node, "dLbls", schemaNodeOrder) { Position = eLabelPosition.Center; + var parentSeries = GetParentSeries(); + + var address = parentSeries.GetDataLabelRange(); + if (string.IsNullOrEmpty(address) == false) + { + DataLabelRange = chart.WorkSheet.Cells[address]; + } } ExcelChartDataLabelCollection _dataLabels = null; /// @@ -41,9 +52,111 @@ public ExcelChartDataLabelCollection DataLabels if (_dataLabels == null) { _dataLabels = new ExcelChartDataLabelCollection(_chart, NameSpaceManager, TopNode, SchemaNodeOrder, this as ExcelChartDataLabelStandard); + + //Fill datalabel addresses + if(DataLabelRange != null) + { + var address = DataLabelRange; + for(int i = 0; i< _dataLabels.Count(); i++) + { + if (address.Rows > address.Columns) + { + _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(i, 0); + } + else + { + _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(0, i); + } + } + } } return _dataLabels; } } + + /// + /// Does the datalabels of this chart contain + /// Value From Cells + /// + public bool ValueFromCells { get { return DataLabelRange != null; } } + + internal ExcelRangeBase DataLabelRange { get; private set; } = null; + + + ExcelChartStandardSerie GetParentSeries() + { + //TODO: The way we aquire the Series instance here is clumsy. + //Fix as part of datalabel refactor? + //Perhaps the series of a series label should be part of its constructor. + //Or use an eventhandler + //For a single case however that feels overkill. + + //Has to get the series index: + var idxNode = (XmlElement)TopNode.ParentNode.SelectSingleNode($"{NsPrefix}:idx", NameSpaceManager); + var idxNodeValue = int.Parse(idxNode.GetAttribute("val")); + //Get the series this datalabel is on + return (ExcelChartStandardSerie)_chart.Series[idxNodeValue]; + } + + /// + /// Select datalabel range for + /// Value From Cells + /// + /// must be a single; cell, row or column + /// Thrown when input is not a cell, a row or a column + public void SelectRange(ExcelRangeBase address) + { + //TODO: Arguably this is just another series with a series cache. + //Same as Cat or Val except that it is added in Ext on the Serie node + //ShowValue property essentially changes the datalabels in the same way. + //This could be unified somehow so that all serie ranges; Cat, Val and DataLabelRange are handled the same way. + + bool moreThanOneRow = address.Rows > 1; + bool moreThanOneColumn = address.Columns > 1; + + if (moreThanOneRow && moreThanOneColumn) + { + throw new InvalidExpressionException($"DataLabelRange cannot be set to invalid range: '{address.Address}'\n" + + $"The range must be a single cell, a single row or a single column"); + } + + DataLabelRange = address; + + var currentSeries = GetParentSeries(); + //Set the ext data needed in the Series node + currentSeries.SetDataLabelRange(address); + + //Create the Datalabels if they do not exist + if (DataLabels.Count < currentSeries.NumberOfItems) + { + for (int i = 0; i < currentSeries.NumberOfItems; i++) + { + ExcelChartDataLabelItem currentLabel; + if (DataLabels.Count - 1 < i) + { + currentLabel = DataLabels.Add(i); + } + else + { + currentLabel = DataLabels[i]; + } + currentLabel.AddExtFieldTableEmpty(); + currentLabel.ShowDatalabelsRange = true; + + if (address.Rows > address.Columns) + { + currentLabel.SingleCellAddressFromSeries = address.TakeSingleCell(i, 0); + } + else + { + currentLabel.SingleCellAddressFromSeries = address.TakeSingleCell(0, i); + } + + ////Adds field CellRange to the paragraph of the label + ///For backwards compatability if opened in excel versions prior to Excel 2013 + //currentLabel.AddField("CELLRANGE"); + } + } + } } -} +} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs index 06c03b88d..52c5c6cc2 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs @@ -19,6 +19,8 @@ Date Author Change using System.Globalization; using System.Runtime.CompilerServices; using System.IO; +using OfficeOpenXml.FormulaParsing.Utilities; + namespace OfficeOpenXml.Drawing.Chart { /// @@ -28,26 +30,28 @@ public class ExcelChartStandardSerie : ExcelChartSerie { private readonly bool _isPivot; - double[] _NumberLiteralsY = null; double[] _NumberLiteralsX = null; string[] _StringLiteralsX = null; string[] _StringLiteralsY = null; + const string extPath = "c:extLst/c:ext"; + const string dlblRangePath = "c:extLst/c:ext/c15:datalabelsRange"; + /// /// Literals for the Y serie, if the literal values are numeric /// - public override double[] NumberLiteralsY - { - get + public override double[] NumberLiteralsY + { + get { - if(string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null) + if (string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null) { ReadNumLiterals(_seriesNumLitPath, out _NumberLiteralsY); return _NumberLiteralsY; } return _NumberLiteralsY; - } + } protected set { _NumberLiteralsY = value; @@ -56,7 +60,7 @@ protected set /// /// Literals for the X serie, if the literal values are numeric /// - public override double[] NumberLiteralsX + public override double[] NumberLiteralsX { get { @@ -121,25 +125,25 @@ protected set /// Is pivotchart internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, bool isPivot) : base(chart, ns, node) - { - _chart = chart; - _isPivot = isPivot; - SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline","errBars", "cat", "val", "xVal", "yVal", "smooth","shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" }; - - if (_chart.ChartNode.LocalName=="scatterChart" || - _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase)) - { - _seriesTopPath = "c:yVal"; - _xSeriesTopPath = "c:xVal"; - } - else - { - _seriesTopPath = "c:val"; - _xSeriesTopPath = "c:cat"; - } - - _seriesPath = string.Format(_seriesPath, _seriesTopPath); - _numCachePath = string.Format(_numCachePath, _seriesTopPath); + { + _chart = chart; + _isPivot = isPivot; + SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline", "errBars", "cat", "val", "xVal", "yVal", "smooth", "shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" }; + + if (_chart.ChartNode.LocalName == "scatterChart" || + _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase)) + { + _seriesTopPath = "c:yVal"; + _xSeriesTopPath = "c:xVal"; + } + else + { + _seriesTopPath = "c:val"; + _xSeriesTopPath = "c:cat"; + } + + _seriesPath = string.Format(_seriesPath, _seriesTopPath); + _numCachePath = string.Format(_numCachePath, _seriesTopPath); var np = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:numRef"); var sp = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:strRef"); @@ -157,54 +161,118 @@ internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNo _xSeriesStrLitPath = string.Format("{0}/c:strLit", _xSeriesTopPath); _xSeriesNumLitPath = string.Format("{0}/c:numLit", _xSeriesTopPath); - } - internal override void SetID(string id) - { - SetXmlNodeString("c:idx/@val",id); - SetXmlNodeString("c:order/@val", id); - } - const string headerPath="c:tx/c:v"; - /// - /// Header for the serie. - /// - public override string Header - { - get - { + } + internal override void SetID(string id) + { + SetXmlNodeString("c:idx/@val", id); + SetXmlNodeString("c:order/@val", id); + } + + internal bool HasDataLabelRange() + { + return ExistsNode(dlblRangePath); + } + + internal string GetDataLabelRange() + { + if(ExistsNode($"{dlblRangePath}/c15:f")) + { + return GetXmlNodeString($"{dlblRangePath}/c15:f"); + } + return null; + } + + internal void AddExtLstXml() + { + NameSpaceManager.AddNamespace("c15", ExcelPackage.schemaChart2012); + NameSpaceManager.AddNamespace("c16", ExcelPackage.schemaChart2014); + + XmlElement ext15Node; + + var c15Uri = "{02D57815-91ED-43cb-92C2-25804820EDAC}"; + + //Only add node if it doesn't already exist + if (ExistsNode(extPath + $"[@uri='{c15Uri}']") == false) + { + XmlElement el = (XmlElement)CreateNode($"{extPath}"); + el.SetAttribute("xmlns:c15", ExcelPackage.schemaChart2012); + SetXmlNodeString($"{extPath}/@uri", $"{c15Uri}"); + ext15Node = el; + } + else + { + ext15Node = (XmlElement)GetNode($"{extPath}"); + } + + //Only add node if it doesn't already exist + if (ExistsNode($"{extPath}[2]") == false) + { + XmlElement element = (XmlElement)CreateNode($"{extPath}", false, true); + element.SetAttribute("xmlns:c16", ExcelPackage.schemaChart2014); + SetXmlNodeString($"{extPath}[2]/@uri", "{C3380CC4-5D6E-409C-BE32-E72D297353CC}"); + var _guidId = Guid.NewGuid(); + + var extNode2 = GetNode($"{extPath}[2]"); + var uniqueIdNode = (XmlElement)CreateNode(extNode2, "c16:uniqueID"); + uniqueIdNode.SetAttribute("val", $"{{{_guidId}}}"); + } + } + + + internal void SetDataLabelRange(ExcelRangeBase address) + { + AddExtLstXml(); + + var datalabelsRange = CreateNode(dlblRangePath); + var formulaNode = CreateNode($"{dlblRangePath}/c15:f"); + formulaNode.InnerText = address.AddressAbsolute; + + var rangeNode = CreateNode($"{dlblRangePath}/c15:dlblRangeCache"); + CreateCache(address.FullAddressAbsolute, rangeNode); + } + + const string headerPath = "c:tx/c:v"; + /// + /// Header for the serie. + /// + public override string Header + { + get + { return GetXmlNodeString(headerPath); } set { Cleartx(); - SetXmlNodeString(headerPath, value); + SetXmlNodeString(headerPath, value); } } - private void Cleartx() - { - var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager); - if (n != null) - { - n.InnerXml = ""; - } - } - const string headerAddressPath = "c:tx/c:strRef/c:f"; + private void Cleartx() + { + var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager); + if (n != null) + { + n.InnerXml = ""; + } + } + const string headerAddressPath = "c:tx/c:strRef/c:f"; /// - /// Header address for the serie. - /// - public override ExcelAddressBase HeaderAddress - { - get - { - string address = GetXmlNodeString(headerAddressPath); - if (address == "") - { - return null; - } - else - { - return new ExcelAddressBase(address); - } + /// Header address for the serie. + /// + public override ExcelAddressBase HeaderAddress + { + get + { + string address = GetXmlNodeString(headerAddressPath); + if (address == "") + { + return null; + } + else + { + return new ExcelAddressBase(address); + } } set { @@ -217,7 +285,7 @@ public override ExcelAddressBase HeaderAddress SetXmlNodeString(headerAddressPath, ExcelCellBase.GetFullAddress(value.WorkSheetName, value.Address)); SetXmlNodeString("c:tx/c:strRef/c:strCache/c:ptCount/@val", "0"); } - } + } string _seriesTopPath; string _seriesPath = "{0}/c:numRef/c:f"; string _numCachePath = "{0}/c:numRef/c:numCache"; @@ -227,18 +295,18 @@ public override ExcelAddressBase HeaderAddress /// public override string Series { - get - { - return GetXmlNodeString(_seriesPath); - } - set - { + get + { + return GetXmlNodeString(_seriesPath); + } + set + { value = value.Trim(); if (value.StartsWith("=", StringComparison.OrdinalIgnoreCase)) value = value.Substring(1); if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase)) { GetLitValues(value, out double[] numLit, out string[] strLit); - if(strLit!=null) + if (strLit != null) { throw (new ArgumentException("Value series can't contain strings")); } @@ -255,22 +323,22 @@ public override string Series } - string _xSeries=null; - string _xSeriesTopPath; - string _xSeriesParentPath = "{0}/{1}"; - string _xSeriesPath = "{0}/{1}/c:f"; - string _xSeriesStrLitPath, _xSeriesNumLitPath; + string _xSeries = null; + string _xSeriesTopPath; + string _xSeriesParentPath = "{0}/{1}"; + string _xSeriesPath = "{0}/{1}/c:f"; + string _xSeriesStrLitPath, _xSeriesNumLitPath; /// /// Set an address for the horisontal labels /// - public override string XSeries - { - get - { - return GetXmlNodeString(_xSeriesPath); - } - set - { + public override string XSeries + { + get + { + return GetXmlNodeString(_xSeriesPath); + } + set + { _xSeries = value.Trim(); if (_xSeries.StartsWith("=", StringComparison.OrdinalIgnoreCase)) _xSeries = _xSeries.Substring(1); if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase)) @@ -285,7 +353,7 @@ public override string XSeries NumberLiteralsX = null; StringLiteralsX = null; CreateNode(_xSeriesPath, true); - if(ExcelCellBase.IsValidAddress(_xSeries)) + if (ExcelCellBase.IsValidAddress(_xSeries)) { SetXmlNodeString(_xSeriesPath, ExcelCellBase.GetFullAddress(_chart.WorkSheet.Name, _xSeries)); } @@ -296,7 +364,7 @@ public override string XSeries SetXSerieFunction(); } } - } + } private void ReadNumLiterals(string path, out double[] numberLiterals) { @@ -306,9 +374,9 @@ private void ReadNumLiterals(string path, out double[] numberLiterals) foreach (XmlNode node in childNodes) { - if(node.NodeType==XmlNodeType.Element && node.LocalName == "pt") + if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt") { - if(double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false) + if (double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false) { throw new InvalidDataException($"numberLiteral in xml node:'{node.Name}' in chart:'{_chart.Name}' with value:'{node.InnerText}' could not be parsed as double. Chart cannot be read."); } @@ -323,7 +391,7 @@ private void ReadStringLiterals(string path, out string[] stringLiterals) var parentNode = GetNode(path); List strLits = new(); - if(parentNode != null) + if (parentNode != null) { var childNodes = parentNode.ChildNodes; @@ -465,12 +533,12 @@ private void SetXSerieFunction() } private void SetLits(double[] numLit, string[] strLit, string numLitPath, string strLitPath) { - if(strLit!=null) + if (strLit != null) { XmlNode lit = CreateNode(strLitPath); SetLitArray(lit, strLit); } - else if(numLit!=null) + else if (numLit != null) { XmlNode lit = CreateNode(numLitPath); SetLitArray(lit, numLit); @@ -506,7 +574,7 @@ private void SetLitArray(XmlNode lit, string[] strLit) { //Remove previous child nodes var previousPt = lit.SelectNodes("c:pt", NameSpaceManager); - if(previousPt != null) + if (previousPt != null) { for (int i = 0; i < previousPt.Count; i++) { @@ -535,9 +603,9 @@ private void AddCount(XmlNode lit, int count) } ExcelChartTrendlineCollection _trendLines = null; - /// - /// Access to the trendline collection - /// + /// + /// Access to the trendline collection + /// public override ExcelChartTrendlineCollection TrendLines { get @@ -556,7 +624,7 @@ public override int NumberOfItems { get { - if(ExcelCellBase.IsValidAddress(Series)) + if (ExcelCellBase.IsValidAddress(Series)) { var a = new ExcelAddressBase(Series); return a.Rows; @@ -574,16 +642,16 @@ public override int NumberOfItems /// public void CreateCache() { - if (_isPivot) throw(new NotImplementedException("Cache for pivotcharts has not been implemented yet.")); + if (_isPivot) throw (new NotImplementedException("Cache for pivotcharts has not been implemented yet.")); if (!string.IsNullOrEmpty(Series)) { - if(new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1) + if (new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1) { throw (new InvalidOperationException("A serie cannot be multiple columns. Please add one serie per column to create a cache")); } var node = GetTopNode(Series, _seriesTopPath); - + CreateCache(Series, node); } @@ -598,8 +666,9 @@ public void CreateCache() CreateCache(XSeries, node); } + } - private void CreateCache(string address, XmlNode node) + internal void CreateCache(string address, XmlNode node) { //var ws = _chart.WorkSheet; var wb = _chart.WorkSheet.Workbook; @@ -633,7 +702,7 @@ private void CreateCache(string address, XmlNode node) } CreateCacheFromRange(node, ws.Cells[address]); } - + } private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range) @@ -641,17 +710,27 @@ private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range) if (range == null) return; var startRow = range._fromRow; var items = 0; - var cse = new CellStoreEnumerator(range.Worksheet._values, startRow,range._fromCol, range._toRow, range._toCol); + var cse = new CellStoreEnumerator(range.Worksheet._values, startRow, range._fromCol, range._toRow, range._toCol); while (cse.Next()) { var v = cse.Value._value; if (v != null) { - var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + string xmlValue = ""; + if (v.IsNumeric()) + { + var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904); + } + else + { + xmlValue = string.Format(CultureInfo.InvariantCulture, v.ToString()); + } + var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); node.AppendChild(ptNode); ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture)); - ptNode.InnerXml = $"{Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904)}"; + ptNode.InnerXml = $"{xmlValue}"; items++; } } @@ -700,10 +779,10 @@ private XmlNode GetTopNode(string address, string seriesTopPath) if (addr.IsExternal) { var erIx = wb.ExternalLinks.GetExternalLink(addr._wb); - if(erIx>=0) + if (erIx >= 0) { var er = wb.ExternalLinks[erIx].As.ExternalWorkbook; - if(er.Package!=null) + if (er.Package != null) { var ws = er.Package.Workbook.Worksheets[addr.WorkSheetName]; var range = ws.Cells[addr.LocalAddress]; @@ -712,7 +791,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) else { var ws = er.CachedWorksheets[addr.WorkSheetName]; - if(ws==null) + if (ws == null) { v = null; } @@ -728,7 +807,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) v = null; } } - else + else { ExcelWorksheet ws; if (string.IsNullOrEmpty(addr.WorkSheetName)) @@ -752,22 +831,22 @@ private XmlNode GetTopNode(string address, string seriesTopPath) string cachePath; bool isNum; - if(Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null) + if (Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null) { cachePath = string.Format("{0}/c:numRef/c:numCache", seriesTopPath); isNum = true; } else { - cachePath=string.Format("{0}/c:strRef/c:strCache", seriesTopPath); + cachePath = string.Format("{0}/c:strRef/c:strCache", seriesTopPath); isNum = false; } var node = CreateNode(cachePath); if (node.HasChildNodes) { - if(isNum) + if (isNum) { - if(node.FirstChild.LocalName== "formatCode") + if (node.FirstChild.LocalName == "formatCode") { node.InnerXml = node.FirstChild.OuterXml; } @@ -778,7 +857,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) } else { - node.InnerXml = ""; + node.InnerXml = ""; } } CreateNode($"{cachePath}/c:ptCount"); @@ -796,14 +875,14 @@ internal static XmlElement CreateSerieElement(ExcelChart chart) //If the chart is added from a chart template, then use the chart templates series xml if (chart._drawings._seriesTemplateXml != null) { - if(chart._drawings._seriesTemplateXml.Count != 0) + if (chart._drawings._seriesTemplateXml.Count != 0) { ser.InnerXml = chart._drawings._seriesTemplateXml[0]; return ser; } } - int idx = FindIndex(chart._topChart??chart); + int idx = FindIndex(chart._topChart ?? chart); ser.InnerXml = string.Format("{2}{5}{0}{3}{4}", AddExplosion(chart.ChartType), idx, AddSpPrAndScatterPoint(chart.ChartType), AddAxisNodes(chart.ChartType), AddSmooth(chart.ChartType), AddMarker(chart.ChartType)); return ser; } diff --git a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs index f1e5f8ad6..d5f78979d 100644 --- a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs @@ -10,13 +10,14 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing.Interfaces; using System; using System.Collections.Generic; +using System.Data; +using System.Drawing; using System.Globalization; using System.Text; using System.Xml; -using System.Drawing; -using OfficeOpenXml.Drawing.Interfaces; namespace OfficeOpenXml.Drawing.Chart { diff --git a/src/EPPlus/Style/RichText/ExcelParagraph.cs b/src/EPPlus/Style/RichText/ExcelParagraph.cs index 841314f29..e90136448 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraph.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraph.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Interfaces; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System; using System.Collections.Generic; using System.Text; @@ -30,6 +31,7 @@ internal ExcelParagraph(IPictureRelationDocument pictureRelationDocument, XmlNam } const string TextPath = "../a:t"; + const string FldPath = "../a:fld"; /// /// Text /// diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index 0c75e93be..841b5081a 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -10,14 +10,15 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing; using System; using System.Collections.Generic; -using System.Text; -using System.Xml; -using OfficeOpenXml.Drawing; using System.Drawing; -using System.Linq; using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; namespace OfficeOpenXml.Style { @@ -213,6 +214,30 @@ public string Text } } } + + internal List> GetParagraphTextLists() + { + List> strings = new List>(); + var pars = TopNode.SelectNodes(_path, NameSpaceManager); + + foreach(XmlNode paragraph in pars) + { + List paragraphTexts = new List(); + foreach (XmlNode node in paragraph.ChildNodes) + { + if (node.LocalName == "fld" || node.LocalName == "r") + { + var textNode = node.SelectSingleNode("a:t", NameSpaceManager); + var text = textNode.InnerText; + paragraphTexts.Add(text); + } + } + strings.Add(paragraphTexts); + } + + return strings; + } + #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() diff --git a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs index f6cea68a8..eb4ae0bd4 100644 --- a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs +++ b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs @@ -249,5 +249,169 @@ public void SimpleChartDataLabels() SaveAndCleanup(p); } } + + [TestMethod] + //TODO: This test is one instance of a larger problem + //Many datalabels have different allowed positions depending on chart type + //Going against it will often create corrupt files. + //See microsoft offical documentation: + //"MS-OE376" page 659 2.1.1475 Part 4 Section 5.7.2.48, dLblPos (Data Label Position) for details. + public void TopIsDisallowedOnBarDataLabels() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("DataLabelSheet"); + + ws.Cells["A1"].Value = "Week"; + ws.Cells["B1"].Value = "Income"; + + ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)"; + ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7"; + ws.Calculate(); + + var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered); + chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]); + + var SeriesDataLabel = chart.Series[0].DataLabel; + + Assert.Throws(() => SeriesDataLabel.Position = eLabelPosition.Top); + } + } + + [TestMethod] + public void CreateFileWithDataLabelsManualAndGeneral() + { + using (var p = OpenPackage("dlblMissMatchTest.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("DataLabelSheet"); + + ws.Cells["A1"].Value = "Week"; + ws.Cells["B1"].Value = "Income"; + + ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)"; + ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7"; + ws.Cells["C2:C10"].Formula = $"\"Comment \"&(ROW()-1)"; + ws.Calculate(); + + var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered); + + var barSerie = chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]); + var sDlbl = barSerie.DataLabel; + + sDlbl.Separator = ","; + sDlbl.ShowValue = true; + sDlbl.ShowCategory = true; + sDlbl.Position = eLabelPosition.OutEnd; + + sDlbl.SelectRange(ws.Cells["C2:C10"]); + Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange); + + Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address); + Assert.AreEqual("Comment 6", ws.Cells["C7"].Text); + + //Ensure replacement text works + var labelFive = chart.Series[0].DataLabel.DataLabels[5]; + labelFive.SetText("My replacement text"); + + Assert.AreEqual("My replacement text", labelFive.GetExistingParagraphStrings()[0][0]); + + SaveAndCleanup(p); + } + + //Ensure data is read correctly after write + using (var p = OpenPackage("dlblMissMatchTest.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var chart = ws.Drawings[0].As.Chart.BarChart; + + var barSerie = chart.Series[0]; + Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange); + + Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address); + Assert.AreEqual("Comment 6", ws.Cells["C7"].Text); + + //Ensure replacement text works + var labelFive = chart.Series[0].DataLabel.DataLabels[5]; + Assert.AreEqual("My replacement text", labelFive.GetExistingParagraphStrings()[0][0]); + } + } + + [TestMethod] + public void ReadSimpleFile() + { + using (var package = OpenTemplatePackage("editedDataLabel.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var myChart = ws.Drawings[0].As.Chart.BarChart; + + var lbl = myChart.Series[0].DataLabel.DataLabels[0]; + + var lblTxtBody = myChart.Series[0].DataLabel.DataLabels[0].TextBody; + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ReadFile() + { + using (var package = OpenTemplatePackage("S1008_NoComment.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var chart = ws.Drawings[0].As.Chart.LineChart; + + chart.Series[0].DataLabel.Separator = " "; + + //Select comment range + chart.Series[0].DataLabel.SelectRange(ws.Cells["E1:E53"]); + + //Set the relevant labels to not show value + chart.Series[0].DataLabel.DataLabels[21].ShowValue = false; + chart.Series[0].DataLabel.DataLabels[26].ShowValue = false; + + Assert.AreEqual("E22", chart.Series[0].DataLabel.DataLabels[21].SingleCellAddressFromSeries.Address); + Assert.AreEqual("First comment", ws.Cells["E22"].Text); + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void TestAddCommentRangeToExistingFile() + { + using (var package = OpenTemplatePackage("S1008_NoComment.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var commentText = "Added Comment"; + + ws.Cells["E30"].Value = commentText; + + var chart = ws.Drawings[0].As.Chart.LineChart; + chart.Series[0].DataLabel.Separator = " "; + + //Select comment range + chart.Series[0].DataLabel.SelectRange(ws.Cells["E2:E53"]); + + //Note that since we start on E2 the datalabel idx becomes 20 for row 22 etc. + var label1 = chart.Series[0].DataLabel.DataLabels[20]; + var label2 = chart.Series[0].DataLabel.DataLabels[25]; + var label3 = chart.Series[0].DataLabel.DataLabels[28]; + + //Set the relevant labels to not show value as we only want them to show comments + label1.ShowValue = false; + label2.ShowValue = false; + label3.ShowValue = false; + + Assert.AreEqual("E30", chart.Series[0].DataLabel.DataLabels[28].SingleCellAddressFromSeries.Address); + Assert.AreEqual(commentText, ws.Cells["E30"].Text); + + //XforSave is set soley on labels that are not truly neccesary + + SaveAndCleanup(package); + } + } } }