Technisch artikel

BIFF8-chartgroepen met secundaire as in Delphi met HotXLS

HotXLS schrijft BIFF8-chartgroepen voor een secundaire as door een tweede AxisParent-blok uit te geven en niet door na de assen een tweede chartgroep toe te voegen. In elke Classic XLS-chartsubstream leeft elke chartgroep — ChartFormat, het charttyperecord en CrtLink — binnen zijn eigen asgroepblok, en elke serie bindt via SerToCrt aan één groep. Draai die nesting om en er is geen tweede chartgroep waaraan een serie kan binden, hoeveel asrecords je ook uitstuurt

Waarom bindt een tweede chartgroep na de assen nergens aan?

De grammar is het hele antwoord, en het is één regel ABNF. De CHARTFOMATS-regel in [MS-XLS] 2.1.7.20.1 zegt AxesUsed 1*2AXISPARENT en schrijft daarna AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End uit. Lees die twee producties samen en de vorm volgt vanzelf: de chartgroepen zijn children van een asgroep en geen siblings ervan. Een chartsubstream met twee asparen en één afsluitende chartgroep is geen dual-axis-chart met een layoutgril, maar een chart met één chartgroep en een set verweesde asrecords. Dat doet ertoe omdat SerToCrt ($1045), dat in het SERIESFORMAT-blok zit, een zero-based chart group-index bevat en geen asindex. crt = 1 schrijven terwijl er maar één CRT-blok bestaat, wijst een serie naar een chartgroep die nooit is uitgegeven. De intuïtie die mensen op het verkeerde been zet zit in de recordnaam: AXESUSED ($1046) klinkt alsof hij assen telt, dus de natuurlijke volgende stap is meer assen uitgeven. Hij telt asgroepen, en elke asgroep sleept een volledig plotgebied en een chartgroep mee

Een serie markeren voor de secundaire asgroep

Aan de HotXLS-kant reduceert dit tot één Boolean. TXLSChartSeriesInfo bevat een veld SecondaryAxis, en als je dat instelt op een serie in de array die je aan TXLSWorksheets.AddChartSheet meegeeft, schakelt de volledige builder over naar tweegroepenmodus. Er is geen afzonderlijke call "secundaire as inschakelen" en geen parameter voor het aantal assen, omdat dat aantal kan worden afgeleid: als een serie de secundaire groep wil, heeft de chart er twee nodig

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... vul A1:C12 met categorieën, omzet en marge ...

    Series[0] := Default(TXLSChartSeriesInfo);   // initialiseer dit record nooit met 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 wordt 2

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

De regel Default(TXLSChartSeriesInfo) is geen decoratie. TXLSChartSeriesInfo mengt managed fields (de WideString-namen, de dynamische trendline- en error-bar-arrays) met gewone Boolean-members, en Delphi garandeert alleen dat de managed fields voor je worden gewist. Laat je SecondaryAxis ongeïnitialiseerd, dan is het wat er toevallig op de stack stond, wat in de praktijk betekent dat dezelfde binary een single-axis-chart oplevert vanuit een consolehost en een dual-axis-chart onder de testrunner. De category- en value-ranges worden ondertussen via de EXTERNSHEET-table van de workbook geresolved voordat de builder ze ooit ziet — dezelfde indexmachinery die wordt behandeld in hoe HotXLS BIFF SupBook- en XTI-externe links classificeert — dus een range naar een onbekend sheet degradeert naar een lege placeholder BRAI in plaats van de build te laten falen

Wat geeft HotXLS uit wanneer een serie secundair is?

De emitter verandert van vorm en niet alleen van waarde. Zonder secundaire serie bevat AXESUSED de waarde 1 (of 0 voor pie en 3D-pie, die helemaal geen asgroepen hebben) en volgt één AxisParent-blok. Met één secundaire serie bevat AXESUSED de waarde 2 en voert de builder het blok tweemaal uit, met iax — het eerste woord van de payload van 18 bytes van AxisParent ($1041) — eerst op 0 en daarna op 1. Elke pass geeft Pos, de category axis en de value axis (Axis, $101D), de PlotArea-marker ($1035), een default Frame, vervolgens ChartFormat ($1014), het charttyperecord, CrtLink ($1022) en twee End-markers uit om de chartgroep en de asgroep te sluiten. De secundaire serie bindt daarna met SerToCrt crt = 1, en de primaire serie houdt crt = 0. Eén record wordt bewust niet gedupliceerd: de legend wordt alleen in de eerste groep uitgegeven, omdat Excel één legend voor een chart geeft ongeacht hoeveel asgroepen die bevat. Twee andere properties moeten duidelijk worden genoemd. De emitter zo herstructureren dat de chartgroep binnen het axis-parent-blok zit, veranderde de output voor gewone charts niet — zonder secundaire serie is de substream byte-identiek aan de vorige versie, omdat AddAxisParent parametriseren met iax = 0 precies het oude codepad is. En de builder geeft nog steeds één volledig aspaar per groep uit, zodat een secundaire groep altijd zijn eigen category axis krijgt, zelfs als je alleen om zijn valueschaal geeft

Hoe herstelt chartinspectie de binding aan een asgroep?

