mirror of
https://github.com/iOfficeAI/OfficeCLI
synced 2026-04-21 13:37:23 +00:00
fix(xlsx): emit Excel 2010+ x14 extension for databar CF
Databar conditional format previously emitted only the Excel 2007
<x:dataBar> element, which causes Excel to render negative values as
positive (rightward bars, no zero-axis, no red for negatives).
Now emit the paired x14 extension under the worksheet extLst:
- x14:dataBar with axisPosition, fillColor, negativeFillColor, axisColor
- x14:cfRule linked to the 2007 cfRule via {B025F937-...} extension GUID
- mc:Ignorable="x14" on the worksheet root (via new helper
EnsureWorksheetX14Ignorable, factored out of the sparkline path)
New user props:
- negativeColor=<hex> (default FF0000)
- axisColor=<hex> (default 000000)
- axisPosition=automatic|middle|none
Also: omit cfvo val="" attribute when type is min/max (Excel rejects
the empty attribute).
This commit is contained in:
parent
502976ef29
commit
318edca91f
2 changed files with 134 additions and 8 deletions
|
|
@ -1076,16 +1076,19 @@ public partial class ExcelHandler
|
|||
Priority = NextCfPriority(GetSheet(cfWorksheet))
|
||||
};
|
||||
var dataBar = new DataBar();
|
||||
dataBar.Append(new ConditionalFormatValueObject
|
||||
// R10-1: when cfvo type is min/max, omit `val` attribute (Excel rejects val="").
|
||||
var dbMinCfvo = new ConditionalFormatValueObject
|
||||
{
|
||||
Type = minVal != null ? ConditionalFormatValueObjectValues.Number : ConditionalFormatValueObjectValues.Min,
|
||||
Val = minVal
|
||||
});
|
||||
dataBar.Append(new ConditionalFormatValueObject
|
||||
Type = minVal != null ? ConditionalFormatValueObjectValues.Number : ConditionalFormatValueObjectValues.Min
|
||||
};
|
||||
if (minVal != null) dbMinCfvo.Val = minVal;
|
||||
dataBar.Append(dbMinCfvo);
|
||||
var dbMaxCfvo = new ConditionalFormatValueObject
|
||||
{
|
||||
Type = maxVal != null ? ConditionalFormatValueObjectValues.Number : ConditionalFormatValueObjectValues.Max,
|
||||
Val = maxVal
|
||||
});
|
||||
Type = maxVal != null ? ConditionalFormatValueObjectValues.Number : ConditionalFormatValueObjectValues.Max
|
||||
};
|
||||
if (maxVal != null) dbMaxCfvo.Val = maxVal;
|
||||
dataBar.Append(dbMaxCfvo);
|
||||
dataBar.Append(new DocumentFormat.OpenXml.Spreadsheet.Color { Rgb = normalizedColor });
|
||||
cfRule.Append(dataBar);
|
||||
// CF6 — dataBar `showValue=false` hides the cell's numeric
|
||||
|
|
@ -1095,6 +1098,23 @@ public partial class ExcelHandler
|
|||
dataBar.ShowValue = false;
|
||||
ApplyStopIfTrue(cfRule, properties);
|
||||
|
||||
// R10-1: Also emit Excel 2010+ x14 extension so negative values
|
||||
// render leftward in red with an axis. Without this block, Excel
|
||||
// uses the 2007 dataBar which treats all values as positive
|
||||
// (rightward blue bars, no axis, no red for negatives).
|
||||
var dbGuid = "{" + Guid.NewGuid().ToString().ToUpperInvariant() + "}";
|
||||
// Attach x14:id extension onto the 2007 cfRule so it's paired
|
||||
// with the sibling x14:cfRule in the worksheet extLst.
|
||||
var dbRuleExtList = new ConditionalFormattingRuleExtensionList();
|
||||
var dbRuleExt = new ConditionalFormattingRuleExtension
|
||||
{
|
||||
Uri = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
|
||||
};
|
||||
dbRuleExt.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main");
|
||||
dbRuleExt.Append(new X14.Id(dbGuid));
|
||||
dbRuleExtList.Append(dbRuleExt);
|
||||
cfRule.Append(dbRuleExtList);
|
||||
|
||||
var cf = new ConditionalFormatting(cfRule)
|
||||
{
|
||||
SequenceOfReferences = new ListValue<StringValue>(
|
||||
|
|
@ -1104,6 +1124,57 @@ public partial class ExcelHandler
|
|||
var wsElement = GetSheet(cfWorksheet);
|
||||
InsertConditionalFormatting(wsElement, cf);
|
||||
|
||||
// R10-1: Build the x14:dataBar counterpart under worksheet extLst.
|
||||
var dbNegColor = ParseHelpers.NormalizeArgbColor(properties.GetValueOrDefault("negativeColor", "FF0000"));
|
||||
var dbAxisColor = ParseHelpers.NormalizeArgbColor(properties.GetValueOrDefault("axisColor", "000000"));
|
||||
var dbAxisPos = (properties.GetValueOrDefault("axisPosition") ?? "automatic").ToLowerInvariant();
|
||||
var dbAxisPosVal = dbAxisPos switch
|
||||
{
|
||||
"middle" => X14.DataBarAxisPositionValues.Middle,
|
||||
"none" => X14.DataBarAxisPositionValues.None,
|
||||
_ => X14.DataBarAxisPositionValues.Automatic
|
||||
};
|
||||
|
||||
var x14DataBar = new X14.DataBar
|
||||
{
|
||||
MinLength = 0U,
|
||||
MaxLength = 100U,
|
||||
AxisPosition = dbAxisPosVal
|
||||
};
|
||||
var x14MinCfvo = new X14.ConditionalFormattingValueObject
|
||||
{
|
||||
Type = minVal != null
|
||||
? X14.ConditionalFormattingValueObjectTypeValues.Numeric
|
||||
: X14.ConditionalFormattingValueObjectTypeValues.AutoMin
|
||||
};
|
||||
if (minVal != null) x14MinCfvo.Append(new DocumentFormat.OpenXml.Office.Excel.Formula(minVal));
|
||||
x14DataBar.Append(x14MinCfvo);
|
||||
var x14MaxCfvo = new X14.ConditionalFormattingValueObject
|
||||
{
|
||||
Type = maxVal != null
|
||||
? X14.ConditionalFormattingValueObjectTypeValues.Numeric
|
||||
: X14.ConditionalFormattingValueObjectTypeValues.AutoMax
|
||||
};
|
||||
if (maxVal != null) x14MaxCfvo.Append(new DocumentFormat.OpenXml.Office.Excel.Formula(maxVal));
|
||||
x14DataBar.Append(x14MaxCfvo);
|
||||
x14DataBar.Append(new X14.FillColor { Rgb = normalizedColor });
|
||||
x14DataBar.Append(new X14.NegativeFillColor { Rgb = dbNegColor });
|
||||
x14DataBar.Append(new X14.BarAxisColor { Rgb = dbAxisColor });
|
||||
|
||||
var x14CfRule = new X14.ConditionalFormattingRule
|
||||
{
|
||||
Type = ConditionalFormatValues.DataBar,
|
||||
Id = dbGuid
|
||||
};
|
||||
x14CfRule.Append(x14DataBar);
|
||||
|
||||
var x14Cf = new X14.ConditionalFormatting();
|
||||
x14Cf.AddNamespaceDeclaration("xm", "http://schemas.microsoft.com/office/excel/2006/main");
|
||||
x14Cf.Append(x14CfRule);
|
||||
x14Cf.Append(new DocumentFormat.OpenXml.Office.Excel.ReferenceSequence(sqref));
|
||||
|
||||
EnsureWorksheetX14ConditionalFormatting(wsElement, x14Cf);
|
||||
|
||||
SaveWorksheet(cfWorksheet);
|
||||
var dbCfCount = wsElement.Elements<ConditionalFormatting>().Count();
|
||||
return $"/{cfSheetName}/cf[{dbCfCount}]";
|
||||
|
|
|
|||
|
|
@ -633,6 +633,61 @@ public partial class ExcelHandler
|
|||
rule.StopIfTrue = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the worksheet root declares `xmlns:x14` + `mc:Ignorable="x14"`.
|
||||
/// Without both, Excel silently drops the x14 extension block where
|
||||
/// sparklines, dataBar 2010+ extensions, and other Office2010 features
|
||||
/// live. CONSISTENCY(x14-ignorable): same pattern the sparkline branch
|
||||
/// uses inline.
|
||||
/// </summary>
|
||||
internal static void EnsureWorksheetX14Ignorable(Worksheet ws)
|
||||
{
|
||||
const string mcNs = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
||||
const string x14Ns = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
|
||||
if (ws.LookupNamespace("mc") == null)
|
||||
ws.AddNamespaceDeclaration("mc", mcNs);
|
||||
if (ws.LookupNamespace("x14") == null)
|
||||
ws.AddNamespaceDeclaration("x14", x14Ns);
|
||||
var ignorable = ws.MCAttributes?.Ignorable?.Value ?? "";
|
||||
if (!ignorable.Split(' ').Contains("x14"))
|
||||
{
|
||||
ws.MCAttributes ??= new MarkupCompatibilityAttributes();
|
||||
ws.MCAttributes.Ignorable = string.IsNullOrEmpty(ignorable) ? "x14" : $"{ignorable} x14";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append an x14:conditionalFormatting block to the worksheet's extLst under
|
||||
/// ext URI `{78C0D931-6437-407d-A8EE-F0AAD7539E65}`. Creates the extension
|
||||
/// on first call, appends to the existing x14:conditionalFormattings
|
||||
/// container on subsequent calls. Also ensures mc:Ignorable="x14" is set.
|
||||
/// </summary>
|
||||
internal static void EnsureWorksheetX14ConditionalFormatting(Worksheet ws, X14.ConditionalFormatting x14Cf)
|
||||
{
|
||||
const string cfExtUri = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}";
|
||||
const string x14Ns = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
|
||||
|
||||
EnsureWorksheetX14Ignorable(ws);
|
||||
|
||||
var extList = ws.GetFirstChild<WorksheetExtensionList>() ?? ws.AppendChild(new WorksheetExtensionList());
|
||||
var ext = extList.Elements<WorksheetExtension>().FirstOrDefault(e => e.Uri == cfExtUri);
|
||||
X14.ConditionalFormattings cfContainer;
|
||||
if (ext != null)
|
||||
{
|
||||
cfContainer = ext.GetFirstChild<X14.ConditionalFormattings>()
|
||||
?? ext.AppendChild(new X14.ConditionalFormattings());
|
||||
}
|
||||
else
|
||||
{
|
||||
ext = new WorksheetExtension { Uri = cfExtUri };
|
||||
ext.AddNamespaceDeclaration("x14", x14Ns);
|
||||
cfContainer = new X14.ConditionalFormattings();
|
||||
ext.Append(cfContainer);
|
||||
extList.Append(ext);
|
||||
}
|
||||
cfContainer.Append(x14Cf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark a worksheet as dirty. The actual save (with schema-order reorder) is
|
||||
/// deferred to <see cref="FlushDirtyParts"/> which runs in Dispose().
|
||||
|
|
|
|||
Loading…
Reference in a new issue