Teknisk artikel

Excel Waterfall- och Treemap-diagram i Delphi med HotXLS

HotXLS skriver Excel-waterfall- och treemap-diagram som fristående cx:chartSpace-delar — xl/charts/chartExN.xml med innehållstypen application/vnd.ms-office.chartex+xml — och förankrar dem genom ett mc:AlternateContent-hölje så att Excel 2016 och senare renderar diagrammet medan äldre läsare får en fallback-form. Välj xlsxChartWaterfall eller xlsxChartTreemap och hela delpipelinen följer med

Sättet det här problemet brukar annonsera sig är en supportbiljett. En kund skickar en arbetsbok som deras ekonomiteam byggde i Excel 2019, ditt rapportverktyg öppnar den, sparar den, och waterfall-diagrammet är borta. Inget undantag, ingen varning, ingenting i loggen — diagrammet finns helt enkelt inte i utdata. Letar du efter det i den klassiska diagramkodvägen kommer du att leta länge, för diagrammet fanns aldrig där från början

Det mc:AlternateContent-ankare HotXLS skriver för ett chartEx-diagram: mc:Choice-grenen kräver cx1 och håller en xdr:graphicFrame vars a:graphicData refererar chartex-namespacen med en bar cx:chart-relation, medan mc:Fallback bär en notisrektangel som äldre läsare ritar i stället
Ankaret mc:AlternateContent dirigerar ett chartEx-diagram mellan Excel 2016 och äldre läsare: Choice-grenen kräver cx1 och bär chartex-ramen, medan Fallback håller den notisrektangel äldre läsare måste rita

Varför finns inte waterfall- och treemap-diagram i c:chartSpace?

För att Microsoft inte kunde lägga dem där. DrawingML-diagramschemat i ECMA-376 Part 1 §21.2 räknar upp sina diagramelement — c:barChart, c:lineChart, c:pieChart och resten — som ett slutet val, så att lägga till waterfall, treemap, sunburst och funnel i Excel 2016 hade betytt att bryta ett schema varje befintlig läsare förlitar sig på. De nya typerna hamnade i ett parallellt dokument i stället: en cx:chartSpace-del i 2014 års chartex-namespace, http://schemas.microsoft.com/office/drawing/2014/chartex, lagrad bredvid de klassiska diagrammen men strukturellt orelaterad till dem. Ritningssidan syr ihop de två världarna med mc:AlternateContent, kompatibilitetsmekanismen från ECMA-376 Part 3: mc:Choice-grenen deklarerar Requires="cx1" och håller en xdr:graphicFrame vars a:graphicData/@uri är chartex-namespacen och vars innehåll är en bar <cx:chart r:id="rIdN"/>-referens, medan mc:Fallback håller en rektangel med den notis Excel själv skriver dit. En läsare som inte förstår cx1 är enligt Part 3-kontraktet tvungen att ta fallbacken

Relationstypen är den andra ledtråden. Ett klassiskt diagram hänger i ritningsdelen med den vanliga OOXML-diagramrelationen; en chartEx-del använder http://schemas.microsoft.com/office/2014/relationships/chartEx i stället, dokumenterad i [MS-ODRAWXML]. HotXLS exponerar båda världarna som konstanter — XlsxRtChartEx för den relationen, XlsxCtChartEx för innehållstypen, XlsxNsMsChartEx och XlsxNsMsChartEx2015 för de två namespaces ankaret behöver — och predikatet XlsxChartIsChartEx är det som dirigerar ett diagram ned på den utökade vägen vid sparning. Kombinationsdiagram och bevarad klassisk diagram-XML går den andra vägen, beskriven i artikeln om bevarad diagram-XML och kombinationsdiagram

Fyra schemaregler som avgör om Excel öppnar filen

