Το 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