Τεχνικό Άρθρο

BIFF8 secondary axis groups σε Delphi με HotXLS

Το HotXLS γράφει BIFF8 chart secondary axis groups εκδίδοντας δεύτερο AxisParent block και όχι προσθέτοντας δεύτερο chart group μετά τα axes. Στο Classic XLS chart substream κάθε chart group — ChartFormat, το chart type record και το CrtLink — ζει μέσα στο δικό του axis-group block, και κάθε series συνδέεται με ένα από αυτά μέσω του SerToCrt. Αν κάνεις ανάποδα αυτό το nesting, δεν υπάρχει δεύτερο chart group για να συνδεθεί η series, όσα axis records και αν εκπέμψεις

Γιατί ένα δεύτερο chart group μετά τα axes δεν συνδέεται με τίποτε

Η grammar είναι όλη η απάντηση, και είναι μία γραμμή ABNF. Ο κανόνας CHARTFOMATS στο [MS-XLS] 2.1.7.20.1 λέει AxesUsed 1*2AXISPARENT και μετά εξηγεί AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Διάβασε μαζί αυτές τις δύο productions και προκύπτει το σχήμα: τα chart groups είναι children ενός axis group και όχι siblings του. Chart substream με δύο axis pairs και ένα trailing chart group δεν είναι dual-axis chart με layout quirk· είναι chart με ένα chart group και σύνολο orphan axis records. Αυτό μετρά επειδή το SerToCrt ($1045), που βρίσκεται μέσα στο SERIESFORMAT block, μεταφέρει zero-based chart group index και όχι axis index. Αν γράψεις crt = 1 όταν υπάρχει μόνο ένα CRT block, δείχνεις τη series σε chart group που δεν εκδόθηκε ποτέ. Η διαίσθηση που παγιδεύει τον κόσμο είναι το όνομα του record: το AXESUSED ($1046) ακούγεται σαν να μετρά axes, οπότε η φυσική επόμενη κίνηση είναι να εκδώσεις περισσότερα axes. Μετρά axis groups και κάθε axis group παρασύρει μαζί του ολόκληρο plot area και chart group

Σήμανση series για το secondary axis group

Στην πλευρά του HotXLS αυτό συμπυκνώνεται σε ένα Boolean. Το TXLSChartSeriesInfo έχει field SecondaryAxis, και όταν το ορίσεις σε οποιαδήποτε series του array που δίνεις στο TXLSWorksheets.AddChartSheet, όλος ο builder περνά σε two-group mode. Δεν υπάρχει ξεχωριστό call «enable secondary axis» ούτε axis-count parameter, επειδή το count μπορεί να εξαχθεί: αν οποιαδήποτε series θέλει το secondary group, το chart χρειάζεται δύο

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... γέμισε τα A1:C12 με categories, revenue και margin ...

    Series[0] := Default(TXLSChartSeriesInfo);   // ποτέ FillChar σε αυτό το record
    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 γίνεται 2

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

Η γραμμή Default(TXLSChartSeriesInfo) δεν είναι decoration. Το TXLSChartSeriesInfo συνδυάζει managed fields (τα ονόματα WideString, τα dynamic trendline και error-bar arrays) με plain Boolean members, και το Delphi εγγυάται ότι καθαρίζει για σένα μόνο τα managed fields. Άφησε το SecondaryAxis uninitialized και είναι ό,τι υπήρχε στο stack, πράγμα που στην πράξη σημαίνει ότι το ίδιο binary παράγει single-axis chart όταν τρέχει από console host και dual-axis chart κάτω από test runner. Τα category και value ranges, στο μεταξύ, επιλύονται μέσω του workbook EXTERNSHEET table πριν τα δει ο builder — ο ίδιος index machinery που καλύπτεται στο πώς ταξινομεί το HotXLS BIFF SupBook και XTI external links — οπότε range με unknown sheet υποβαθμίζεται σε empty placeholder BRAI αντί να αποτύχει το build

