HotXLS zapisuje sekundárne skupiny osí grafu BIFF8 tak, že emituje druhý blok AxisParent, nie tak, že za osi pripojí druhý chart group. V Classic XLS chart substreame žije každá chart group — ChartFormat, record typu grafu, CrtLink — vo vnútri vlastného bloku skupiny osí a každá séria sa na jednu viaže cez SerToCrt. Otočte toto vnorenie a pre sériu nemáte druhú chart group, nech emitujete akokoľvek veľa recordov osí
Prečo druhá chart group za osami na nič nenaviaže?
Celou odpoveďou je gramatika a je to jeden riadok ABNF. Pravidlo CHARTFOMATS v [MS-XLS] 2.1.7.20.1 hovorí AxesUsed 1*2AXISPARENT a potom rozpisuje AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Prečítajte tieto dve produkcie spolu a tvar vyplynie sám: chart groups sú deťmi skupiny osí, nie jej súrodencami. Chart substream s dvoma pármi osí a jednou trailing chart group nie je dual-axis graf s layout zvláštnosťou; je to graf s jednou chart group a množinou osirelých recordov osí. Záleží na tom, pretože SerToCrt ($1045), ktorý sedí v bloku SERIESFORMAT, nesie zero-based index chart group, nie index osi. Zápis crt = 1, keď existuje iba jeden CRT blok, namieri sériu na chart group, ktorá nikdy nebola emitovaná. Intuícia, na ktorej sa ľudia potkýnajú, je názov recordu: AXESUSED ($1046) znie, akoby počítal osi, takže prirodzeným ďalším krokom je emitovať viac osí. Počíta skupiny osí a každá skupina osí so sebou nesie úplnú plot area a chart group
Označenie série pre sekundárnu skupinu osí
Na strane HotXLS sa to zrúti na jeden boolean. TXLSChartSeriesInfo nesie pole SecondaryAxis a jeho nastavenie na ľubovoľnej sérii v poli, ktoré odovzdáte do TXLSWorksheets.AddChartSheet, prepne celý builder do režimu dvoch skupín. Neexistuje samostatné volanie „enable secondary axis“ ani parameter počtu osí, pretože počet sa dá odvodiť: ak ktorákoľvek séria chce sekundárnu skupinu, graf potrebuje dve
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... naplň A1:C12 kategóriami, revenue a margin ...
Series[0] := Default(TXLSChartSeriesInfo); // tento record nikdy neplň FillChar
Series[0].Name := 'Revenue';
Series[0].Categories := 'Data!$A$1:$A$12';
Series[0].Values := 'Data!$B$1:$B$12';
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'Margin';
Series[1].Categories := 'Data!$A$1:$A$12';
Series[1].Values := 'Data!$C$1:$C$12';
Series[1].SecondaryAxis := True; // AXESUSED bude 2
Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
'Revenue vs Margin', '', '', Series);
Wb.SaveAs('dual-axis.xls', xlExcel97);
finally
Wb.Free;
end;
end;
Riadok Default(TXLSChartSeriesInfo) nie je dekorácia. TXLSChartSeriesInfo mieša managed fields (názvy WideString, dynamickú array trendline a error-bar) s obyčajnými členmi Boolean a Delphi garantuje, že za vás vyčistí iba managed fields. Nechajte SecondaryAxis neinicializovaný a bude tým, čo bolo na stacku, čo v praxi znamená, že ten istý binary vytvorí pri spustení z console hosta jednoosový graf a pod test runnerom dvojosový graf. Rozsahy kategórií a hodnôt sa medzitým vyriešia cez workbookovu tabuľku EXTERNSHEET ešte skôr, než ich builder uvidí — rovnaký indexový mechanizmus, aký pokrýva článok o klasifikácii externých odkazov BIFF SupBook a XTI v HotXLS — takže rozsah s neznámym sheetom degraduje na prázdny placeholder BRAI namiesto zlyhania buildu
Čo HotXLS emituje, keď je séria sekundárna
Emitter mení tvar, nie iba hodnotu. Bez sekundárnej série drží AXESUSED hodnotu 1 (alebo 0 pri pie a 3D pie, ktoré vôbec nemajú skupiny osí) a nasleduje jeden blok AxisParent. S jednou sekundárnou sériou drží AXESUSED hodnotu 2 a builder spustí blok dvakrát, pričom iax — prvé slovo 18-bajtového payloadu AxisParent ($1041) — nastaví najprv na 0 a potom na 1. Každý prechod emituje Pos, category axis a value axis (Axis, $101D), marker PlotArea ($1035), predvolený Frame, potom ChartFormat ($1014), record typu grafu, CrtLink ($1022) a dva markery End na uzavretie chart group aj skupiny osí. Sekundárna séria sa potom naviaže cez SerToCrt crt = 1 a primárna ponechá crt = 0. Jeden record sa zámerne neduplikuje: legend sa emituje iba v prvej skupine, pretože Excel dá grafu jednu legendu bez ohľadu na počet skupín osí. Ďalšie dve vlastnosti treba povedať priamo. Reštrukturalizácia emittera tak, aby chart group vložil dovnútra bloku axis-parent, nezmenila výstup obyčajných grafov — bez sekundárnej série je substream bajtovo identický s predchádzajúcou verziou, pretože parametrizácia AddAxisParent s iax = 0 je presne stará code path. Builder stále emituje úplný pár osí pre každú skupinu, takže sekundárna skupina vždy príde s vlastnou category axis, aj keď vám záleží iba na jej value scale
Ako inspection grafu obnoví väzbu na skupinu osí?
Čítanie je dvojprechod cez zoznam recordov a musí ním byť, pretože AXESUSED príde pred blokmi, ktoré opisuje. Prvý prechod hľadá iba $1046 a číta jeho prvé slovo ako počet skupín osí. Táto hodnota začína na 1 a iba rastie, nikdy neklesá: HotXLS vezme maximum aktuálneho počtu a deklarovaného, takže chybný alebo duplikovaný AXESUSED nemôže znížiť graf, o ktorom už videl, že deklaruje dve skupiny. Druhý prechod sleduje aktuálnu skupinu osí, aktualizuje ju pri každom AxisParent a túto hodnotu opečiatkuje na každý record Axis, na ktorý narazí, až kým sa neobjaví ďalší AxisParent
var
Model: TXLSChartModel;
i: Integer;
begin
// Sheets[1] je dátový worksheet, Sheets[2] chart sheet
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
if Model.AxisGroupCount = 2 then
Writeln('AXESUSED declares a secondary axis group');
for i := 0 to Model.AxisCount - 1 do
Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
for i := 0 to Model.SeriesCount - 1 do
Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
finally
Model.Free;
end;
end;
Dve obmedzenia sa oplatí pomenovať. TXLSChartModel.AxisGroupCount hlási, čo súbor deklaruje, nie koľko blokov AxisParent sa skutočne našlo; súbor, ktorý hovorí 2 a dodá jeden blok, nahlási 2 a AxisCount je miesto, kde si to všimnete. A TXLSChartAxis.AxisGroup je pozičná pečiatka: zaznamenáva, v ktorom bloku bola os načítaná, čo je jediná vec, ktorú vám formát povie. Na strane sérií je dekódovanie SerToCrt podmienené tým, že ste vo vnútri Series bloku, pretože rovnaké record id sa objavuje v kontextoch, kde nie je väzbou série, a nekontrolovaný dekóder by ochotne prepísal nesprávnu sériu
Overenie sekundárnych osí bez skutočného súboru Excel
Overenie tu nepotrebovalo súbor Excel so sekundárnou osou a to je užitočná časť príbehu. Štrukturálne dekódovanie je vlastnosťou sekvencie recordov, takže syntetizovaná sekvencia ju dokáže preukázať rovnako presne ako zachytená. Regresia zostaví AXESUSED s payloadom 2, potom dva bloky AxisParent, z ktorých každý obalí category axis a value axis, a overí, že model sa vráti s AxisGroupCount = 2, štyrmi osami opečiatkovanými 0, 0, 1, 1 a očakávanými typmi osí v druhom páre
// Štrukturálne overenie úplne bez súboru Excel
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED: dve skupiny osí
AddAxisParentGroup(0); // AxisParent iax=0 + Begin + 2 Axis + End
AddAxisParentGroup(1); // AxisParent iax=1 + Begin + 2 Axis + End
Model := Chart.GetChartModel;
Assert.AreEqual(2, Model.AxisGroupCount);
Assert.AreEqual(4, Model.AxisCount);
Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
Model.Free;
Chart.Free;
end;
Dve praktické poznámky, ak si recordy syntetizujete sami. TXLSCustomChart.AddData(RecID, Len, nil) dereferencuje payload, keď Len nie je nula, takže markery Begin ($1033) a End ($1034) treba pridať s nulovou dĺžkou, nie s nil blobom a zastaranou dĺžkou. A syntetická sekvencia dokazuje dekóder, nikdy nie akceptáciu vášho výstupu Excelom — write side bola štruktúrovaná z ABNF a potom skontrolovaná round-tripom cez GetChartModel, s assertionmi pozičných pečiatok 0/0/1/1 na osiach a chart groups 0/1 na sériách, pričom bezpečnostnou sieťou je bajtovo identická cesta bez sekundárnej skupiny. Je to rovnaký konzervatívny postoj, na ktorom stojí zvyšok podpory grafov, obrázkov a kreslenia HotXLS pre Delphi: dekódujte, čo recordy hovoria, a odmietnite hádať binárny layout, ktorý ste nečítali v špecifikácii. Úplná Delphi suite po nasadení write side prešla 1650 z 1650 na Win32 aj Win64
Parametre scény Chart3d a pasca fAuto
Dva susedné detaily potrápia ľudí, ktorí idú za predvolený graf. Prvým je Chart3d ($103A, [MS-XLS] 2.4.46), plochý 14-bajtový payload emitovaný vo vnútri chart group pre 3D varianty: anRot (rotácia, 0 až 360), anElev (elevácia, signed, -90 až 90), pcDist (perspektívna vzdialenosť, 0 až 100, ignorovaná, pokiaľ nie je nastavené fPerspective), pcHeight a pcDepth (percento šírky grafu, 5 až 500), pcGap (0 až 500) a grbit, ktorého bity sú fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 a f2DWalls $0020. Špecifikácia pridáva obmedzenia, ktoré layout recordu za vás nevynúti: pri transponovanom stĺpcovom grafe anRot a anElev nesmú prekročiť 44 a pri pie grafe anElev nesmie byť záporné
Druhým je bit fAuto a práve on vyrába bug report „moje farby boli ignorované“. LineFormat ($1007), AreaFormat ($100A) aj MarkerFormat ($1009) nesú fAuto v bite 0 svojho grbit a keď je bit nastavený, Excel použije automatický štýl a explicitné RGB hodnoty, line style, tvar markera aj veľkosť markera vedľa neho považuje za dekoráciu. Každý emitter zapisujúci vlastný štýl série musí bit 0 vymazať; predvolené emitery ho držia nastavený práve preto, aby Excel vybral paletu. Ak upravujete existujúci workbook namiesto jeho tvorby, pravidlá zachovania sa líšia znova a pokrýva ich článok o úprave Excel grafov bez straty zachovaného ChartML
Sekundárne skupiny osí, väzba SerToCrt a typovaný chart model zobrazený tu sa dodávajú v HotXLS Delphi spreadsheet component pre Delphi a C++Builder, ktorý číta a zapisuje grafy BIFF8 bez nainštalovaného Excelu; produktová stránka obsahuje úplnú referenciu chart recordov a zoznam overloadov AddChartSheet