Artykuł techniczny

Grupy osi pomocniczych wykresu BIFF8 w Delphi z HotXLS

HotXLS zapisuje grupy osi pomocniczych wykresu BIFF8 przez emisję drugiego bloku AxisParent, a nie przez dopisanie drugiej grupy wykresu za osiami. W podstrumieniu wykresu Classic XLS każda grupa wykresu — ChartFormat, rekord typu wykresu i CrtLink — mieści się we własnym bloku grupy osi, a każda seria wiąże się z nim przez SerToCrt. Odwróć to zagnieżdżenie, a dla serii nie będzie drugiej grupy wykresu, z którą mogłaby się związać, niezależnie od liczby wyemitowanych rekordów osi

Dlaczego druga grupa wykresu za osiami nie wiąże niczego?

Całą odpowiedź zawiera gramatyka, a dokładnie jedna linia ABNF. Reguła CHARTFOMATS w [MS-XLS] 2.1.7.20.1 mówi AxesUsed 1*2AXISPARENT, a następnie rozwija AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Przeczytaj te dwie produkcje razem, a kształt stanie się jasny: grupy wykresu są dziećmi grupy osi, a nie jej rodzeństwem. Podstrumień wykresu z dwiema parami osi i jedną końcową grupą wykresu nie jest wykresem z dwiema osiami i dziwnym layoutem, tylko wykresem z jedną grupą wykresu oraz zestawem osieroconych rekordów osi. To ważne, ponieważ SerToCrt ($1045), znajdujący się wewnątrz bloku SERIESFORMAT, zawiera indeks grupy wykresu liczony od zera, a nie indeks osi. Zapisanie crt = 1, gdy istnieje tylko jeden blok CRT, kieruje serię do grupy wykresu, której nigdy nie wyemitowano. Ludziom myli się nazwa rekordu: AXESUSED ($1046) brzmi tak, jakby liczył osie, więc naturalnym następnym krokiem jest emisja kolejnych osi. Liczy grupy osi, a każda grupa osi przyciąga za sobą pełny obszar wykresu i grupę wykresu

Gramatyka wykresu BIFF8, której HotXLS przestrzega w Delphi: CHARTFOMATS czyta AxesUsed 1*2AXISPARENT, a AXISPARENT opakowuje AxisParent, Begin, Pos, osie i od jednego do czterech bloków CRT, więc każda grupa wykresu ChartFormat i CrtLink żyje wewnątrz grupy osi, podczas gdy grupa wykresu wyemitowana po End pozostawia SerToCrt wskazujący na nic
ABNF czyni zagnieżdżenie jawnym: grupa wykresu jest dzieckiem swojej grupy osi, więc końcowy CRT po osiach nie jest grupą pomocniczą, lecz zestawem osieroconych rekordów

Oznaczanie serii dla pomocniczej grupy osi

Po stronie HotXLS sprowadza się to do jednego Boolean. TXLSChartSeriesInfo zawiera pole SecondaryAxis, a ustawienie go dla dowolnej serii w tablicy przekazanej do TXLSWorksheets.AddChartSheet przełącza cały builder w tryb dwóch grup. Nie ma osobnego wywołania „włącz oś pomocniczą” ani parametru liczby osi, bo tę liczbę można wyprowadzić: jeśli dowolna seria potrzebuje grupy pomocniczej, wykres potrzebuje dwóch

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... wypełnij A1:C12 kategoriami, przychodem i marżą ...

    Series[0] := Default(TXLSChartSeriesInfo);   // nigdy nie używaj FillChar dla tego rekordu
    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 przyjmuje wartość 2

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

Wiersz Default(TXLSChartSeriesInfo) nie jest ozdobnikiem. TXLSChartSeriesInfo łączy pola zarządzane (nazwy WideString, dynamiczną tablicę linii trendu i tablicę pasków błędów) ze zwykłymi składowymi typu Boolean, a Delphi gwarantuje wyczyszczenie tylko pól zarządzanych. Pozostaw SecondaryAxis niezainicjalizowane, a będzie zawierało to, co akurat leżało na stosie, co w praktyce oznacza, że ten sam plik binarny tworzy wykres z jedną osią uruchomiony z hosta konsolowego, a wykres z dwiema osiami uruchomiony przez test runner. Zakresy kategorii i wartości tymczasem rozwiązuje się przez tabelę EXTERNSHEET skoroszytu, zanim builder w ogóle je zobaczy — to ta sama mechanika indeksów opisana w artykule o klasyfikowaniu przez HotXLS zewnętrznych odsyłaczy BIFF SupBook i XTI — więc zakres wskazujący nieznany arkusz zamienia się w pusty placeholder BRAI, zamiast przerwać budowanie