Lezen gebeurt in twee passes over de recordlijst en dat moet ook, omdat AXESUSED vóór de blokken komt die het beschrijft. De eerste pass zoekt alleen naar $1046 en leest het eerste woord als het aantal asgroepen. Die waarde begint bij 1 en wordt alleen verhoogd, nooit verlaagd: HotXLS neemt het maximum van de huidige count en de gedeclareerde waarde, zodat een misvormde of dubbele AXESUSED een chart die al als twee groepen is gezien niet kan terugzetten. De tweede pass houdt de huidige asgroep bij, werkt die bij bij elke AxisParent en stempelt die index op elk Axis-record dat hij tegenkomt totdat de volgende AxisParent verschijnt

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] is het dataworksheet, Sheets[2] het chartsheet
  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;

Twee grenzen zijn het noemen waard. TXLSChartModel.AxisGroupCount rapporteert wat het bestand declareert en niet hoeveel AxisParent-blokken werkelijk zijn gevonden; een bestand dat 2 zegt en één blok levert, rapporteert 2, en AxisCount is waar je het merkt. En TXLSChartAxis.AxisGroup is een positionele stempel: hij legt vast in welk blok een as is gelezen, het enige dat het formaat je vertelt. Aan de seriekant wordt het decoderen van SerToCrt afgeschermd doordat de decoder binnen een Series-blok moet zitten, omdat dezelfde record-ID in contexten voorkomt waar hij geen serie-binding is en een decoder zonder die guard vrolijk de verkeerde serie zou overschrijven

Secundaire assen controleren zonder een echt Excel-bestand

Voor deze verificatie was geen Excel-bestand met een secundaire as nodig, en dat is het nuttige deel van het verhaal. Structurele decoding is een eigenschap van de recordsequence, dus een gesynthetiseerde sequence bewijst het net zo goed als een opgenomen sequence. De regressietest bouwt AXESUSED met een payload van 2, daarna twee AxisParent-blokken die elk een category axis en een value axis omwikkelen, en assert dat het model terugkomt met AxisGroupCount = 2, vier assen met de stamps 0, 0, 1, 1 en de verwachte asstypen op het tweede paar

// Structurele verificatie waarbij helemaal geen Excel-bestand betrokken is
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: twee asgroepen
  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;

Twee praktische notities als je zelf records synthetiseert. TXLSCustomChart.AddData(RecID, Len, nil) dereferereert de payload wanneer Len niet nul is, dus Begin ($1033)- en End ($1034)-markers moeten worden toegevoegd met een lengte van nul en niet met een nil blob en een verouderde lengte. En een synthetische sequence bewijst de decoder, nooit dat Excel je output accepteert — de write-kant is volgens de ABNF opgebouwd en daarna gecontroleerd met een round-trip door GetChartModel, met per-as-stamps 0/0/1/1 en per-seriechartgroepen 0/1 als assertions en het byte-identieke pad zonder secundaire groep als vangnet. Dat is dezelfde conservatieve houding waarop de rest van de HotXLS-chart-, image- en drawing-support voor Delphi is gebouwd: decodeer wat de records zeggen en weiger een binaire layout te raden die je niet in de specificatie hebt gelezen. De volledige Delphisuite draaide na het landen van de write-kant 1650 van 1650 op Win32 en Win64

Chart3d-sceneparameters en de fAuto-valkuil

Twee aangrenzende details bij charts die verder gaan dan de default bijten. Het eerste is Chart3d ($103A, [MS-XLS] 2.4.46), een vlakke payload van 14 bytes die binnen de chartgroep wordt uitgegeven voor 3D-varianten: anRot (rotatie, 0 tot 360), anElev (elevatie, signed, -90 tot 90), pcDist (perspectiefafstand, 0 tot 100, genegeerd tenzij fPerspective is ingesteld), pcHeight en pcDepth (percentage van de chartbreedte, 5 tot 500), pcGap (0 tot 500) en een grbit waarvan de bits fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 en f2DWalls $0020 zijn. De specificatie voegt beperkingen toe die de recordlayout niet voor je afdwingt: op een getransponeerde barchart mogen anRot en anElev niet hoger zijn dan 44, en op een piechart mag anElev niet negatief zijn

Het tweede is de bit fAuto, en die veroorzaakt de bugmelding "mijn kleuren werden genegeerd". LineFormat ($1007), AreaFormat ($100A) en MarkerFormat ($1009) dragen allemaal fAuto in bit 0 van hun grbit, en wanneer die bit is ingesteld past Excel zijn automatische stijl toe en behandelt het de expliciete RGB-waarden, lijnstijl, markervorm en marker size die ernaast staan als decoratie. Elke emitter die een aangepaste seriestijl schrijft moet bit 0 wissen; de default emitters laten hem juist staan zodat Excel het palette kiest. Als je een bestaande workbook bewerkt in plaats van er één te bouwen, verschillen de preservationregels opnieuw en worden ze behandeld in Excel-charts bewerken zonder bewaarde ChartML te verliezen

Secundaire asgroepen, de SerToCrt-binding en het hier getoonde typed chart model worden geleverd in de HotXLS Delphi spreadsheet component voor Delphi en C++Builder, die BIFF8-charts leest en schrijft zonder dat Excel geïnstalleerd is; de productpagina bevat de volledige referentie voor chartrecords en de lijst met AddChartSheet-overloads