Техническа статия

BIFF8 secondary axis groups в Delphi с HotXLS

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

BIFF8 chart граматиката, която HotXLS следва в Delphi: CHARTFOMATS чете AxesUsed 1*2AXISPARENT, а AXISPARENT увива AxisParent, Begin, Pos, осите и един до четири CRT блока, така че всяка chart група от ChartFormat и CrtLink живее вътре в axis група, докато chart група, издадена след End, оставя SerToCrt да сочи нищо
ABNF-ът прави вложението явно: chart групата е дете на своята axis група, затова trailing CRT след осите не е втора група, а купчина сирашки records

Маркиране на 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

Последователността от records, която HotXLS издава за Delphi chart sheet с две оси: SecondaryAxis True вдига AXESUSED на 2, два AxisParent блока с iax 0 и 1 носят всеки Pos, Axis, PlotArea, Frame, ChartFormat, record-а на chart типа и CrtLink, а SerToCrt закача primary серията към група 0 и secondary към група 1
Включването на SecondaryAxis на една серия превключва builder-а в режим с две групи, а всеки pass издава пълна осна двойка и chart група, като legend-ата е умишлено записана само веднъж

Как 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

Как chart инспекцията на HotXLS възстановява axis-group обвързванията в два pass-а: първото сканиране чете думата AXESUSED $1046 като брой групи и никога не я смалява, второто следи текущия AxisParent и щамповва всеки Axis record с този индекс, а моделът докладва AxisGroupCount, axis щампи 0 0 1 1 и series групи 0 и 1
Два pass-а са нужни, защото AXESUSED пристига преди блоковете, които описва, затова броят се прочита първо, а позиционните axis щампи се нанасят на второ обхождане на списъка с records
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