Co emituje HotXLS, gdy seria jest pomocnicza

Emitter zmienia kształt, nie tylko wartość. Bez serii pomocniczej AXESUSED ma wartość 1 (albo 0 dla wykresu kołowego i kołowego 3D, które w ogóle nie mają grup osi), po czym pojawia się jeden blok AxisParent. Z jedną serią pomocniczą AXESUSED ma wartość 2, a builder wykonuje blok dwukrotnie, ustawiając iax — pierwsze słowo 18-bajtowego payloadu AxisParent ($1041) — najpierw na 0, a potem na 1. Każde przejście emituje Pos, oś kategorii i oś wartości (Axis, $101D), znacznik PlotArea ($1035), domyślną Frame, następnie ChartFormat ($1014), rekord typu wykresu, CrtLink ($1022) oraz dwa znaczniki End zamykające grupę wykresu i grupę osi. Seria pomocnicza wiąże się wtedy przez SerToCrt crt = 1, a główna zachowuje crt = 0. Jeden rekord celowo nie jest duplikowany: legenda jest emitowana tylko w pierwszej grupie, ponieważ Excel daje wykresowi jedną legendę niezależnie od liczby grup osi. Warto jasno powiedzieć jeszcze dwie rzeczy. Przebudowanie emittera tak, aby umieszczał grupę wykresu wewnątrz bloku axis-parent, nie zmieniło wyjścia zwykłych wykresów — bez serii pomocniczej podstrumień jest identyczny bajt po bajcie z poprzednią wersją, ponieważ sparametryzowanie AddAxisParent przez iax = 0 to dokładnie stara ścieżka kodu. Builder nadal emituje pełną parę osi dla każdej grupy, więc grupa pomocnicza zawsze przychodzi z własną osią kategorii, nawet gdy interesuje cię tylko jej skala wartości

Sekwencja rekordów emitowana przez HotXLS dla dwuosiowego arkusza wykresu w Delphi: SecondaryAxis True podnosi AXESUSED do 2, dwa bloki AxisParent z iax 0 i 1 niosą każdy Pos, Axis, PlotArea, Frame, ChartFormat, rekord typu wykresu i CrtLink, a SerToCrt wiąże serię główną z grupą 0, a pomocniczą z grupą 1
Ustawienie SecondaryAxis na jednej serii przełącza buildera w tryb dwóch grup, a każde przejście emituje kompletną parę osi i grupę wykresu, przy czym legenda jest zapisywana celowo tylko raz

Jak inspekcja wykresu odtwarza wiązanie grupy osi?

Odczyt odbywa się w dwóch przejściach po liście rekordów i musi tak wyglądać, ponieważ AXESUSED pojawia się przed blokami, które opisuje. Pierwsze przejście szuka tylko $1046 i odczytuje jego pierwsze słowo jako liczbę grup osi. Ta wartość zaczyna się od 1 i może tylko rosnąć: HotXLS bierze maksimum bieżącej liczby i zadeklarowanej, więc zniekształcony albo zduplikowany AXESUSED nie cofnie wykresu, który już został rozpoznany jako deklarujący dwie grupy. Drugie przejście śledzi bieżącą grupę osi, aktualizując ją przy każdym AxisParent, i stempluje ten indeks na każdym napotkanym rekordzie Axis aż do pojawienia się następnego AxisParent

Jak inspekcja wykresu HotXLS odtwarza wiązania grup osi w dwóch przebiegach: pierwszy skan czyta słowo AXESUSED $1046 jako liczbę grup i nigdy go nie obniża, drugi śledzi bieżący AxisParent i stempluje każdy rekord Axis tym indeksem, a model raportuje AxisGroupCount, stemple osi 0 0 1 1 oraz grupy serii 0 i 1
Dwa przebiegi są konieczne, ponieważ AXESUSED przychodzi przed blokami, które opisuje, więc liczbę czyta się najpierw, a pozycyjne stemple osi nakłada się w drugim przejściu po liście rekordów
var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] to arkusz danych, a Sheets[2] to arkusz wykresu
  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;

