مقاله فنی

گروه‌های axis ثانویه نمودار BIFF8 در Delphi با HotXLS

HotXLS گروه‌های axis ثانویه نمودار BIFF8 را با emit کردن یک block دوم از نوع AxisParent می‌نویسد، نه با append کردن chart group دوم بعد از axisها. در chart substream مربوط به Classic XLS، هر chart group یعنی ChartFormat، record نوع chart و CrtLink داخل block مخصوص خودش از axis group قرار دارد و هر series از طریق SerToCrt به یکی از آن‌ها bind می‌شود. این nesting را برعکس کنید، برای bind شدن series هیچ chart group دومی وجود نخواهد داشت، مهم نیست چند record از نوع axis emit کنید

چرا chart group دوم بعد از axisها به هیچ چیز bind نمی‌شود؟

grammar تمام پاسخ است و فقط یک خط ABNF دارد. rule مربوط به CHARTFOMATS در [MS-XLS] 2.1.7.20.1 می‌گوید AxesUsed 1*2AXISPARENT و سپس AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End را مشخص می‌کند. این دو production را کنار هم بخوانید تا شکل روشن شود: chart groupها child یک axis group هستند، نه sibling آن. chart substreamی با دو جفت axis و یک chart group در انتها، chartی dual-axis با layout عجیب نیست؛ chartی با یک chart group و مجموعه‌ای از axis recordهای orphan است. این تفاوت مهم است چون SerToCrt با مقدار $1045 که داخل block مربوط به SERIESFORMAT قرار دارد، index صفرمبنای chart group را حمل می‌کند، نه index مربوط به axis را. وقتی crt = 1 می‌نویسید در حالی که فقط یک block از نوع CRT وجود دارد، series را به chart groupی اشاره می‌دهید که هرگز emit نشده است. چیزی که افراد را به اشتباه می‌اندازد نام record است: AXESUSED با مقدار $1046 طوری به نظر می‌رسد که تعداد axisها را می‌شمارد، پس حرکت طبیعی این است که axis بیشتری emit کنید. اما این record تعداد axis groupها را می‌شمارد و هر axis group یک plot area و chart group کامل را با خود می‌آورد

علامت‌گذاری یک series برای axis group ثانویه

در سمت HotXLS، این کار به یک Boolean خلاصه می‌شود. TXLSChartSeriesInfo fieldی به نام SecondaryAxis دارد و set کردن آن روی هر series در arrayای که به TXLSWorksheets.AddChartSheet می‌دهید، کل builder را وارد حالت دوگروهی می‌کند. call جداگانه‌ای برای «فعال کردن axis ثانویه» و parameterی برای تعداد axis وجود ندارد، چون count قابل مشتق‌سازی است: اگر هر series گروه ثانویه را بخواهد، chart به دو گروه نیاز دارد

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) تزئینی نیست. TXLSChartSeriesInfo fieldهای managed یعنی nameهای WideString و arrayهای dynamic مربوط به trendline و error-bar را با memberهای ساده Boolean ترکیب می‌کند و Delphi فقط تضمین می‌کند fieldهای managed را برای شما clear کند. اگر SecondaryAxis را initialize نکنید، مقدارش هر چیزی است که روی stack بوده؛ در عمل یعنی یک binary یکسان وقتی از console host اجرا می‌شود chart تک‌محوری می‌سازد و زیر test runner chart دو‌محوری. rangeهای category و value نیز پیش از آنکه builder آن‌ها را ببیند، از طریق EXTERNSHEET table workbook resolve می‌شوند؛ همان machinery مربوط به index که در نحوه طبقه‌بندی external linkهای BIFF SupBook و XTI در HotXLS پوشش داده شده است. پس rangeای که نام sheet ناشناخته‌ای دارد به placeholder خالی BRAI degrade می‌شود، نه اینکه build fail شود

HotXLS وقتی یک series ثانویه است چه چیزی emit می‌کند؟

