Technický článek

Skupiny sekundární osy grafu BIFF8 v Delphi s HotXLS

HotXLS zapisuje sekundární skupiny os grafů BIFF8 vyemitováním druhého bloku AxisParent, nikoli připojením druhé skupiny grafu za osy. V chart substreamu klasického XLS žije každá skupina grafu — ChartFormat, rekord typu grafu a CrtLink — uvnitř vlastního bloku skupiny os a každá série se na jednu váže přes SerToCrt. Obraťte toto vnoření a žádná druhá skupina grafu nebude existovat, na kterou by se série mohla navázat, ať vyemitujete os kolik chcete

Proč se druhá skupina grafu za osami na nic nenaváže

Celá odpověď je v gramatice a má podobu jednoho řádku ABNF. Pravidlo CHARTFOMATS v [MS-XLS] 2.1.7.20.1 říká AxesUsed 1*2AXISPARENT a potom rozepisuje AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Přečtěte tyto dvě produkce dohromady a tvar vyplyne: skupiny grafů jsou děti skupiny os, nikoli její sourozenci. Chart substream se dvěma dvojicemi os a jednou koncovou skupinou grafu není graf se dvěma osami a drobnou zvláštností layoutu; je to graf s jednou skupinou grafu a sadou osiřelých rekordů os. Záleží na tom, protože SerToCrt ($1045), který leží uvnitř bloku SERIESFORMAT, nese index skupiny grafu od nuly, nikoli index osy. Zápis crt = 1, když existuje jen jeden blok CRT, ukáže sérii na skupinu grafu, která nikdy nebyla vyemitována. Intuice, která lidi mate, vychází z názvu rekordu: AXESUSED ($1046) zní, jako by počítal osy, takže přirozeným krokem je vyemitovat další osy. Ve skutečnosti počítá skupiny os a každá skupina os s sebou přináší kompletní plot area i skupinu grafu

Označení série pro sekundární skupinu os

Na straně HotXLS se to zredukuje na jeden Boolean. TXLSChartSeriesInfo obsahuje pole SecondaryAxis a jeho nastavení u libovolné série v poli předaném do TXLSWorksheets.AddChartSheet přepne celý builder do režimu dvou skupin. Neexistuje samostatné volání „enable secondary axis“ ani parametr počtu os, protože počet lze odvodit: pokud některá série chce sekundární skupinu, graf potřebuje dvě

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... naplň A1:C12 kategoriemi, tržbami a marží ...

    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 se stane 2

    Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
      'Revenue vs Margin', '', '', Series);
    Wb.SaveAs('dual-axis.xls', xlExcel97);
  finally
    Wb.Free;
  end;
end;

Řádek Default(TXLSChartSeriesInfo) není dekorace. TXLSChartSeriesInfo míchá managed fields (jména WideString, dynamická pole trendline a error-bar) s obyčejnými členy Boolean a Delphi zaručuje automatické vyčištění pouze managed fields. Nechte SecondaryAxis neinicializované a bude obsahovat cokoli, co leželo na stacku, což v praxi znamená, že stejný binární soubor vytvoří jednoosý graf z console hostu a dvouosý graf pod test runnerem. Rozsahy kategorií a hodnot se mezitím vyřeší přes tabulku EXTERNSHEET workbooku ještě předtím, než je builder uvidí — stejná indexová mechanika je popsaná v článku jak HotXLS klasifikuje externí odkazy BIFF SupBook a XTI — takže rozsah s neznámým sheetem degraduje na prázdný placeholder BRAI místo selhání buildu

Co HotXLS emituje, když je série sekundární

Emitter mění tvar, nejen hodnotu. Bez sekundární série obsahuje AXESUSED 1 (nebo 0 pro pie a 3D pie, které vůbec nemají skupiny os) a následuje jeden blok AxisParent. S jednou sekundární sérií obsahuje AXESUSED 2 a builder blok spustí dvakrát, přičemž iax — první slovo 18bajtového payloadu AxisParent ($1041) — nastaví nejprve na 0 a potom na 1. Každý průchod emituje Pos, category axis a value axis (Axis, $101D), marker PlotArea ($1035), výchozí Frame, potom ChartFormat ($1014), rekord typu grafu, CrtLink ($1022) a dva markery End k uzavření skupiny grafu a skupiny os. Sekundární série se potom naváže přes SerToCrt crt = 1 a primární si ponechá crt = 0. Jeden rekord se záměrně neduplikuje: legend se emituje jen v první skupině, protože Excel dává grafu jedinou legendu bez ohledu na počet skupin os. Dvě další vlastnosti stojí za jasné uvedení. Restrukturalizace emitteru tak, aby skupina grafu ležela uvnitř bloku axis-parent, nezměnila výstup běžných grafů — bez sekundární série je substream bajtově shodný s předchozí verzí, protože parametrizovat AddAxisParent hodnotou iax = 0 je přesně stará cesta. Builder také stále emituje úplný pár os pro každou skupinu, takže sekundární skupina vždy přijde s vlastní category axis, i když vám záleží jen na její value scale

Jak inspekce grafu obnoví vazbu na skupinu os

