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

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

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

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