emitter شکل خود را عوض می‌کند، نه فقط یک value را. بدون series ثانویه، AXESUSED مقدار 1 دارد، یا برای pie و 3D pie که اصلاً axis group ندارند مقدار 0، و یک block از نوع AxisParent بعد از آن می‌آید. با وجود series ثانویه، AXESUSED مقدار 2 دارد و builder block را دو بار اجرا می‌کند؛ با iax که نخستین word از payload هجده‌بایتی AxisParent با مقدار $1041 است، یک بار 0 و بار دیگر 1. هر pass، Pos، category axis و value axis یعنی Axis با مقدار $101D، marker مربوط به PlotArea با مقدار $1035، یک Frame پیش‌فرض، سپس ChartFormat با مقدار $1014، record نوع chart، CrtLink با مقدار $1022 و دو marker از نوع End برای بستن chart group و axis group emit می‌کند. سپس series ثانویه با SerToCrt crt = 1 bind می‌شود و primary با crt = 0 باقی می‌ماند. یک record عمداً duplicate نمی‌شود: legend فقط در group اول emit می‌شود، چون Excel صرف‌نظر از تعداد axis groupها، برای chart یک legend می‌دهد. دو property دیگر نیز باید شفاف گفته شوند. بازسازی emitter به‌طوری که chart group را داخل axis-parent block بگذارد، output chartهای معمولی را تغییر نداد؛ وقتی series ثانویه نداریم، substream با version قبلی byte-identical است، چون parameterize کردن AddAxisParent با iax = 0 دقیقاً همان code path قدیمی است. builder هنوز برای هر group یک جفت axis کامل emit می‌کند، بنابراین group ثانویه همیشه با category axis خودش می‌آید، حتی وقتی فقط به value scale آن اهمیت می‌دهید

بازرسی chart چگونه binding مربوط به axis group را بازیابی می‌کند؟

خواندن در دو pass روی record list انجام می‌شود و ناچار باید دو pass باشد، چون AXESUSED پیش از blockهایی می‌آید که توصیفشان می‌کند. pass اول فقط دنبال $1046 می‌گردد و word اول آن را به‌عنوان count مربوط به axis group می‌خواند. این value از 1 شروع می‌شود و فقط بالا می‌رود، هرگز پایین نمی‌آید: HotXLS maximum بین count فعلی و مقدار اعلام‌شده را می‌گیرد، بنابراین AXESUSED malformed یا duplicate نمی‌تواند chartی را که قبلاً دو group اعلام کرده به یک group برگرداند. pass دوم axis group فعلی را track می‌کند، در هر AxisParent آن را update می‌کند و تا ظاهر شدن AxisParent بعدی، همان index را روی هر record از نوع Axis که می‌بیند stamp می‌کند

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] 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 چیزی را report می‌کند که file اعلام می‌کند، نه تعداد blockهای AxisParent که واقعاً پیدا شده‌اند؛ fileی که 2 می‌گوید و یک block می‌فرستد، 2 report می‌کند و AxisCount جایی است که متوجه می‌شوید. همچنین TXLSChartAxis.AxisGroup یک positional stamp است: ثبت می‌کند axis درون کدام block خوانده شده و این تنها چیزی است که format به شما می‌گوید. در سمت series، decode مربوط به SerToCrt فقط وقتی فعال است که داخل Series block باشیم، چون همان record id در contextهایی ظاهر می‌شود که binding series نیستند و decoder بدون guard با خوشحالی series اشتباه را overwrite می‌کند

verify کردن axis ثانویه بدون file واقعی Excel

verification اینجا به هیچ file Excel با axis ثانویه نیاز نداشت و نکته مفید داستان همین است. Structural decoding ویژگی sequence مربوط به record است، بنابراین sequence ساخته‌شده آن را به همان دقت sequence capture‌شده ثابت می‌کند. regression، AXESUSED را با payload برابر 2 می‌سازد، سپس دو block از نوع AxisParent می‌سازد که هرکدام category axis و value axis دارند و assert می‌کند model با AxisGroupCount = 2، چهار axis با stampهای 0، 0، 1، 1 و typeهای مورد انتظار برای جفت دوم برمی‌گردد