Warto nazwać dwa ograniczenia. TXLSChartModel.AxisGroupCount raportuje to, co deklaruje plik, a nie liczbę faktycznie znalezionych bloków AxisParent; plik mówiący 2 i zawierający jeden blok nadal zgłosi 2, a AxisCount wskaże, gdzie to zauważyć. TXLSChartAxis.AxisGroup jest stemplem pozycyjnym: zapisuje, wewnątrz którego bloku odczytano oś, i to jedyna informacja przekazywana przez format. Po stronie serii dekodowanie SerToCrt jest włączane dopiero wewnątrz bloku Series, ponieważ ten sam identyfikator rekordu pojawia się w kontekstach, w których nie jest wiązaniem serii, a dekoder bez sprawdzenia kontekstu chętnie nadpisałby niewłaściwą serię

Weryfikowanie osi pomocniczych bez prawdziwego pliku Excela

Ta weryfikacja nie potrzebowała pliku Excela z osią pomocniczą i to jest użyteczna część historii. Dekodowanie strukturalne jest własnością sekwencji rekordów, więc syntetyczna sekwencja dowodzi tego równie dokładnie jak przechwycona. Regresja buduje AXESUSED z payloadem równym 2, potem dwa bloki AxisParent, z których każdy opakowuje oś kategorii i oś wartości, i sprawdza, czy model wraca z AxisGroupCount = 2, czterema osiami oznaczonymi 0, 0, 1, 1 oraz oczekiwanymi typami osi w drugiej parze

// Weryfikacja strukturalna bez udziału pliku Excela
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: dwie grupy osi
  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;

Dwie praktyczne uwagi, jeśli sam syntetyzujesz rekordy. TXLSCustomChart.AddData(RecID, Len, nil) wyłuskuje payload, gdy Len jest różne od zera, więc znaczniki Begin ($1033) i End ($1034) trzeba dodawać z długością zero, a nie z nil blobem i nieaktualną długością. Syntetyczna sekwencja dowodzi jednak dekodera, nigdy akceptacji wyjścia przez Excela — stronę zapisu zbudowano na podstawie ABNF, a potem sprawdzono przez cykl z GetChartModel, asercje stempli osi 0/0/1/1 i grup wykresu serii 0/1, z identyczną bajtowo ścieżką bez osi pomocniczej jako zabezpieczeniem. To ta sama ostrożna postawa, na której opiera się reszta obsługi wykresów, obrazów i rysunków HotXLS dla Delphi: dekoduj to, co mówią rekordy, i nie zgaduj binarnego layoutu, którego nie przeczytałeś w specyfikacji. Pełny zestaw Delphi przeszedł 1650 z 1650 testów na Win32 i Win64 po wdrożeniu strony zapisu

Parametry sceny Chart3d i pułapka fAuto

Dwa sąsiadujące szczegóły dają się we znaki, gdy wychodzisz poza domyślny wykres. Pierwszy to Chart3d ($103A, [MS-XLS] 2.4.46), płaski payload o długości 14 bajtów emitowany wewnątrz grupy wykresu dla wariantów 3D: anRot (obrót, od 0 do 360), anElev (wzniesienie, ze znakiem, od -90 do 90), pcDist (odległość perspektywy, od 0 do 100, ignorowana, chyba że ustawiono fPerspective), pcHeight i pcDepth (procent szerokości wykresu, od 5 do 500), pcGap (od 0 do 500) oraz grbit, którego bity to fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 i f2DWalls $0020. Specyfikacja dodaje ograniczenia, których layout rekordu za ciebie nie wymusi: dla transponowanego wykresu słupkowego anRot i anElev nie mogą przekroczyć 44, a dla wykresu kołowego anElev nie może być ujemne

Drugi to bit fAuto i właśnie on wywołuje zgłoszenie „zignorowano moje kolory”. LineFormat ($1007), AreaFormat ($100A) i MarkerFormat ($1009) niosą fAuto w bicie 0 swojego grbit, a gdy bit jest ustawiony, Excel stosuje automatyczny styl i traktuje jawne wartości RGB, styl linii, kształt znacznika oraz rozmiar znacznika znajdujące się obok jako ozdobniki. Każdy emitter zapisujący własny styl serii musi wyczyścić bit 0; domyślne emitery pozostawiają go ustawionym właśnie po to, aby Excel wybrał paletę. Jeśli edytujesz istniejący skoroszyt zamiast go budować, reguły zachowania są znów inne i opisuje je artykuł o edycji wykresów Excela bez utraty zachowanego ChartML

Grupy osi pomocniczych, wiązanie SerToCrt i pokazany tutaj typowany model wykresu są częścią komponentu arkuszy HotXLS dla Delphi dla Delphi i C++Builder, który odczytuje i zapisuje wykresy BIFF8 bez zainstalowanego Excela; strona produktu zawiera pełną referencję rekordów wykresu i listę przeciążeń AddChartSheet