技術記事

DelphiのHotXLSでBIFF8 secondary axis group

HotXLSはBIFF8 chartのsecondary axis groupを、axisの後ろへ2つ目のchart groupをappendするのではなく、2つ目のAxisParent blockをemitして書き出します。Classic XLS chart substreamでは、各chart group、ChartFormat、chart type record、CrtLinkが自身のaxis-group block内にあり、各seriesはSerToCrtでその1つへbindします。このnestingを逆にすると、どれだけaxis recordをemitしてもseriesがbindする2つ目のchart groupはありません

axis後の2つ目のchart groupが何にもbindしない理由

grammarがすべての答えで、ABNFの1行に書かれています。[MS-XLS] 2.1.7.20.1のCHARTFOMATS ruleはAxesUsed 1*2AXISPARENTと述べ、さらにAXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT Endと展開します。この2つのproductionを合わせると形が出ます。chart groupはaxis groupのchildであり、siblingではありません。2つのaxis pairと1つのtrailing chart groupを持つchart substreamは、dual-axis chartのlayout quirkではなく、1つのchart groupとorphan axis recordの集合です。これは、SERIESFORMAT block内にあるSerToCrt($1045)がaxis indexではなく0-basedのchart group indexを持つため重要です。chart groupを1つしかemitしていないのにcrt = 1を書くと、CRT blockが1つしかない場合、seriesは存在しないchart groupを指します。人をつまずかせるintuitionはrecord nameです。AXESUSED($1046)はaxisを数えるように聞こえるので、axisを追加したくなります。しかし数えるのはaxis groupで、各axis groupはcompleteなplot areaとchart groupを連れてきます

seriesをsecondary axis groupへ付ける

HotXLS側ではBoolean 1つに縮みます。TXLSChartSeriesInfoSecondaryAxis fieldを持ち、TXLSWorksheets.AddChartSheetへ渡すarray内の任意のseriesでこれをsetすると、builder全体がtwo-group modeへ切り替わります。「enable secondary axis」の別callもaxis-count parameterもありません。countは導出できるからです。secondary groupを望むseriesが1つでもあれば、chartには2つ必要です

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... A1:C12へcategory、revenue、marginをfill ...

    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が自動clearを保証するのはmanaged fieldだけです。SecondaryAxisをuninitializedのままにすると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です。unknown sheetを名指しするrangeはbuildをfailさせず、empty placeholder BRAIへdegradeします

seriesがsecondaryのときHotXLSがemitするもの

emitterはvalueだけでなくshapeを変えます。secondary seriesがなければAXESUSEDは1です。pieと3D pieはaxis groupを持たないため0です。その後に1つのAxisParent blockが続きます。secondaryが1つあるとAXESUSEDは2になり、builderはblockを2回実行します。18-byteのAxisParent($1041)payloadのfirst wordであるiaxを、0、1の順に設定します。各passはPos、category axis、value axis(Axis、$101D)、PlotArea marker($1035)、default Frame、続いてChartFormat($1014)、chart type record、CrtLink($1022)、chart groupとaxis groupを閉じる2つのEnd markerをemitします。secondary seriesはSerToCrt crt = 1でbindし、primary seriesはcrt = 0のままです。意図的にduplicateしないrecordが1つあります。legendはfirst groupだけにemitします。Excelはaxis groupの数に関係なくchartにlegendを1つだけ与えるためです。さらに2つを明確にします。chart groupをaxis-parent blockの内側へ移すようemitterを再構成しても、ordinary chartのoutputは変わりません。secondary seriesがなければ、iax = 0AddAxisParentをparameterizeする経路が旧code pathそのものなので、substreamはbyte-identicalです。またbuilderはgroupごとにfull axis pairをemitするため、value scaleだけに関心があってもsecondary groupには独自のcategory axisが必ず到着します

chart inspectionがaxis-group bindingを復元する方法

