HotXLS scrie grupuri de axe secundare pentru chart-uri BIFF8 emițând un al doilea bloc AxisParent, nu adăugând un al doilea chart group după axe. În chart substream-ul Classic XLS, fiecare chart group — ChartFormat, record-ul chart type, CrtLink — trăiește în propriul bloc axis-group, iar fiecare series se leagă de unul prin SerToCrt. Inversează această imbricare și nu există un al doilea chart group la care să se lege un series, indiferent câte axis records emiți
De ce nu leagă nimic un al doilea chart group după axe?
Grammar-ul este întregul răspuns și încape într-o singură linie ABNF. Regula CHARTFOMATS din [MS-XLS] 2.1.7.20.1 spune AxesUsed 1*2AXISPARENT, apoi descrie AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Citește cele două productions împreună și forma devine clară: chart groups sunt copii ai unui axis group, nu frații lui. Un chart substream cu două perechi de axe și un chart group trailing nu este un chart dual-axis cu un quirk de layout; este un chart cu un chart group și un set de axis records orphan. Asta contează deoarece SerToCrt ($1045), aflat în blocul SERIESFORMAT, poartă un index zero-based de chart group, nu un axis index. Dacă scrii crt = 1 când există un singur bloc CRT, îndrepți un series către un chart group care nu a fost emis niciodată. Intuiția care păcălește este numele record-ului: AXESUSED ($1046) sună ca și cum ar număra axe, așa că pasul natural pare să fie să emiți mai multe axe. El numără axis groups, iar fiecare axis group aduce cu el o zonă completă de plot și un chart group
Marcarea unui series pentru grupul de axe secundare
În partea HotXLS totul se reduce la un singur Boolean. TXLSChartSeriesInfo are un field SecondaryAxis, iar setarea lui pe orice series din array-ul transmis lui TXLSWorksheets.AddChartSheet comută builder-ul în two-group mode. Nu există un apel separat „enable secondary axis” și nici un parameter pentru numărul de axe, deoarece count-ul poate fi derivat: dacă orice series vrea grupul secundar, chart-ul are nevoie de două
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... completează A1:C12 cu categorii, revenue și margin ...
Series[0] := Default(TXLSChartSeriesInfo); // nu folosi niciodată FillChar pentru acest 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 devine 2
Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
'Revenue vs Margin', '', '', Series);
Wb.SaveAs('dual-axis.xls', xlExcel97);
finally
Wb.Free;
end;
end;
Linia Default(TXLSChartSeriesInfo) nu este decorativă. TXLSChartSeriesInfo combină fields managed (numele WideString, trendline dynamic și error-bar arrays) cu membri Boolean simpli, iar Delphi garantează curățarea fields managed, nu și a celorlalți. Lasă SecondaryAxis neinițializat și el are valoarea care se afla în stack, ceea ce în practică înseamnă că același binary produce un chart single-axis când rulează dintr-un console host și un chart dual-axis sub test runner. Între timp, ranges pentru categories și values se rezolvă prin tabela EXTERNSHEET a workbook-ului înainte ca builder-ul să le vadă — aceeași machinery de index acoperită în clasificarea BIFF SupBook și XTI external links în HotXLS — așa că un range care numește un sheet necunoscut degradează la un placeholder BRAI gol în loc să eșueze build-ul
Ce emite HotXLS când un series este secundar
Emitter-ul își schimbă forma, nu doar o valoare. Fără un series secundar, AXESUSED conține 1 (sau 0 pentru pie și 3D pie, care nu au deloc axis groups), iar după el urmează un singur bloc AxisParent. Cu unul, AXESUSED conține 2, iar builder-ul rulează blocul de două ori, cu iax — primul word din payload-ul de 18 bytes al lui AxisParent ($1041) — setat întâi la 0, apoi la 1. Fiecare pass emite Pos, category axis și value axis (Axis, $101D), marker-ul PlotArea ($1035), un Frame default, apoi ChartFormat ($1014), chart type record, CrtLink ($1022) și două markers End pentru a închide chart group-ul și axis group-ul. Series-ul secundar se leagă apoi prin SerToCrt crt = 1, iar cel primar păstrează crt = 0. Un record nu este duplicat intenționat: legend-ul este emis doar în primul group, deoarece Excel dă unui chart o singură legendă indiferent câte axis groups are. Alte două proprietăți merită spuse direct. Restructurarea emitter-ului pentru a pune chart group-ul în blocul axis-parent nu a schimbat output-ul pentru chart-uri obișnuite — fără series secundar, substream-ul este byte-identic cu versiunea anterioară, deoarece parametrizarea lui AddAxisParent cu iax = 0 este exact ruta veche. Iar builder-ul emite în continuare o pereche completă de axe pentru fiecare group, așa că un group secundar vine mereu cu propria category axis chiar și când te interesează doar scara lui de values
Cum recuperează inspecția chart-ului binding-ul axis-group?
Citirea este în două treceri peste lista de records și trebuie să fie așa, deoarece AXESUSED apare înaintea blocurilor pe care le descrie. Prima trecere caută doar $1046 și citește primul său word ca număr de axis groups. Valoarea începe la 1 și poate doar crește: HotXLS ia maximul dintre count-ul curent și cel declarat, astfel încât un AXESUSED malformed sau duplicat să nu poată regresa un chart care a fost deja văzut declarând două groups. A doua trecere urmărește axis group-ul curent, actualizându-l la fiecare AxisParent, și aplică acel index fiecărui record Axis întâlnit până la următorul AxisParent
var
Model: TXLSChartModel;
i: Integer;
begin
// Sheets[1] este worksheet-ul de date, Sheets[2] chart sheet-ul
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;
Două limite merită numite. TXLSChartModel.AxisGroupCount raportează ce declară fișierul, nu câte blocuri AxisParent au fost găsite efectiv; un fișier care spune 2 și livrează un singur bloc va raporta 2, iar AxisCount este locul în care observi problema. Iar TXLSChartAxis.AxisGroup este un positional stamp: înregistrează în ce bloc a fost citită o axă, singurul lucru pe care formatul ți-l spune. Pe partea series, decoding-ul SerToCrt este controlat de faptul că te afli într-un Series block, deoarece același record id apare în contexte în care nu reprezintă binding de series, iar un decoder fără guard ar suprascrie cu plăcere series-ul greșit
Verificarea axelor secundare fără un fișier Excel real
Verificarea de aici nu a avut nevoie de un fișier Excel cu axă secundară, iar aceasta este partea utilă. Decoding-ul structural este o proprietate a secvenței de records, așa că o secvență sintetizată o dovedește la fel de exact ca una capturată. Regression-ul construiește AXESUSED cu payload 2, apoi două blocuri AxisParent, fiecare învelind o category axis și o value axis, și verifică faptul că modelul revine cu AxisGroupCount = 2, patru axe marcate 0, 0, 1, 1 și tipurile de axe așteptate pe a doua pereche
// Verificare structurală fără niciun fișier Excel implicat
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED: două 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;
Două note practice dacă sintetizezi singur records. TXLSCustomChart.AddData(RecID, Len, nil) dereferențiază payload-ul când Len este non-zero, așa că marker-ele Begin ($1033) și End ($1034) trebuie adăugate cu lungime zero, nu cu un blob nil și un length stale. Iar o secvență sintetică verifică decoder-ul, niciodată acceptarea output-ului de către Excel — write side-ul a fost structurat din ABNF și apoi verificat prin round-trip cu GetChartModel, cu aserțiuni per-axis 0/0/1/1 și per-series chart groups 0/1, iar ruta byte-identică fără secondary este safety net-ul. Este aceeași atitudine conservatoare pe care se bazează restul suportului HotXLS pentru chart-uri, imagini și drawings în Delphi: decodează ce spun records și refuză să ghicești un layout binar pe care nu l-ai citit în spec. Suita completă Delphi a rulat 1650 din 1650 pe Win32 și Win64 după ce write side-ul a fost livrat
Parametri de scenă Chart3d și capcana fAuto
Două detalii adiacente îi lovesc pe cei care trec de chart-ul default. Primul este Chart3d ($103A, [MS-XLS] 2.4.46), un payload plat de 14 bytes emis în chart group pentru variantele 3D: anRot (rotație, 0 până la 360), anElev (elevație, signed, -90 până la 90), pcDist (distanță de perspectivă, 0 până la 100, ignorată dacă fPerspective nu este setat), pcHeight și pcDepth (procent din lățimea chart-ului, 5 până la 500), pcGap (0 până la 500) și un grbit ale cărui bits sunt fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 și f2DWalls $0020. Spec-ul adaugă constrângeri pe care layout-ul record-ului nu le impune singur: pe un bar chart transposed, anRot și anElev nu trebuie să depășească 44, iar pe un pie chart anElev nu trebuie să fie negativ
Al doilea este bitul fAuto, cel care produce bug report-ul „culorile mele au fost ignorate”. LineFormat ($1007), AreaFormat ($100A) și MarkerFormat ($1009) poartă toate fAuto în bitul 0 al lui grbit, iar când acel bit este setat Excel aplică style-ul automat și tratează valorile RGB explicite, line style, marker shape și marker size de lângă el ca decorație. Orice emitter care scrie un custom series style trebuie să curețe bitul 0; emitter-ele default îl păstrează setat tocmai pentru ca Excel să aleagă paleta. Dacă editezi un workbook existent, nu construiești unul, regulile de păstrare diferă din nou și sunt acoperite în editarea chart-urilor Excel fără pierderea ChartML păstrat
Secondary axis groups, binding-ul SerToCrt și typed chart model-ul arătat aici sunt livrate în componenta spreadsheet HotXLS pentru Delphi pentru Delphi și C++Builder, care citește și scrie chart-uri BIFF8 fără Excel instalat; pagina produsului conține referința completă pentru chart records și lista de overload-uri AddChartSheet