HotXLS는 BIFF8 chart의 secondary axis group을 axes 뒤에 두 번째 chart group으로 append하지 않고 두 번째 AxisParent block을 내보내 구현합니다. Classic XLS chart substream에서는 모든 chart group, 즉 ChartFormat, chart type record와 CrtLink가 자체 axis-group block 안에 들어가며 각 series는 SerToCrt를 통해 하나에 bind됩니다. 이 nesting을 거꾸로 하면 axis record를 몇 개 내보내든 series가 bind할 두 번째 chart group이 없습니다
axes 뒤의 두 번째 chart group이 아무것도 bind하지 못하는 이유
문법이 전부를 설명하며 ABNF 한 줄에 들어 있습니다. [MS-XLS] 2.1.7.20.1의 CHARTFOMATS rule은 AxesUsed 1*2AXISPARENT라고 하고 이어서 AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End를 적습니다. 두 production을 함께 읽으면 모양이 드러납니다. chart group은 axis group의 child이지 sibling이 아닙니다. 두 axis pair와 뒤따르는 chart group 하나를 가진 chart substream은 layout이 조금 이상한 dual-axis chart가 아니라 chart group 하나와 orphan axis record 집합입니다. SerToCrt ($1045)가 SERIESFORMAT block 안에서 axis index가 아니라 0-based chart group index를 담기 때문에 중요합니다. CRT block이 하나뿐인데 crt = 1을 쓰면 series가 아직 emit되지 않은 chart group을 가리킵니다. 사람을 헷갈리게 하는 직관은 record name입니다. AXESUSED ($1046)는 axis를 세는 것처럼 들려서 자연스럽게 axis를 더 내보내게 됩니다. 실제로 세는 것은 axis group이며 각 axis group은 완전한 plot area와 chart group을 함께 끌고 옵니다
secondary axis group에 series 표시하기
HotXLS 쪽에서는 Boolean 하나로 끝납니다. TXLSChartSeriesInfo에 SecondaryAxis field가 있으며 TXLSWorksheets.AddChartSheet에 넘기는 array의 어느 series에서든 이를 set하면 전체 builder가 two-group mode로 전환됩니다. 별도의 "enable secondary axis" call이나 axis-count parameter는 없습니다. count를 유도할 수 있기 때문입니다. 어느 series라도 secondary group을 원하면 chart에는 두 group이 필요합니다
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... A1:C12에 category, revenue와 margin을 채웁니다 ...
Series[0] := Default(TXLSChartSeriesInfo); // 이 record를 FillChar하지 않습니다
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) line은 장식이 아닙니다. TXLSChartSeriesInfo는 managed field(WideString name, dynamic trendline과 error-bar array)와 plain Boolean member를 섞으며 Delphi는 managed field가 clear되는 것만 보장합니다. SecondaryAxis를 초기화하지 않으면 stack에 있던 값이 되며 실제로 같은 binary가 console host에서는 single-axis chart를 만들고 test runner에서는 dual-axis chart를 만들 수 있습니다. 한편 category와 value range는 builder가 보기 전에 workbook의 EXTERNSHEET table을 통해 resolve됩니다. 이는 HotXLS가 BIFF SupBook과 XTI external link를 분류하는 방법에서 다룬 것과 같은 index machinery이며 알 수 없는 sheet를 이름으로 쓴 range는 build를 실패시키지 않고 empty placeholder BRAI로 degrade됩니다
series가 secondary일 때 HotXLS가 내보내는 것
emitter는 value 하나만 바꾸는 것이 아니라 shape를 바꿉니다. secondary series가 없으면 AXESUSED는 1을 담고(축 group이 전혀 없는 pie와 3D pie는 0) 하나의 AxisParent block이 뒤따릅니다. secondary series가 있으면 AXESUSED는 2를 담고 builder는 block을 두 번 실행합니다. 18-byte payload의 첫 word인 iax를 0, 그 다음 1로 설정하며 각 AxisParent ($1041) pass는 Pos, category axis와 value axis(Axis, $101D), PlotArea marker ($1035), default Frame, 이어서 ChartFormat ($1014), chart type record, CrtLink ($1022)와 chart group 및 axis group을 닫는 두 End marker를 내보냅니다. secondary series는 SerToCrt crt = 1로 bind되고 primary series는 crt = 0을 유지합니다. 일부러 복제하지 않는 record도 하나 있습니다. Excel은 axis group 수와 관계없이 chart 하나에 legend 하나만 주므로 legend는 첫 group에만 emit됩니다. 두 property도 분명히 해 둘 가치가 있습니다. chart group을 axis-parent block 안에 넣도록 emitter를 재구성해도 ordinary chart output은 바뀌지 않았습니다. secondary series가 없을 때 substream은 이전 version과 byte-identical한데 AddAxisParent를 iax = 0으로 parameterize하는 것이 정확히 예전 code path이기 때문입니다. 또 builder는 group마다 full axis pair를 계속 내보내므로 secondary group은 value scale만 필요해도 자체 category axis를 가집니다
chart inspection이 axis-group binding을 복구하는 방법
record list를 두 pass로 읽어야 합니다. AXESUSED가 자신이 설명하는 block보다 먼저 오기 때문입니다. 첫 pass는 $1046만 찾아 첫 word를 axis-group count로 읽습니다. 이 값은 1에서 시작해 낮아지지 않고 올라가기만 합니다. HotXLS는 current count와 declared count의 maximum을 취하므로 malformed 또는 duplicated AXESUSED가 이미 두 group을 선언한 것으로 확인된 chart를 되돌릴 수 없습니다. 두 번째 pass는 current axis group을 추적하고 각 AxisParent에서 갱신하며 다음 AxisParent가 나올 때까지 만나는 모든 Axis record에 그 index를 찍습니다
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는 실제로 찾은 AxisParent block 수가 아니라 file이 선언한 값을 보고합니다. block 하나를 내보내면서 2라고 적은 file은 2를 보고하며 AxisCount에서 그 사실을 알아챌 수 있습니다. 그리고 TXLSChartAxis.AxisGroup은 positional stamp입니다. axis가 어느 block 안에서 읽혔는지를 기록하며 format이 알려 주는 것은 그것뿐입니다. series 쪽의 SerToCrt decoding은 Series block 내부에 있을 때만 gate됩니다. 같은 record id가 series binding이 아닌 context에도 나타나며 gate가 없는 decoder는 잘못된 series를 쉽게 덮어쓸 수 있기 때문입니다
실제 Excel file 없이 secondary axis 검증하기
여기서 검증에는 secondary axis가 있는 Excel file이 필요하지 않았고 바로 그 점이 유용합니다. structural decoding은 record sequence의 속성이므로 synthesized sequence도 captured sequence와 똑같이 정확하게 이를 증명합니다. regression은 payload 2인 AXESUSED를 만들고 category axis와 value axis를 각각 감싼 두 AxisParent block을 만든 다음 model이 AxisGroupCount = 2, 0, 0, 1, 1이 찍힌 네 axis와 두 번째 pair에 기대한 axis type을 반환하는지 assert합니다
// Excel file을 전혀 사용하지 않는 structural verification
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED: 두 axis group
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;
record를 직접 synthesize할 때 실무적인 주의점 두 가지가 있습니다. TXLSCustomChart.AddData(RecID, Len, nil)은 Len이 0이 아니면 payload를 dereference하므로 Begin ($1033)과 End ($1034) marker는 nil blob과 stale length가 아니라 length 0으로 추가해야 합니다. 그리고 synthetic sequence는 decoder를 증명할 뿐 Excel이 output을 accept하는 것까지 증명하지는 않습니다. write side는 ABNF를 보고 구조화한 뒤 GetChartModel로 round-trip하고 per-axis stamp 0/0/1/1 및 per-series chart group 0/1을 assert했으며 byte-identical한 no-secondary path가 safety net입니다. 이는 HotXLS의 Delphi chart, image와 drawing 지원 전체가 취하는 보수적인 태도와 같습니다. record가 말하는 것을 decode하고 spec을 읽지 않은 binary layout은 추측하지 않습니다. write side가 들어간 뒤 전체 Delphi suite는 Win32와 Win64에서 1650개 중 1650개를 통과했습니다
Chart3d scene parameter와 fAuto trap
default chart를 넘어가면 연달아 물리는 세부 사항이 두 가지 있습니다. 첫 번째는 3D variant에서 chart group 안에 emit되는 평평한 14-byte payload인 Chart3d ($103A, [MS-XLS] 2.4.46)입니다. anRot(rotation, 0부터 360), anElev(elevation, signed, -90부터 90), pcDist(perspective distance, 0부터 100이며 fPerspective가 set되지 않으면 무시), pcHeight와 pcDepth(chart width의 percent, 5부터 500), pcGap(0부터 500), 그리고 fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010, f2DWalls $0020인 grbit입니다. spec은 record layout만으로는 강제하지 않는 제약도 추가합니다. transposed bar chart에서는 anRot과 anElev가 44를 넘으면 안 되고 pie chart에서는 anElev가 음수이면 안 됩니다
두 번째는 fAuto bit이며 "내 colors가 무시되었다"는 bug report를 만드는 항목입니다. LineFormat ($1007), AreaFormat ($100A), MarkerFormat ($1009)은 모두 grbit bit 0에 fAuto를 담습니다. 이 bit가 set되면 Excel이 automatic style을 적용하고 바로 옆의 explicit RGB value, line style, marker shape와 marker size를 장식으로 취급합니다. custom series style을 emit하는 emitter는 bit 0을 clear해야 합니다. default emitter가 이를 set한 채로 두는 것은 Excel이 palette를 고르게 하려는 의도입니다. 기존 workbook을 edit하는 것이 아니라 build하는 것이라면 preservation rule은 다시 달라지며 preserved ChartML을 잃지 않고 Excel chart를 편집하는 방법에서 다룹니다
secondary axis group, SerToCrt binding과 여기서 보인 typed chart model은 Delphi와 C++Builder용 HotXLS Delphi spreadsheet component에 포함되어 있으며 Excel을 설치하지 않고 BIFF8 chart를 읽고 씁니다. 제품 페이지에는 전체 chart record reference와 AddChartSheet overload list가 있습니다