Τι εκπέμπει το HotXLS όταν μια series είναι secondary

Ο emitter αλλάζει shape και όχι μόνο value. Χωρίς secondary series, το AXESUSED κρατά 1 (ή 0 για pie και 3D pie, που δεν έχουν καθόλου axis groups) και ακολουθεί ένα AxisParent block. Με μία secondary series, το AXESUSED κρατά 2 και ο builder τρέχει το block δύο φορές, με το iax — την πρώτη word του 18-byte AxisParent ($1041) payload — να είναι πρώτα 0 και μετά 1. Κάθε pass εκπέμπει Pos, το category axis και το value axis (Axis, $101D), το marker PlotArea ($1035), ένα default Frame, έπειτα ChartFormat ($1014), το chart type record, το CrtLink ($1022) και δύο End markers για να κλείσει το chart group και το axis group. Η secondary series συνδέεται τότε με SerToCrt crt = 1, ενώ η primary κρατά crt = 0. Ένα record σκόπιμα δεν διπλασιάζεται: το legend εκπέμπεται μόνο στο πρώτο group, επειδή το Excel δίνει σε ένα chart ένα legend όσα axis groups κι αν έχει. Δύο ακόμη ιδιότητες αξίζει να ειπωθούν καθαρά. Η αναδιάρθρωση του emitter ώστε το chart group να μπει μέσα στο axis-parent block δεν άλλαξε το output των ordinary charts — χωρίς secondary series το substream είναι byte-identical με την προηγούμενη έκδοση, επειδή το parameterizing του AddAxisParent με iax = 0 είναι ακριβώς το παλιό code path. Και ο builder συνεχίζει να εκπέμπει πλήρες axis pair ανά group, άρα ένα secondary group έχει πάντα το δικό του category axis ακόμη κι όταν σε ενδιαφέρει μόνο η value scale

Πώς ανακτά η chart inspection το axis-group binding

Η ανάγνωση γίνεται σε δύο passes πάνω στη record list και πρέπει να γίνει έτσι επειδή το AXESUSED έρχεται πριν από τα blocks που περιγράφει. Το πρώτο pass κοιτά μόνο για $1046 και διαβάζει την πρώτη word ως axis-group count. Η τιμή ξεκινά από 1 και μόνο αυξάνεται: το HotXLS παίρνει maximum του current count και του declared, ώστε malformed ή duplicated AXESUSED να μην μπορεί να υποβιβάσει chart που έχει ήδη φανεί ότι δηλώνει δύο groups. Το δεύτερο pass παρακολουθεί το current axis group, το ενημερώνει σε κάθε AxisParent και σημειώνει αυτό το index σε κάθε Axis record που συναντά μέχρι να εμφανιστεί το επόμενο AxisParent

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] είναι το data 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;

Αξίζει να ονομαστούν δύο όρια. Το TXLSChartModel.AxisGroupCount αναφέρει τι δηλώνει το file και όχι πόσα AxisParent blocks βρέθηκαν πραγματικά· file που λέει 2 και παραδίδει ένα block θα αναφέρει 2, και στο AxisCount θα το προσέξεις. Και το TXLSChartAxis.AxisGroup είναι positional stamp: καταγράφει μέσα σε ποιο block διαβάστηκε ένα axis, το μόνο που σου λέει το format. Στην πλευρά των series, το decoding του SerToCrt ενεργοποιείται μόνο όταν βρίσκεσαι μέσα σε Series block, επειδή το ίδιο record id εμφανίζεται σε contexts όπου δεν είναι series binding και ένας ungated decoder θα έκανε overwrite τη λάθος series

Επαλήθευση secondary axes χωρίς πραγματικό Excel file