// verification ساختاری بدون درگیر کردن هیچ file از Excel
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 می‌سازید، دو نکته عملی مهم است. TXLSCustomChart.AddData(RecID, Len, nil) وقتی Len غیرصفر باشد payload را dereference می‌کند؛ پس markerهای Begin با مقدار $1033 و End با مقدار $1034 باید با length صفر اضافه شوند، نه با nil blob و length کهنه. همچنین sequence مصنوعی decoder را ثابت می‌کند، نه پذیرش output توسط Excel را؛ write side از ABNF ساخته شد و سپس با round-trip از مسیر GetChartModel check شد، با assert برای stampهای هر axis یعنی 0/0/1/1 و chart groupهای هر series یعنی 0/1 و مسیر بدون secondary که byte-identical باقی می‌ماند به‌عنوان safety net. این همان موضع محافظه‌کارانه‌ای است که باقی پشتیبانی HotXLS از chart، image و drawing در Delphi بر آن بنا شده است: آنچه recordها می‌گویند decode کن و از حدس زدن layout باینری‌ای که در spec نخوانده‌ای خودداری کن. suite کامل Delphi پس از ورود write side، در Win32 و Win64 هر 1650 از 1650 test را گذراند

پارامترهای صحنه Chart3d و دام fAuto

دو جزئیات مجاور، کسانی را که از chart پیش‌فرض جلوتر می‌روند گرفتار می‌کنند. اولی Chart3d با مقدار $103A و تعریف [MS-XLS] 2.4.46 است؛ payload تخت چهارده‌بایتی که برای variantهای 3D داخل chart group emit می‌شود: anRot یعنی rotation از 0 تا 360، anElev یعنی elevation علامت‌دار از -90 تا 90، pcDist یعنی فاصله perspective از 0 تا 100 که اگر fPerspective set نباشد نادیده گرفته می‌شود، pcHeight و pcDepth یعنی درصدی از chart width از 5 تا 500، pcGap از 0 تا 500 و grbit با bitهای fPerspective به مقدار $0001، fCluster به مقدار $0002، fAutoscale به مقدار $0004، f3DScaling به مقدار $0010 و f2DWalls به مقدار $0020. spec constraintهایی اضافه می‌کند که layout record خودش برای شما enforce نمی‌کند: در bar chart ترانهاده، anRot و anElev نباید از 44 بیشتر شوند و در pie chart، anElev نباید منفی باشد

دومی bit مربوط به fAuto است و همان چیزی است که bug report «رنگ‌های من نادیده گرفته شدند» را تولید می‌کند. LineFormat با مقدار $1007، AreaFormat با مقدار $100A و MarkerFormat با مقدار $1009 همگی fAuto را در bit 0 مربوط به grbit حمل می‌کنند و وقتی این bit set باشد، Excel style خودکار را اعمال می‌کند و RGB صریح، line style، marker shape و marker size مجاور آن را decoration در نظر می‌گیرد. هر emitterی که style سفارشی برای series می‌نویسد باید bit 0 را clear کند؛ emitterهای پیش‌فرض آن را عمداً set نگه می‌دارند تا Excel palette را انتخاب کند. اگر به‌جای ساخت workbook در حال ویرایش workbook موجود هستید، ruleهای preservation دوباره فرق می‌کنند و در ویرایش chartهای Excel بدون از دست دادن ChartML حفظ‌شده پوشش داده شده‌اند

گروه‌های axis ثانویه، binding مربوط به SerToCrt و typed chart model نشان‌داده‌شده در اینجا، در HotXLS Delphi spreadsheet component برای Delphi و C++Builder ارائه می‌شوند؛ این component chartهای BIFF8 را بدون نصب Excel می‌خواند و می‌نویسد و صفحه محصول، مرجع کامل chart record و فهرست overloadهای AddChartSheet را دارد