HotXLS записва BIFF8 chart secondary axis groups, като emit-ва втори AxisParent block, а не като append-ва second chart group след axes. В Classic XLS chart substream всеки chart group — ChartFormat, chart type record, CrtLink — живее вътре в собствен axis-group block и всеки series се bind-ва към един от тях чрез SerToCrt. Обърнете тази nesting и няма second chart group, към който series да се bind-не, независимо колко axis records emit-нете
Защо втори chart group след axes не се bind-ва към нищо?
Grammar-ът е целият отговор и е един ред ABNF. CHARTFOMATS rule в [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, който никога не е emit-нат. Интуицията, която подвежда хората, идва от името на record-а: AXESUSED ($1046) звучи сякаш брои axes, така че естественият следващ ход е да emit-нете още 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. Няма отделен „enable secondary axis“ call и няма axis-count parameter, защото count-ът е derivable: ако някой 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 names, dynamic trendline и error-bar arrays) с plain Boolean members, а Delphi гарантира, че managed fields ще бъдат cleared, не и че plain fields ще бъдат. Оставете SecondaryAxis uninitialized и той ще е каквото е било на stack-а, което на практика означава, че един и същ binary произвежда single-axis chart от console host и dual-axis chart под test runner. Category и value ranges междувременно се resolve-ват през workbook EXTERNSHEET table, преди builder-ът изобщо да ги види — същият index machinery, разгледан в как HotXLS класифицира BIFF SupBook и XTI external links — така че range, назоваващ unknown sheet, се свежда до empty placeholder BRAI, вместо build-ът да fail-не
Какво emit-ва HotXLS, когато series е secondary?
Emitter-ът променя shape, а не само value. Без secondary series AXESUSED съдържа 1 (или 0 за pie и 3D pie, които изобщо нямат axis groups) и следва един AxisParent block. С такъв series AXESUSED съдържа 2 и builder-ът изпълнява block-а два пъти, с iax — първата дума от 18-byte AxisParent ($1041) payload — зададена последователно на 0 и 1. Всеки pass emit-ва Pos, category axis и value axis (Axis, $101D), PlotArea marker ($1035), default Frame, след това ChartFormat ($1014), chart type record, CrtLink ($1022) и два End markers за затваряне на chart group и axis group. Secondary series после се bind-ва с SerToCrt crt = 1, а primary запазва crt = 0. Един record нарочно не се duplicate-ва: legend се emit-ва само в първата group, защото Excel дава на chart един legend, независимо колко axis groups има. Още две properties си струва да се кажат директно. Restructuring-ът на emitter-а, така че chart group да стои в axis-parent block-а, не промени output-а за обикновените charts — без secondary series substream-ът е byte-identical спрямо старата версия, тъй като parameterizing на AddAxisParent с iax = 0 е точно старият code path. И builder-ът продължава да emit-ва пълна axis pair за всяка group, така че secondary group винаги пристига със собствен category axis, дори когато ви интересува само нейната value scale
Как chart inspection възстановява axis-group binding-а?
Reading-ът е на два pass-а през record list и трябва да е такъв, защото AXESUSED идва преди block-овете, които описва. Първият pass гледа само за $1046 и чете първата му word като axis-group count. Тази стойност започва от 1 и се увеличава, никога не се намалява: HotXLS взема maximum от текущия count и declared value, така че malformed или duplicate AXESUSED не може да влоши chart, който вече е бил видян да декларира две groups. Вторият pass следи current axis group, обновява го при всеки AxisParent и stamp-ва този 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-ите SerToCrt decoding-ът е gated да работи вътре в Series block, защото същият record id се появява в context-и, където не е series binding, а ungated decoder с готовност би overwrite-нал грешния series
Проверка на secondary axes без реален Excel file
Тази verification не се нуждаеше от Excel file с secondary axis и това е полезната част от историята. Structural decoding е property на record sequence, така че synthesized sequence го доказва точно толкова добре, колкото и captured. Regression-ът изгражда AXESUSED с payload 2, после два AxisParent blocks, всеки обгръщащ category axis и value axis, и assert-ва, че model-ът се връща с AxisGroupCount = 2, четири axes, stamped 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 е non-zero, така че Begin ($1033) и End ($1034) markers трябва да се добавят с length zero, а не с nil blob и stale length. И synthetic sequence доказва decoder-а, никога Excel acceptance-а на вашия output — 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 posture, върху която е изградена останалата HotXLS chart, image и drawing support за Delphi: decode-вайте каквото records казват и отказвайте да гадаете за binary layout, който не сте прочели в spec-а. Пълната Delphi suite премина 1650 от 1650 на Win32 и Win64 след landing-а на write side
Chart3d scene parameters и fAuto trap
Два съседни detail-а хващат хората, които излизат извън default chart. Първият е Chart3d ($103A, [MS-XLS] 2.4.46), flat 14-byte payload, emit-нат вътре в chart group за 3D variants: anRot (rotation, 0 до 360), anElev (elevation, signed, -90 до 90), pcDist (perspective distance, 0 до 100, игнориран, освен ако fPerspective не е set), 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 не трябва да е negative
Вторият е bit-ът fAuto и той произвежда bug report „my colors were ignored“. LineFormat ($1007), AreaFormat ($100A) и MarkerFormat ($1009) носят fAuto в bit 0 на своя grbit, а когато bit-ът е set, Excel прилага automatic style и третира explicit RGB values, line style, marker shape и marker size до него като decoration. Всеки emitter, който записва custom series style, трябва да изчисти bit 0; default emitter-ите го оставят set точно за да избере Excel palette-а. Ако редактирате existing workbook, вместо да build-вате one, preservation rules са други и са разгледани в редактиране на Excel charts без загуба на preserved ChartML
Secondary axis groups, SerToCrt binding и typed chart model-ът, показан тук, са част от HotXLS Delphi spreadsheet component за Delphi и C++Builder, който чете и записва BIFF8 charts без Excel; продуктовата страница носи пълния chart record reference и списъка с overload-и на AddChartSheet