HotXLS เขียน secondary axis group ของ BIFF8 chart ด้วยการ emit AxisParent block ตัวที่สอง ไม่ใช่ append chart group ตัวที่สองหลัง axes ใน Classic XLS chart substream ทุก chart group ไม่ว่าจะเป็น ChartFormat, chart type record หรือ CrtLink จะอยู่ภายใน axis-group block ของตัวเอง และแต่ละ series จะ bind เข้ากับหนึ่ง group ผ่าน SerToCrt หากวาง nesting ผิดก็จะไม่มี chart group ตัวที่สองให้ series bind ไม่ว่าคุณจะ emit axis record มากเพียงใด
ทำไม chart group ตัวที่สองหลัง axes จึง bind ไม่ได้
คำตอบทั้งหมดอยู่ใน grammar และมีความยาวเพียงหนึ่งบรรทัด ABNF กฎ 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 pair สองชุดกับ chart group ที่ต่อท้ายหนึ่งชุดจึงไม่ใช่ dual-axis chart ที่ layout แปลก แต่เป็น chart ที่มี chart group เดียวและมี axis record กำพร้าชุดหนึ่ง เรื่องนี้สำคัญเพราะ SerToCrt ($1045) ซึ่งอยู่ใน SERIESFORMAT block เก็บ index ของ chart group แบบเริ่มที่ศูนย์ ไม่ใช่ axis index การเขียน crt = 1 เมื่อมี CRT block เดียวจึงชี้ series ไปยัง chart group ที่ไม่เคย emit สัญชาตญาณที่ทำให้คนสับสนมาจากชื่อ record AXESUSED ฟังเหมือนนับ axis จึงมักคิดว่าจะ emit axis เพิ่ม แต่มันนับ axis group และทุก axis group จะพา plot area กับ chart group ครบชุดมาด้วย
mark series ให้ใช้ secondary axis group
ฝั่ง HotXLS เรื่องนี้ยุบเหลือ Boolean เดียว TXLSChartSeriesInfo มี field SecondaryAxis และเมื่อ set ให้ series ใดใน array ที่ส่งเข้า TXLSWorksheets.AddChartSheet builder ทั้งชุดจะเปลี่ยนเป็น two-group mode ไม่มี call แยกสำหรับ "เปิด secondary axis" และไม่มี axis-count parameter เพราะ count derive ได้ หากมี 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); // ห้าม 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) ไม่ใช่ของประดับ TXLSChartSeriesInfo ผสม managed field ซึ่งรวม WideString name, dynamic trendline และ error-bar array เข้ากับ Boolean ธรรมดา และ Delphi รับประกันเพียงว่า managed field จะถูก clear ให้เอง หากปล่อย SecondaryAxis ไม่ initialize มันจะมีค่าตามสิ่งที่อยู่บน stack ซึ่งในทางปฏิบัติ binary เดียวกันอาจสร้าง chart แกนเดียวเมื่อรันจาก console host แต่สร้าง dual-axis chart ภายใต้ test runner ขณะเดียวกัน category และ value range จะ resolve ผ่าน EXTERNSHEET table ของ workbook ก่อน builder จะเห็นมันเสียอีก ซึ่งเป็น index machinery เดียวกับที่กล่าวใน วิธีที่ HotXLS จัดประเภท BIFF SupBook และ XTI external link ดังนั้น range ที่อ้าง sheet ซึ่งไม่รู้จักจะลดรูปเป็น empty placeholder BRAI แทนการทำให้ build fail
HotXLS emit อะไรเมื่อ series เป็น secondary
emitter เปลี่ยนรูป ไม่ใช่แค่ค่าเดียว เมื่อไม่มี secondary series AXESUSED จะเป็น 1 หรือเป็น 0 สำหรับ pie และ 3D pie ที่ไม่มี axis group เลย และจะมี AxisParent block เดียวตามมา เมื่อมี secondary AXESUSED เป็น 2 และ builder จะรัน block สองครั้ง โดย iax ซึ่งเป็น word แรกของ payload AxisParent ขนาด 18 byte ($1041) เป็น 0 แล้วเป็น 1 ในแต่ละรอบจะ emit Pos, category axis กับ value axis (Axis, $101D), marker PlotArea ($1035), default Frame ตามด้วย ChartFormat ($1014), chart type record, CrtLink ($1022) และ End marker สองตัวเพื่อปิด chart group กับ axis group secondary series จะ bind ด้วย SerToCrt crt = 1 ส่วน primary series คง crt = 0 ไว้ record หนึ่งตัวไม่ถูก duplicate โดยตั้งใจ legend จะ emit เฉพาะใน group แรก เพราะ Excel ให้ chart มี legend เดียวไม่ว่าจะมี axis group กี่ชุด มีคุณสมบัติอีกสองอย่างที่ควรพูดให้ชัด การปรับ emitter ให้ chart group อยู่ภายใน axis-parent block ไม่เปลี่ยน output ของ ordinary chart เมื่อไม่มี secondary series substream จะ byte-identical กับ version เดิม เพราะการ parameterize AddAxisParent ด้วย iax = 0 ก็คือ old code path พอดี และ builder ยัง emit axis pair เต็มชุดต่อ group ดังนั้น secondary group จะมาพร้อม category axis ของตัวเองเสมอ แม้คุณสนใจเฉพาะ value scale
chart inspection กู้ axis-group binding อย่างไร
การอ่านต้องทำสอง pass บน record list และจำเป็นเพราะ AXESUSED มาก่อน block ที่มันอธิบาย pass แรกหาเฉพาะ $1046 แล้วอ่าน word แรกเป็นจำนวน axis group ค่านี้เริ่มที่ 1 และเพิ่มได้อย่างเดียวไม่ลด HotXLS จะใช้ค่าสูงสุดระหว่าง count ปัจจุบันกับค่าที่ประกาศ ดังนั้น AXESUSED ที่ malformed หรือซ้ำจะไม่ทำให้ chart ที่เคยประกาศสอง group แล้วถอยกลับ pass ที่สอง track axis group ปัจจุบัน update ทุกครั้งที่เจอ 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 รายงานสิ่งที่ไฟล์ ประกาศ ไม่ใช่จำนวน AxisParent block ที่พบจริง ไฟล์ที่บอก 2 แต่ส่งมา block เดียวจะรายงาน 2 และคุณจะสังเกตได้ที่ AxisCount ส่วน TXLSChartAxis.AxisGroup เป็น positional stamp บอกว่า axis ถูกอ่านอยู่ภายใน block ไหน ซึ่งเป็นข้อมูลเดียวที่ format บอกได้ ฝั่ง series การ decode SerToCrt จะทำเมื่ออยู่ใน Series block เท่านั้น เพราะ record id เดียวกันปรากฏใน context ที่ไม่ใช่ series binding และ decoder ที่ไม่ gate จะเขียนทับ series ผิดตัวอย่างสบาย
ตรวจสอบ secondary axis โดยไม่ใช้ไฟล์ Excel จริง
การตรวจสอบนี้ไม่ต้องใช้ Excel file ที่มี secondary axis และนี่คือส่วนที่มีประโยชน์ structural decoding เป็นคุณสมบัติของ record sequence ดังนั้น sequence ที่สังเคราะห์ขึ้นพิสูจน์ได้ตรง ๆ ไม่ต่างจาก sequence ที่ capture มา regression สร้าง AXESUSED ด้วย payload 2 แล้วสร้าง AxisParent block สองตัวที่แต่ละตัวครอบ category axis กับ value axis และ assert ว่า model กลับมาด้วย AxisGroupCount = 2 มี axis สี่ตัวที่ stamp เป็น 0, 0, 1, 1 และมี axis type ที่คาดไว้ใน pair ที่สอง
// ตรวจโครงสร้างโดยไม่เกี่ยวข้องกับ Excel file เลย
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;
มี practical note สองข้อหากคุณ synthesize record เอง TXLSCustomChart.AddData(RecID, Len, nil) จะ dereference payload เมื่อ Len ไม่เป็นศูนย์ ดังนั้น marker Begin ($1033) กับ End ($1034) ต้อง add ด้วย length ศูนย์ ไม่ใช่ส่ง nil blob พร้อม length เก่าที่ไม่ใช่ศูนย์ และ synthetic sequence พิสูจน์ decoder ไม่ได้พิสูจน์ว่า Excel จะรับ output ของคุณ ฝั่งเขียนถูกวางโครงจาก ABNF แล้ว check ด้วยการ round-trip ผ่าน GetChartModel พร้อม assert axis stamp 0/0/1/1 และ chart group ของ series 0/1 โดยมี no-secondary path ที่ byte-identical เป็น safety net นี่คือท่าที conservative เดียวกับที่ HotXLS chart, image และ drawing support สำหรับ Delphi ทั้งหมดสร้างอยู่บนมัน คือ decode ตามที่ record บอก และปฏิเสธการเดา binary layout ที่ยังไม่ได้อ่านจาก spec Delphi suite ฉบับเต็มรันผ่าน 1650 จาก 1650 บน Win32 และ Win64 หลังฝั่งเขียนเสร็จ
Chart3d scene parameter และกับดัก fAuto
รายละเอียดที่อยู่ติดกันสองอย่างจะเล่นงานคนที่ไปไกลกว่า chart default อย่างแรกคือ Chart3d ($103A, [MS-XLS] 2.4.46) ซึ่งเป็น payload แบนขนาด 14 byte ที่ emit ใน chart group สำหรับ 3D variant ได้แก่ anRot (rotation, 0 ถึง 360), anElev (elevation, signed, -90 ถึง 90), pcDist (perspective distance, 0 ถึง 100 และจะถูก ignore หากไม่ set fPerspective), pcHeight กับ pcDepth (เปอร์เซ็นต์ของ chart width, 5 ถึง 500), pcGap (0 ถึง 500) และ grbit ที่มี bit เป็น fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 และ f2DWalls $0020 spec ยังใส่ constraint ที่ record layout ไม่บังคับให้คุณ สำหรับ transposed bar chart anRot กับ anElev ต้องไม่เกิน 44 และสำหรับ pie chart anElev ห้ามติดลบ
อย่างที่สองคือ bit fAuto และมันเป็นตัวสร้าง bug report ว่า "ทำไมสีของฉันถูก ignore" LineFormat ($1007), AreaFormat ($100A) และ MarkerFormat ($1009) ต่างมี fAuto ใน bit 0 ของ grbit และเมื่อ bit นี้ถูก set Excel จะใช้ automatic style และถือ explicit RGB, line style, marker shape กับ marker size ที่อยู่ข้าง ๆ เป็นเพียง decoration emitter ที่เขียน custom series style ต้อง clear bit 0 ส่วน default emitter ปล่อยให้ set ไว้เพื่อให้ Excel เลือก palette หากคุณกำลังแก้ workbook เดิมแทนการสร้างใหม่ preservation rule จะแตกต่างกัน และมีอธิบายใน การแก้ Excel chart โดยไม่ทำ ChartML ที่ preserve ไว้หาย
secondary axis group, SerToCrt binding และ typed chart model ที่แสดงอยู่ใน HotXLS Delphi spreadsheet component สำหรับ Delphi และ C++Builder ซึ่งอ่านและเขียน BIFF8 chart ได้โดยไม่ต้องติดตั้ง Excel ส่วน product page มี chart record reference ฉบับเต็มและรายการ overload ของ AddChartSheet