HotXLS scrive i grafici waterfall e treemap di Excel come parti cx:chartSpace autonome — xl/charts/chartExN.xml con content type application/vnd.ms-office.chartex+xml — e le aggancia tramite un wrapper mc:AlternateContent così Excel 2016 e successivi renderizza il grafico mentre i lettori più vecchi ricevono una shape di fallback. Scegli xlsxChartWaterfall o xlsxChartTreemap e tutta la pipeline delle parti segue
Il modo in cui questo problema di solito si annuncia è un ticket di supporto. Un cliente manda una cartella di lavoro creata dal suo team finance in Excel 2019, il tuo tool di report la apre, la salva, e il grafico waterfall è sparito. Nessuna eccezione, nessun warning, nulla nel log: il grafico semplicemente non è nell'output. Se lo cerchi nel percorso di codice dei grafici classici, lo cerchi a lungo, perché il grafico non c'è mai stato dall'inizio
Perché i grafici waterfall e treemap non stanno in c:chartSpace?
Perché Microsoft non poteva metterceli. Lo schema dei grafici DrawingML in ECMA-376 Part 1 §21.2 enumera i suoi elementi di grafico — c:barChart, c:lineChart, c:pieChart e il resto — come una scelta chiusa, quindi aggiungere waterfall, treemap, sunburst e funnel in Excel 2016 avrebbe significato rompere uno schema da cui dipende ogni lettore esistente. I nuovi tipi sono finiti invece in un documento parallelo: una parte cx:chartSpace nel namespace chartex del 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, conservata accanto ai grafici classici ma strutturalmente estranea a loro. Il lato del disegno ricuce i due mondi con mc:AlternateContent, il meccanismo di compatibilità di ECMA-376 Part 3: il ramo mc:Choice dichiara Requires="cx1" e contiene un xdr:graphicFrame il cui a:graphicData/@uri è il namespace chartex e il cui payload è un riferimento <cx:chart r:id="rIdN"/> nudo, mentre mc:Fallback contiene un rettangolo con l'avviso che Excel stesso scrive lì. Un lettore che non capisce cx1 è tenuto dal contratto della Part 3 a prendere il fallback
Il tipo di relazione è l'altro indizio rivelatore. Un grafico classico pende dalla parte di disegno con la relazione OOXML standard; una parte chartEx usa invece http://schemas.microsoft.com/office/2014/relationships/chartEx, documentato in [MS-ODRAWXML]. HotXLS espone entrambi i mondi come costanti — XlsxRtChartEx per quella relazione, XlsxCtChartEx per il content type, XlsxNsMsChartEx e XlsxNsMsChartEx2015 per i due namespace di cui l'ancora ha bisogno — e il predicato XlsxChartIsChartEx è ciò che instrada un grafico sul percorso esteso al salvataggio. I grafici combinati e l'XML dei grafici classici preservato vanno sull'altro percorso, descritto in l'articolo sull'XML di grafico preservato e i grafici combinati
Quattro regole dello schema che decidono se Excel apre il file
Lo schema chartex è più severo di quello classico in quattro punti specifici, e ognuno di loro produce lo stesso inutile avviso di riparazione quando sbagli. BuildChartExKnownXml e BuildChartExRelsXml in HotXLS li incorporano tutti e quattro, ma vale la pena conoscerli se ispezioni mai l'XML a mano
- Ordine.
cx:chartDatadeve venire prima dicx:chartnel documento. È l'opposto dell'intuizione che ti costruisci dai grafici classici, dove le cache stanno dentro le serie dentro l'area del tracciato; in una parte chartEx i dati stanno davanti in un blocco tutto loro e il corpo del grafico li riferisce solo per id - Cardinalità.
cx:plotAreapuò contenere esattamente uncx:plotAreaRegion. Non zero, non due. Qui non c'è storia di grafico combinato: una regione, e le serie dentro - Posizione del tipo. Il tipo di grafico non è affatto un nome di elemento. È l'attributo
layoutIdsucx:series, che prende un token dall'enumerazioneST_SeriesLayoutdello schema chartex di Microsoft.XlsxChartExLayoutIdmappa l'enum HotXLS su quel token e restituisce'waterfall'o'treemap';XlsxChartExLayoutIdToTypeesegue la mappatura al contrario in lettura - La relazione che i grafici classici non servono mai.
cx:chartDatacontiene un elemento<cx:externalData r:id="rId1"/>, e quella relazione deve esistere inxl/charts/_rels/chartExN.xml.relspuntando di nuovo al foglio di lavoro che possiede i dati, perché Excel la usa per risolvere le formule di intervallocx:f. Tralascia la parte rels e il grafico si apre vuoto senza nulla che spieghi il perché
Come aggiungi un grafico waterfall o treemap in Delphi?
Nello stesso modo in cui aggiungi qualsiasi altro grafico in HotXLS: i tipi estesi sono membri di TXLSXChartType, quindi TXLSXWorksheet.AddChart li accetta direttamente e tutto a valle si basa sul valore dell'enum. Non c'è un'API chartEx separata da imparare
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Rettangolo di ancoraggio in coordinate riga/colonna, come un grafico classico
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;
Al salvataggio, HotXLS vede XlsxChartIsChartEx restituire True, assegna un nome di parte da un contatore chartEx separato, e scrive xl/charts/chartEx1.xml, il suo file rels, l'override del content type e l'ancora mc:AlternateContent prodotta da XlsxChartExAnchorPayloadXml. Il contatore indipendente conta: una cartella di lavoro con due grafici classici e un waterfall ottiene chart1.xml, chart2.xml e chartEx1.xml, non chartEx3.xml. I treemap poi differiscono dai waterfall in due dettagli, entrambi gestiti dallo stesso builder. La dimensione numerica cambia tipo: cx:numDim type="val" per un waterfall diventa type="size" per un treemap, perché un rettangolo treemap codifica la grandezza come area invece che come valore tracciato, e i treemap di solito vogliono una gerarchia di categorie, che il modello esprime tramite TXLSXChartSeries.CategoriesMultiLevel. Imposta quel flag e S.ResolveCategoryLevels legge l'intervallo dal foglio di lavoro al momento della scrittura, lo divide in un TXLSXChartDataCache per livello dietro S.CategoryLevelCount e S.CategoryLevels[], e BuildChartExLevelXml emette un cx:lvl per livello dentro un singolo cx:strDim, livello foglia per primo, lo stesso ordinamento fisico che ECMA-376 Part 1 §21.2 usa per c:multiLvlStrCache, quindi quella convenzione passa oltre anche se nessuno dei markup lo fa
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // due colonne: etichetta della foglia, poi gruppo padre
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
La forma delle cache che infrange ogni assunzione dei grafici classici
Se hai scritto un reader di cache per grafici classici, tre abitudini ti tradiranno su una parte chartEx, e tutte e tre vengono da una sola decisione di design: chartex memorizza in attributi e contenuto di elemento ciò che DrawingML memorizza in elementi figli
- Un punto in cache è
<cx:pt idx="0">42</cx:pt>. Il valore è il contenuto dell'elemento. Non c'è un figlioc:vin cui scendere, quindi un parser che cammina cercando nodivnon trova nulla e riporta una cache vuota ptCounteformatCodesono attributi sucx:lvl. In unac:numCacheclassica entrambi sono elementi figli- Più livelli di gerarchia sono più elementi
cx:lvldentro un solocx:strDim. Non esiste un elemento di cache multilivello separato comec:multiLvlStrCacheè separato dac:strCache
BuildChartExLevelXml scrive quella forma e ParseChartExXml la rilegge, facendo passare ogni valore da XlsxEscapeText in uscita così un'etichetta di categoria con una e commerciale o una parentesi angolare sopravvive al round trip. Vale la pena notare cosa fa il builder quando un livello numerico non porta un format code: scrive formatCode="General" invece di omettere l'attributo, perché qui l'attributo non è opzionale come lo è la sua controparte classica
Il prefisso di namespace che faceva fallire la rilettura del tipo
La prima versione del parser chartEx di HotXLS scriveva file perfettamente validi e ne rileggeva ognuno come un grafico a colonne. La causa principale era una riga, e la lezione si generalizza a qualsiasi parser OOXML tu scriverai mai: TXMLReader.Name restituisce il nome come scritto nel documento, prefisso incluso. In una parte di grafico classica la maggior parte degli elementi arriva senza prefisso, quindi SameText(Reader.Name, 'series') funziona e smetti di pensarci. In una parte chartEx ogni elemento è prefissato: il reader ti consegna 'cx:series', il confronto fallisce in silenzio su tutti, e non ottieni né eccezioni né XML malformato, solo una lista di serie vuota e un tipo di grafico che ripiega sul default. Il fix è confrontare sul local name, in un punto solo invece che in ogni chiamata
// Reader.Name porta il suo prefisso di namespace: 'cx:series', non '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 usa quell'helper per ogni test sugli elementi e un helper equivalente AttrVal per gli attributi, visto che anche cx:autoUpdate e compagni sono prefissati. Una volta che gli elementi di serie vengono davvero visti, il tipo torna dal primo layoutId incontrato, e ogni cx:series viene collegata al suo blocco cx:data facendo combaciare cx:dataId/@val con cx:data/@id. I grafici aperti ma mai modificati prendono invece il percorso più vecchio: i byte originali vengono riprodotti da FRawChartXml, quindi un file del cliente non toccato esce com'era entrato, la stessa disciplina di scrittore conservativo che governa le estensioni a livello di foglio in l'articolo sulle estensioni sparkline
Cosa HotXLS non fa deliberatamente con questi grafici
Due confini meritano di essere dichiarati chiaramente, perché entrambi sono scelte e non lacune in attesa di essere colmate. I renderer PDF e SVG integrati non disegnano le serie waterfall o treemap: i tipi non corrispondono a nessun ramo di disegno, quindi il rendering delle serie viene saltato e il resto del foglio viene esportato normalmente, e fingere un treemap dall'aspetto plausibile in un PDF esportato sarebbe peggio che non disegnare nulla, perché sarebbe in silenzioso disaccordo con ciò che Excel mostra per lo stesso file. E xlsxChartWaterfall e xlsxChartTreemap sono gli unici due layout estesi che HotXLS costruisce dal modello; ST_SeriesLayout definisce anche sunburst, funnel, box-and-whisker e histogram, e una cartella di lavoro che ne contiene uno qualsiasi fa comunque il round trip attraverso il percorso di replay dell'XML grezzo, solo che oggi non puoi crearne uno dal codice Delphi. Il rendering dei tipi di grafico classici è invariato e coperto in l'articolo su grafici, immagini e disegni, e la pipeline chartEx viaggia accanto ad esso nella HotXLS Delphi Excel Component per Delphi e C++Builder