Chartex-schemat är striktare än det klassiska på fyra specifika ställen, och vart och ett av dem ger samma oanvändbara reparationsprompt när du gör fel. BuildChartExKnownXml och BuildChartExRelsXml i HotXLS bakar in alla fyra, men de är värda att känna till om du någonsin inspekterar XML för hand

  • Ordning. cx:chartData måste komma före cx:chart i dokumentet. Det är motsatsen till intuitionen du bygger från klassiska diagram, där cacherna bor inuti serien inuti plot area; i en chartEx-del sitter datan längst fram i ett eget block och diagramkroppen refererar bara den via id
  • Kardinalitet. cx:plotArea får innehålla exakt en cx:plotAreaRegion. Inte noll, inte två. Här finns ingen kombinationsdiagram-historia — en region, och serien inuti den
  • Typens placering. Diagramtypen är inte alls ett elementnamn. Det är layoutId-attributet på cx:series, som tar en token från ST_SeriesLayout-enumerationen i Microsofts chartex-schema. XlsxChartExLayoutId mappar HotXLS-enumen till den token och returnerar 'waterfall' eller 'treemap'; XlsxChartExLayoutIdToType kör mappningen baklänges vid läsning
  • Relationen klassiska diagram aldrig behöver. cx:chartData håller ett <cx:externalData r:id="rId1"/>-element, och den relationen måste finnas i xl/charts/_rels/chartExN.xml.rels och peka tillbaka på det kalkylblad som äger datan, för Excel använder den för att lösa upp cx:f-intervallformlerna. Uteslut rels-delen och diagrammet öppnas tomt utan något som förklarar varför

Hur lägger du till ett waterfall- eller treemap-diagram i Delphi?

På samma sätt som du lägger till vilket annat diagram som helst i HotXLS — de utökade typerna är medlemmar av TXLSXChartType, så TXLSXWorksheet.AddChart tar emot dem direkt och allt nedströms nycklas på enumvärdet. Det finns inget separat chartEx-API att lära sig

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Chart: TXLSXChart;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('cashflow.xlsx');
    Sheet := Book.Sheets[0];

    // Ankarrektangel i rad/kolumnkoordinater, samma som ett klassiskt diagram
    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;
HotXLS sparpipeline för utökade Excel-diagram: AddChart med xlsxChartWaterfall eller xlsxChartTreemap når XlsxChartIsChartEx, som dirigerar klassiska diagram till en delad räknare och chartEx-diagram till en oberoende räknare som skriver chartEx1.xml, dess rels-fil som pekar tillbaka på kalkylbladet, chartex-innehållstypsöverskriften och AlternateContent-ankarhöljet
En diagrammodell och ett dirigeringspredikat matar två räknare vid sparning: klassiska diagram delar chart1.xml och chart2.xml, medan waterfall hamnar som chartEx1.xml för att chartEx-räknaren är oberoende av den klassiska

Vid sparning ser HotXLS XlsxChartIsChartEx returnera True, allokerar ett delnamn från en separat chartEx-räknare, och skriver xl/charts/chartEx1.xml, dess rels-fil, innehållstypsöverskriften och det mc:AlternateContent-ankare som XlsxChartExAnchorPayloadXml producerar. Den oberoende räknaren spelar roll: en arbetsbok med två klassiska diagram och ett waterfall får chart1.xml, chart2.xml och chartEx1.xml, inte chartEx3.xml. Treemaps skiljer sig sedan från waterfalls i två detaljer, båda hanterade av samma byggare. Den numeriska dimensionen byter typ — cx:numDim type="val" för ett waterfall blir type="size" för en treemap, för en treemap-rektangel kodar storlek som area snarare än som ett plottat värde — och treemaps vill oftast ha en kategorihierarki, som modellen uttrycker genom TXLSXChartSeries.CategoriesMultiLevel. Sätt den flaggan så läser S.ResolveCategoryLevels intervallet ur kalkylbladet vid skrivning, delar upp det i en TXLSXChartDataCache per nivå bakom S.CategoryLevelCount och S.CategoryLevels[], och BuildChartExLevelXml avger ett cx:lvl per nivå inuti en enda cx:strDim, lövnivå först — samma fysiska ordning ECMA-376 Part 1 §21.2 använder för c:multiLvlStrCache, så den konventionen följer med även om ingen av markeringen gör det

Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
  2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
  'Sheet1!$A$2:$B$40',   // två kolumner: lövetikett, sedan föräldragrupp
  'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;

Cacheformen som krossar alla antaganden från klassiska diagram

