Files
ClosedXML-ClosedXML/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs
T
dualfroz 9508e12dc7 fix: write data bar cfvo as min/max instead of num for lowest/highest value
DataBar().LowestValue()/HighestValue() emitted the x14 extension cfvo nodes as type="num" with a formula of 0 instead of type="min"/"max". Strict readers such as WPS Spreadsheets honor that value and lock the bar thresholds to 0, so the data bar renders incorrectly.

The extension converter unconditionally overwrote the cfvo type with Numeric whenever a threshold value was present. LowestValue()/HighestValue() store a placeholder "0", so their automatic min/max bounds were turned into fixed numeric thresholds; the same override also collapsed percent, percentile and formula thresholds to num.

Each cfvo is now built from its content type: automatic bounds stay autoMin/autoMax, lowest/highest bounds become min/max with no formula, and value based thresholds keep their type and emit the formula.

Fixes #2843
2026-09-04 22:18:19 +02:00

109 lines
4.9 KiB
C#

using ClosedXML.Extensions;
using DocumentFormat.OpenXml.Office.Excel;
using DocumentFormat.OpenXml.Office2010.Excel;
using System.Collections.Generic;
namespace ClosedXML.Excel
{
internal class XLCFDataBarConverterExtension : IXLCFConverterExtension
{
private static readonly IReadOnlyDictionary<DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues, ConditionalFormattingValueObjectTypeValues> CFValueToTypeMap =
new Dictionary<DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues, ConditionalFormattingValueObjectTypeValues>
{
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Max, ConditionalFormattingValueObjectTypeValues.Max },
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Min, ConditionalFormattingValueObjectTypeValues.Min },
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Number, ConditionalFormattingValueObjectTypeValues.Numeric },
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percent, ConditionalFormattingValueObjectTypeValues.Percent },
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percentile, ConditionalFormattingValueObjectTypeValues.Percentile },
{ DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Formula, ConditionalFormattingValueObjectTypeValues.Formula },
};
public ConditionalFormattingRule Convert(IXLConditionalFormat cf, XLWorkbook.SaveContext context)
{
ConditionalFormattingRule conditionalFormattingRule = new ConditionalFormattingRule()
{
Type = DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValues.DataBar,
Id = ((XLConditionalFormat)cf).Id.WrapInBraces()
};
DataBar dataBar = new DataBar()
{
MinLength = 0,
MaxLength = 100,
Gradient = true,
ShowValue = !cf.ShowBarOnly
};
var cfMin = BuildValueObject(cf, 1, ConditionalFormattingValueObjectTypeValues.AutoMin);
var cfMax = BuildValueObject(cf, 2, ConditionalFormattingValueObjectTypeValues.AutoMax);
var barAxisColor = new BarAxisColor { Rgb = XLColor.Black.Color.ToHex() };
var negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[1].Color.ToHex() };
if (cf.Colors.Count == 2)
{
negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[2].Color.ToHex() };
}
dataBar.Append(cfMin);
dataBar.Append(cfMax);
dataBar.Append(negativeFillColor);
dataBar.Append(barAxisColor);
conditionalFormattingRule.Append(dataBar);
return conditionalFormattingRule;
}
/// <summary>
/// Builds a single x14 conditional formatting value object (cfvo) for a data bar threshold.
/// </summary>
/// <param name="cf">The conditional format the data bar belongs to.</param>
/// <param name="index">1-based threshold index (1 = minimum, 2 = maximum).</param>
/// <param name="automaticType">
/// Type emitted for an automatic bound: autoMin for the minimum, autoMax for the maximum.
/// </param>
/// <remarks>
/// Automatic and min/max bounds are self-describing and carry no formula; only
/// value based thresholds (num/percent/percentile/formula) emit an xm:f formula.
/// </remarks>
private static ConditionalFormattingValueObject BuildValueObject(IXLConditionalFormat cf, int index, ConditionalFormattingValueObjectTypeValues automaticType)
{
cf.Values.TryGetValue(index, out var value);
var formulaText = value?.Value;
if (!cf.ContentTypes.TryGetValue(index, out var contentType))
{
return new ConditionalFormattingValueObject { Type = automaticType };
}
var type = GetCFType(contentType.ToOpenXml());
var valueObject = new ConditionalFormattingValueObject { Type = type };
if (type == ConditionalFormattingValueObjectTypeValues.Min ||
type == ConditionalFormattingValueObjectTypeValues.Max)
{
if (formulaText == null)
{
valueObject.Type = automaticType;
}
return valueObject;
}
if (formulaText != null)
{
valueObject.Append(new Formula { Text = formulaText });
}
return valueObject;
}
private static ConditionalFormattingValueObjectTypeValues GetCFType(DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues value)
{
return CFValueToTypeMap[value];
}
}
}