record listを2 passで読みます。AXESUSEDは説明するblockより先に来るため、1 passでは足りません。first passは$1046だけを探し、first wordをaxis-group countとして読みます。このvalueは1から始まり、下がることはありません。HotXLSはcurrent countとdeclared valueのmaximumを取り、すでに2 groupと確認したchartがmalformedまたはduplicate AXESUSEDで後退しないようにします。second passはcurrent axis groupを追跡し、各AxisParentで更新し、次のAxisParentが現れるまで出会ったすべてのAxis recordへindexをstampします

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;

2つのlimitを名前に出しておきます。TXLSChartModel.AxisGroupCountはfileがdeclareする数を返し、実際に見つかったAxisParent blockの数ではありません。2と書いてblockを1つしか持たないfileは2を報告し、気づく場所はAxisCountです。TXLSChartAxis.AxisGroupはpositional stampです。axisがどのblock内で読まれたかを記録し、formatが教えるのはそれだけです。series側では、SerToCrt decodingはSeries block内にいる場合だけgateされます。同じrecord idがseries bindingではないcontextにも現れるため、gateなしのdecoderは誤ったseriesを簡単に上書きします

real Excel fileなしでsecondary axisを検証する

ここでのverificationにはsecondary axisを持つExcel fileが不要でした。それが役に立つ部分です。structural decodingはrecord sequenceのpropertyなので、synthesized sequenceでもcaptured sequenceと同じだけ正確に証明できます。regressionはpayload 2のAXESUSEDをbuildし、category axisとvalue axisをそれぞれwrapする2つのAxisParent blockを作ります。そしてmodelがAxisGroupCount = 2を返し、4 axisに0、0、1、1がstampされ、second pairに期待するaxis typeがあることをassertします

// Excel fileを一切使わないstructural verification
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED:2つの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する場合の実用的な注意が2つあります。TXLSCustomChart.AddData(RecID, Len, nil)Lenがnon-zeroだとpayloadをdereferenceします。そのためBegin($1033)とEnd($1034)markerは、nil blobとstale lengthではなくlength zeroで追加しなければなりません。synthetic sequenceが証明するのはdecoderであり、outputをExcelが受け入れることではありません。write sideはABNFから構造化し、GetChartModelを通じてround-tripし、per-axis stamp 0/0/1/1とper-series chart group 0/1をassertしました。secondaryなしのbyte-identical pathがsafety netです。これはDelphi向けHotXLS chart、image、drawing support全体が取るconservative postureと同じです。recordが言うことをdecodeし、specで読んでいないbinary layoutを推測することを拒否します。write sideがlandした後、full Delphi suiteはWin32とWin64で1650件中1650件に合格しました

Chart3d scene parameterとfAuto trap

default chartを越えると、隣接する2つの細部が人を刺します。1つ目はChart3d($103A、[MS-XLS] 2.4.46)です。3D variantのchart group内にemitされるflatな14-byte payloadで、anRot(rotation、0から360)、anElev(signed elevation、-90から90)、pcDist(perspective distance、0から100、fPerspectiveがsetされない限りignore)、pcHeightpcDepth(chart widthのpercent、5から500)、pcGap(0から500)、そしてfPerspective $0001、fCluster $0002、fAutoscale $0004、f3DScaling $0010、f2DWalls $0020のbitを持つgrbitです。record layoutだけではspecのconstraintを強制しません。transposed bar chartではanRotanElevが44を超えてはいけず、pie chartではanElevがnegativeであってはいけません

2つ目はfAuto bitで、「my colors were ignored」という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をdecorationとして扱います。custom series styleをemitするemitterはbit 0をclearしなければなりません。default emitterがsetしたままにするのは、Excelにpaletteを選ばせるためです。buildではなくexisting workbookをeditするならpreservation ruleはさらに異なり、preserved ChartMLを失わずにExcel chartをeditする方法で説明しています

secondary axis group、SerToCrt binding、ここで示したtyped chart modelは、DelphiとC++Builder向けHotXLS Delphi spreadsheet componentの一部です。ExcelをinstallせずにBIFF8 chartをreadとwriteでき、product pageには完全なchart record referenceとAddChartSheet overload listがあります