Har du skrivit en klassisk diagramcache-läsare kommer tre vanor att förråda dig på en chartEx-del, och alla tre kommer från ett designbeslut: chartex lagrar i attribut och elementinnehåll det DrawingML lagrar i barnelement

  • En cachad punkt är <cx:pt idx="0">42</cx:pt>. Värdet är elementinnehållet. Det finns inget c:v-barnelement att gå ned i, så en parser som letar efter v-noder hittar ingenting och rapporterar en tom cache
  • ptCount och formatCode är attribut på cx:lvl. I en klassisk c:numCache är båda barnelement
  • Flera hierarkinivåer är flera cx:lvl-element inuti en cx:strDim. Det finns inget separat multi-level-cacheelement på det sätt c:multiLvlStrCache är separat från c:strCache

BuildChartExLevelXml skriver den formen och ParseChartExXml läser tillbaka den, och kör varje värde genom XlsxEscapeText på vägen ut så att en kategorietikett som innehåller ett &-tecken eller en vinkelparentes överlever rundresan. Värt att notera är vad byggaren gör när en numerisk nivå saknar formatkod: den skriver formatCode="General" i stället för att släppa attributet, för här är attributet inte valfritt på det sätt dess klassiska motsvarighet är

Cacheformer sida vid sida i HotXLS chartEx-delar: en klassisk c:numCache lagrar ptCount och formatCode som barnelement med värden inuti c:v, medan cx:numDim lagrar dem som attribut på cx:lvl med värdet som elementinnehåll, och CategoriesMultiLevel avger ett cx:lvl per kategorinivå, löv först, inuti en enda cx:strDim
Chartex lagrar i attribut och elementinnehåll det DrawingML lagrar i barnelement: värdet sitter direkt inuti cx:pt, ptCount och formatCode blir attribut på cx:lvl, och en kategorihierarki är flera cx:lvl-element i en cx:strDim

Namespace-prefixet som fick typläsningen att misslyckas

Första versionen av HotXLS chartEx-parsern skrev fullt giltiga filer och läste tillbaka var och en av dem som ett kolumndiagram. Grundorsaken var en rad, och läxan generaliserar till vilken OOXML-parser du än kommer att skriva: TXMLReader.Name returnerar namnet som skrivet i dokumentet, prefix inkluderat. I en klassisk diagramdel anländer de flesta element utan prefix, så SameText(Reader.Name, 'series') fungerar och du slutar tänka på det. I en chartEx-del är vartenda element prefixat — läsaren ger dig 'cx:series', jämförelsen misslyckas tyst på dem alla, och du får varken undantag eller felformad XML, bara en tom serielista och en diagramtyp som faller tillbaka på standard. Fixen är att jämföra på local name, på ett ställe i stället för vid varje anropsplats

// Reader.Name bär sitt namespace-prefix: 'cx:series', inte '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 använder den hjälparen för varje elementtest och en motsvarande AttrVal-hjälpare för attribut, eftersom cx:autoUpdate och vänner är prefixade också. När serieelementen väl faktiskt syns kommer typen tillbaka från det första layoutId som stöts på, och varje cx:series kopplas till sitt cx:data-block genom att matcha cx:dataId/@val mot cx:data/@id. Diagram som öppnats men aldrig ändrats tar i stället den äldre vägen: originalbyten spelas upp från FRawChartXml, så en orörd kundfil kommer ut som den gick in — samma konservativa-skrivare-diciplin som styr arkivextensioner i artikeln om sparkline-extensionen

Vad HotXLS medvetet inte gör med dessa diagram

Två gränser är värda att säga rakt ut, för båda är val snarare än luckor som väntar på att fyllas. De inbyggda PDF- och SVG-renderarna ritar inte waterfall- eller treemap-serier: typerna matchar ingen ritningsgren, så serierenderingen hoppas över och resten av arket exporteras normalt — att fejka en trovärdig treemap i en exporterad PDF vore värre än att inte rita något, för den skulle tyst avvika från vad Excel visar för samma fil. Och xlsxChartWaterfall och xlsxChartTreemap är de enda två utökade layouterna HotXLS bygger från modellen; ST_SeriesLayout definierar också sunburst, funnel, box-and-whisker och histogram, och en arbetsbok som innehåller någon av dem rundreser fortfarande genom raw-XML-uppspelningsvägen, du kan bara inte skapa en från Delphi-kod idag. Renderingen av de klassiska diagramtyperna är oförändrad och täcks i artikeln om diagram, bilder och ritningar, och chartEx-pipelinen levereras tillsammans med den i HotXLS Delphi Excel Component för Delphi och C++Builder