Čtení probíhá ve dvou průchodech seznamu rekordů a musí tomu tak být, protože AXESUSED přichází před bloky, které popisuje. První průchod hledá pouze $1046 a jeho první slovo přečte jako počet skupin os. Tato hodnota začíná na 1 a pouze roste: HotXLS bere maximum aktuálního počtu a deklarované hodnoty, takže poškozený nebo duplicitní AXESUSED nemůže zhoršit graf, u něhož už bylo zaznamenáno deklarování dvou skupin. Druhý průchod sleduje aktuální skupinu os, aktualizuje ji u každého AxisParent a tento index otiskne do každého rekordu Axis, na který narazí, dokud se neobjeví další AxisParent

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] je datová 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;

Dvě omezení stojí za pojmenování. TXLSChartModel.AxisGroupCount hlásí, co soubor deklaruje, nikoli kolik bloků AxisParent se skutečně našlo; soubor, který říká 2 a dodá jeden blok, ohlásí 2 a místo, kde si toho všimnete, je AxisCount. A TXLSChartAxis.AxisGroup je poziční otisk: zaznamenává, uvnitř kterého bloku byla osa načtena, což je jediná informace, kterou formát poskytuje. Na straně sérií je dekódování SerToCrt podmíněné tím, že jsme uvnitř bloku Series, protože stejné ID rekordu se objevuje v kontextech, kde nejde o vazbu série, a decoder bez guardu by ochotně přepsal špatnou sérii

Ověření sekundárních os bez skutečného souboru Excelu

Ověření zde nepotřebovalo soubor Excelu se sekundární osou, a právě to je užitečná část příběhu. Strukturální dekódování je vlastností posloupnosti rekordů, takže syntetizovaná posloupnost ho prokáže stejně přesně jako zachycená. Regrese sestaví AXESUSED s payloadem 2, potom dva bloky AxisParent, každý obaluje category axis a value axis, a ověří, že model se vrátí s AxisGroupCount = 2, čtyřmi osami označenými 0, 0, 1, 1 a očekávanými typy os na druhé dvojici

// Strukturální ověření zcela bez souboru Excelu
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: dvě 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;

Při vlastní syntéze rekordů si všimněte dvou praktických věcí. TXLSCustomChart.AddData(RecID, Len, nil) dereferencuje payload, když Len není nula, takže markery Begin ($1033) a End ($1034) se musí přidat s délkou nula, nikoli s nil blobem a zapomenutou délkou. Syntetická posloupnost také prokazuje decoder, nikdy ne akceptaci výstupu Excelem — write side vznikla z ABNF a pak se kontrolovala round-tripem přes GetChartModel, s assertion per-axis otisků 0/0/1/1 a per-series skupin grafu 0/1, přičemž bezpečnostní sítí je bajtově identická cesta bez sekundární skupiny. Je to stejný konzervativní postoj, na němž stojí podpora grafů, obrázků a drawingů HotXLS pro Delphi: dekódujte, co říkají rekordy, a odmítněte hádat binární layout, který jste ve specifikaci nečetli. Celá sada Delphi po dokončení write side prošla 1650 z 1650 na Win32 i Win64

Parametry scény Chart3d a past fAuto

Dva sousední detaily potrápí každého, kdo se vydá za výchozí graf. Prvním je Chart3d ($103A, [MS-XLS] 2.4.46), plochý payload o 14 bajtech emitovaný uvnitř skupiny grafu pro 3D varianty: anRot (rotace, 0 až 360), anElev (elevace, signed, -90 až 90), pcDist (perspektivní vzdálenost, 0 až 100, ignorovaná, pokud není nastaveno fPerspective), pcHeight a pcDepth (procento šířky grafu, 5 až 500), pcGap (0 až 500) a grbit, jehož bity jsou fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 a f2DWalls $0020. Specifikace přidává omezení, která layout rekordu za vás nevynutí: u transponovaného sloupcového grafu nesmí anRot a anElev překročit 44 a u pie grafu nesmí být anElev záporné

Druhým je bit fAuto a právě on vytváří hlášení „ignorují se mi barvy“. LineFormat ($1007), AreaFormat ($100A) i MarkerFormat ($1009) nesou fAuto v bitu 0 svého grbit a při jeho nastavení Excel použije automatický styl a explicitní RGB hodnoty, styl čáry, tvar markeru i velikost markeru vedle něj bere jako dekoraci. Emitter zapisující vlastní styl série musí bit 0 vyčistit; výchozí emitery ho ponechávají nastavený přesně proto, aby Excel vybral paletu. Pokud upravujete existující workbook místo sestavení nového, pravidla zachování jsou znovu jiná a pokrývá je úprava grafů Excelu bez ztráty zachovaného ChartML

Skupiny sekundárních os, vazba SerToCrt a typovaný model grafu popsaný zde jsou součástí HotXLS Delphi spreadsheet component pro Delphi a C++Builder, který čte a zapisuje grafy BIFF8 bez instalovaného Excelu; produktová stránka obsahuje úplnou referenci rekordů grafu a seznam overloadů AddChartSheet