HotXLS schreibt Excel-Waterfall- und Treemap-Diagramme als eigenständige cx:chartSpace-Parts, xl/charts/chartExN.xml mit dem Content-Type application/vnd.ms-office.chartex+xml, und verankert sie über einen mc:AlternateContent-Wrapper, sodass Excel 2016 und neuer das Diagramm rendert, während ältere Reader eine Fallback-Form bekommen. Wählen Sie xlsxChartWaterfall oder xlsxChartTreemap, und die ganze Part-Pipeline folgt
So wie sich dieses Problem üblicherweise ankündigt, ist ein Support-Ticket. Ein Kunde schickt eine Arbeitsmappe, die seine Finanzabteilung in Excel 2019 gebaut hat, Ihr Reporting-Tool öffnet sie, speichert sie, und das Waterfall-Diagramm ist weg. Keine Exception, keine Warnung, nichts im Log, das Diagramm ist einfach nicht im Output. Wenn Sie es im klassischen Chart-Codepfad suchen, suchen Sie lange, denn das Diagramm war von Anfang an nie da
Warum stehen Waterfall- und Treemap-Diagramme nicht in c:chartSpace?
Weil Microsoft sie dort nicht unterbringen konnte. Das DrawingML-Chart-Schema in ECMA-376 Part 1 §21.2 zählt seine Diagrammelemente, c:barChart, c:lineChart, c:pieChart und den Rest, als geschlossene Choice auf, also hätte das Hinzufügen von Waterfall, Treemap, Sunburst und Funnel in Excel 2016 ein Schema gebrochen, auf das jeder bestehende Reader angewiesen ist. Die neuen Typen kamen stattdessen in ein paralleles Dokument: ein cx:chartSpace-Part im chartex-Namespace von 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, neben den klassischen Diagrammen gespeichert, aber strukturell unverwandt. Die Zeichnungsseite näht die zwei Welten mit mc:AlternateContent zusammen, dem Kompatibilitätsmechanismus aus ECMA-376 Part 3: Der mc:Choice-Zweig deklariert Requires="cx1" und hält ein xdr:graphicFrame, dessen a:graphicData/@uri der chartex-Namespace ist und dessen Payload eine nackte <cx:chart r:id="rIdN"/>-Referenz ist, während mc:Fallback ein Rechteck mit dem Hinweis hält, den Excel selbst dort hinschreibt. Ein Reader, der cx1 nicht versteht, ist laut Part-3-Vertrag verpflichtet, den Fallback zu nehmen
Der Relationstyp ist der andere Hinweisgeber. Ein klassisches Diagramm hängt mit der Standard-OOXML-Chart-Relation am Drawing-Part; ein chartEx-Part verwendet stattdessen http://schemas.microsoft.com/office/2014/relationships/chartEx, dokumentiert in [MS-ODRAWXML]. HotXLS legt beide Welten als Konstanten offen, XlsxRtChartEx für diese Relation, XlsxCtChartEx für den Content-Type, XlsxNsMsChartEx und XlsxNsMsChartEx2015 für die beiden Namespaces, die der Anker braucht, und das Prädikat XlsxChartIsChartEx lenkt ein Diagramm beim Speichern den erweiterten Pfad entlang. Kombinationsdiagramme und konserviertes klassisches Chart-XML gehen den anderen Weg, beschrieben in dem Artikel über konserviertes Chart-XML und Kombinationsdiagramme
Vier Schema-Regeln, die entscheiden, ob Excel die Datei öffnet
Das chartex-Schema ist strenger als das klassische an vier konkreten Stellen, und jede von ihnen erzeugt dieselbe nutzlose Reparatur-Aufforderung, wenn Sie es falsch machen. BuildChartExKnownXml und BuildChartExRelsXml in HotXLS backen alle vier ein, aber sie lohnen das Wissen, falls Sie je das XML von Hand inspizieren
- Reihenfolge.
cx:chartDatamuss im Dokument vorcx:chartkommen. Das ist die Umkehrung der Intuition, die Sie aus klassischen Diagrammen aufbauen, wo die Caches in den Serien in der Plot-Area leben; in einem chartEx-Part sitzen die Daten vorn in einem eigenen Block, und der Diagrammkörper referenziert sie nur per id - Kardinalität.
cx:plotAreadarf genau eincx:plotAreaRegionenthalten. Nicht null, nicht zwei. Hier gibt es keine Kombinationsdiagramm-Geschichte, eine Region, und die Serien darin - Typ-Platzierung. Der Diagrammtyp ist überhaupt kein Elementname. Es ist das
layoutId-Attribut ancx:series, das ein Token aus derST_SeriesLayout-Enumeration im Microsoft-chartex-Schema nimmt.XlsxChartExLayoutIdmappt die HotXLS-Enum auf dieses Token und gibt'waterfall'oder'treemap'zurück;XlsxChartExLayoutIdToTypeläuft das Mapping beim Lesen rückwärts - Die Relation, die klassische Diagramme nie brauchen.
cx:chartDatahält ein<cx:externalData r:id="rId1"/>-Element, und diese Relation muss inxl/charts/_rels/chartExN.xml.relsexistieren und zurück auf das Arbeitsblatt zeigen, dem die Daten gehören, denn Excel nutzt sie, um diecx:f-Bereichsformeln aufzulösen. Lassen Sie den Rels-Part weg, und das Diagramm öffnet leer, ohne dass irgendetwas erklärt, warum
Wie fügen Sie in Delphi ein Waterfall- oder Treemap-Diagramm hinzu?
Genau so, wie Sie in HotXLS jedes andere Diagramm hinzufügen, die erweiterten Typen sind Mitglieder von TXLSXChartType, also nimmt TXLSXWorksheet.AddChart sie direkt, und alles flussabwärts hängt am Enum-Wert. Es gibt keine separate chartEx-API zum Lernen
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Ankerrechteck in Zeilen-/Spaltenkoordinaten, wie bei einem klassischen Diagramm
Chart := Sheet.AddChart(xlsxChartWaterfall, 'Q3 Cash Flow',
2, 5, 20, 12);
Chart.AddSeries('Movement', 'Sheet1!$A$2:$A$9', 'Sheet1!$B$2:$B$9');
Book.SaveAs('cashflow-waterfall.xlsx');
finally
Book.Free;
end;
end;
Beim Speichern sieht HotXLS XlsxChartIsChartEx True zurückgeben, vergibt einen Part-Namen aus einem separaten chartEx-Zähler und schreibt xl/charts/chartEx1.xml, seine Rels-Datei, den Content-Type-Override und den mc:AlternateContent-Anker, den XlsxChartExAnchorPayloadXml erzeugt. Der unabhängige Zähler zählt: Eine Arbeitsmappe mit zwei klassischen Diagrammen und einem Waterfall bekommt chart1.xml, chart2.xml und chartEx1.xml, nicht chartEx3.xml. Treemaps unterscheiden sich dann in zwei Details von Waterfalls, beide vom selben Builder behandelt. Die numerische Dimension wechselt den Typ, cx:numDim type="val" bei einem Waterfall wird type="size" bei einem Treemap, denn ein Treemap-Rechteck kodiert Größe als Fläche statt als geplotteten Wert, und Treemaps wollen meist eine Kategoriehierarchie, die das Modell über TXLSXChartSeries.CategoriesMultiLevel ausdrückt. Setzen Sie das Flag, und S.ResolveCategoryLevels liest den Bereich beim Schreiben aus dem Arbeitsblatt, splittet ihn in einen TXLSXChartDataCache pro Ebene hinter S.CategoryLevelCount und S.CategoryLevels[], und BuildChartExLevelXml emittiert ein cx:lvl pro Ebene innerhalb eines einzigen cx:strDim, Blattebene zuerst, dieselbe physische Ordnung, die ECMA-376 Part 1 §21.2 für c:multiLvlStrCache verwendet, also zieht diese Konvention mit, obwohl nichts vom Markup übernommen wird
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // zwei Spalten: Blatt-Label, dann Elterngruppe
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
Die Cache-Form, die jede Annahme aus klassischen Diagrammen bricht
Wenn Sie einen klassischen Chart-Cache-Reader geschrieben haben, verraten Sie drei Gewohnheiten an einem chartEx-Part, und alle drei kommen aus einer Design-Entscheidung: chartex speichert in Attributen und Elementinhalt, was DrawingML in Kindelementen speichert
- Ein gecachter Punkt ist
<cx:pt idx="0">42</cx:pt>. Der Wert ist der Elementinhalt. Es gibt keinc:v-Kind, in das man absteigen könnte, also findet ein Parser, der nachv-Knoten sucht, nichts und meldet einen leeren Cache ptCountundformatCodesind Attribute ancx:lvl. In einem klassischenc:numCachesind beide Kindelemente- Mehrere Hierarchieebenen sind mehrere
cx:lvl-Elemente innerhalb einescx:strDim. Es gibt kein separates Multi-Level-Cache-Element, so wiec:multiLvlStrCachevonc:strCachegetrennt ist
BuildChartExLevelXml schreibt diese Form und ParseChartExXml liest sie zurück, wobei jeder Wert beim Rausgehen durch XlsxEscapeText läuft, damit ein Kategorielabel mit einem Ampersand oder einem spitzen Klammerpaar den Round Trip überlebt. Erwähnenswert ist, was der Builder tut, wenn eine numerische Ebene kein Format-Code trägt: Er schreibt formatCode="General", statt das Attribut wegzulassen, denn hier ist das Attribut nicht optional, so wie es sein klassisches Pendant ist
Das Namespace-Präfix, das das Typ-Rücklesen scheitern ließ
Die erste Version des HotXLS-chartEx-Parsers schrieb einwandfreie Dateien und las jede einzelne als Säulendiagramm zurück. Die Wurzel war eine Zeile, und die Lektion verallgemeinert auf jeden OOXML-Parser, den Sie je schreiben werden: TXMLReader.Name gibt den Namen so zurück, wie er im Dokument steht, Präfix inklusive. In einem klassischen Chart-Part kommen die meisten Elemente ohne Präfix an, also funktioniert SameText(Reader.Name, 'series'), und Sie hören auf, darüber nachzudenken. In einem chartEx-Part ist jedes Element präfixiert, der Reader gibt Ihnen 'cx:series', der Vergleich schlägt still auf allen fehl, und Sie bekommen keine Exception und kein kaputtes XML, nur eine leere Serienliste und einen Diagrammtyp, der auf den Default zurückfällt. Der Fix ist, auf den lokalen Namen zu vergleichen, an einer Stelle statt an jedem Aufrufort
// Reader.Name trägt sein Namespace-Präfix: 'cx:series', nicht 'series'
function IsEl(const LocalName: string): Boolean;
var
NodeName: string;
begin
NodeName := String(Reader.Name);
Result := SameText(NodeName, LocalName) or
EndsText(':' + LocalName, NodeName);
end;
ParseChartExXml verwendet diesen Helfer für jeden Elementtest und einen gleichwertigen AttrVal-Helfer für Attribute, denn cx:autoUpdate und Co. sind auch präfixiert. Sobald die Serienelemente tatsächlich gesehen werden, kommt der Typ vom ersten layoutId, auf das man stößt, und jedes cx:series wird mit seinem cx:data-Block verdrahtet, indem cx:dataId/@val gegen cx:data/@id gematcht wird. Geöffnete, aber nie veränderte Diagramme nehmen stattdessen den älteren Pfad: Die Originalbytes laufen aus FRawChartXml wieder durch, also kommt eine unberührte Kundendatei so wieder heraus, wie sie hineinging, dieselbe konservative Writer-Disziplin, die Blattebene-Erweiterungen in dem Sparkline-Erweiterungsartikel regelt
Was HotXLS mit diesen Diagrammen bewusst nicht tut
Zwei Grenzen lohnen die klare Ansage, denn beide sind Entscheidungen statt Lücken, die auf Füllung warten. Die eingebauten PDF- und SVG-Renderer zeichnen keine Waterfall- oder Treemap-Serien: Die Typen matchen keinen Zeichenzweig, also wird die Serien-Renderung übersprungen und der Rest des Blatts exportiert normal, ein plausibel aussehender Treemap in einem exportierten PDF wäre schlimmer als nichts zu zeichnen, denn er würde still vom Excel-Bild desselben Files abweichen. Und xlsxChartWaterfall und xlsxChartTreemap sind die einzigen zwei erweiterten Layouts, die HotXLS aus dem Modell baut; ST_SeriesLayout definiert auch Sunburst, Funnel, Box-and-Whisker und Histogramm, und eine Arbeitsmappe mit einem davon round-tript trotzdem über den Raw-XML-Replay-Pfad, nur können Sie heute keinen aus Delphi-Code erzeugen. Die Renderung der klassischen Diagrammtypen ist unverändert und in dem Artikel über Diagramme, Bilder und Drawings behandelt, und die chartEx-Pipeline fährt neben ihr im HotXLS Delphi Excel Component für Delphi und C++Builder