Η verification εδώ δεν χρειάστηκε Excel file με secondary axis, και αυτό είναι το χρήσιμο μέρος της ιστορίας. Το structural decoding είναι ιδιότητα της record sequence, οπότε synthesized sequence το αποδεικνύει εξίσου ακριβώς με captured sequence. Το regression χτίζει AXESUSED με payload 2, μετά δύο AxisParent blocks που το καθένα τυλίγει ένα category axis και ένα value axis, και ελέγχει ότι το model επιστρέφει AxisGroupCount = 2, τέσσερα axes με stamps 0, 0, 1, 1 και τα αναμενόμενα axis types στο δεύτερο pair

// Structural verification χωρίς να εμπλέκεται καθόλου Excel file
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: δύο axis groups
  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;

Δύο πρακτικές σημειώσεις αν συνθέτεις records μόνος σου. Το TXLSCustomChart.AddData(RecID, Len, nil) κάνει dereference το payload όταν το Len δεν είναι μηδέν, άρα οι markers Begin ($1033) και End ($1034) πρέπει να προστεθούν με length zero και όχι με nil blob και stale length. Και μια synthetic sequence αποδεικνύει τον decoder, ποτέ την αποδοχή του output από το Excel — η write side οργανώθηκε από το ABNF και μετά ελέγχθηκε με round-trip μέσω του GetChartModel, με assertions για per-axis stamps 0/0/1/1 και per-series chart groups 0/1, ενώ το byte-identical no-secondary path είναι safety net. Αυτή είναι η ίδια conservative στάση πάνω στην οποία χτίζεται όλη η HotXLS chart, image και drawing support για Delphi: decode ό,τι λένε τα records και αρνήσου να μαντέψεις binary layout που δεν έχεις διαβάσει στο spec. Η πλήρης Delphi suite έτρεξε 1650 από 1650 σε Win32 και Win64 μετά την ολοκλήρωση της write side

Chart3d scene parameters και η παγίδα fAuto

Δύο γειτονικές λεπτομέρειες δαγκώνουν όποιον ξεπεράσει το default chart. Η πρώτη είναι το Chart3d ($103A, [MS-XLS] 2.4.46), ένα flat payload 14 bytes που εκπέμπεται μέσα στο chart group για 3D variants: anRot (rotation, 0 έως 360), anElev (elevation, signed, -90 έως 90), pcDist (perspective distance, 0 έως 100, αγνοείται αν δεν είναι ενεργό το fPerspective), pcHeight και pcDepth (ποσοστό του chart width, 5 έως 500), pcGap (0 έως 500) και grbit του οποίου τα bits είναι fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 και f2DWalls $0020. Το spec προσθέτει constraints που το record layout δεν θα επιβάλει για σένα: σε transposed bar chart τα anRot και anElev δεν πρέπει να ξεπερνούν το 44, και σε pie chart το anElev δεν πρέπει να είναι αρνητικό

Η δεύτερη είναι το bit fAuto, και αυτό παράγει το report «αγνοήθηκαν τα χρώματά μου». Τα LineFormat ($1007), AreaFormat ($100A) και MarkerFormat ($1009) μεταφέρουν όλα fAuto στο bit 0 του grbit, και όταν αυτό το bit είναι ενεργό το Excel εφαρμόζει automatic style και αντιμετωπίζει τα explicit RGB values, line style, marker shape και marker size δίπλα του ως decoration. Κάθε emitter που γράφει custom series style πρέπει να καθαρίζει το bit 0· οι default emitters το κρατούν ενεργό ακριβώς για να επιλέγει το Excel την palette. Αν επεξεργάζεσαι υπάρχον workbook αντί να χτίζεις ένα, οι preservation rules διαφέρουν ξανά και καλύπτονται στο editing Excel charts χωρίς απώλεια preserved ChartML

Τα secondary axis groups, το SerToCrt binding και το typed chart model που παρουσιάζεται εδώ διατίθενται στο HotXLS Delphi spreadsheet component για Delphi και C++Builder, το οποίο διαβάζει και γράφει BIFF8 charts χωρίς εγκατεστημένο Excel· η product page περιέχει την πλήρη αναφορά chart records και τη λίστα overloads του AddChartSheet