HotXLS ghi nhóm trục phụ của chart BIFF8 bằng cách emit block AxisParent thứ hai, không phải append chart group thứ hai sau các axis. Trong Classic XLS chart substream, mọi chart group — ChartFormat, chart type record và CrtLink — nằm bên trong axis-group block riêng của nó, còn mỗi series bind vào một group qua SerToCrt. Đảo ngược nesting đó thì không có chart group thứ hai cho series bind vào, dù bạn emit bao nhiêu axis record đi nữa
Vì sao chart group thứ hai sau các axis không bind được gì?
Grammar là toàn bộ câu trả lời, và nó chỉ là một dòng ABNF. Rule CHARTFOMATS trong [MS-XLS] 2.1.7.20.1 nói AxesUsed 1*2AXISPARENT, rồi ghi rõ AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Đọc hai production cùng nhau sẽ thấy shape: chart group là child của axis group chứ không phải sibling. Chart substream có hai cặp axis và một chart group ở cuối không phải dual-axis chart có layout kỳ quặc; đó là chart có một chart group và một tập axis record mồ côi. Điều này quan trọng vì SerToCrt ($1045), nằm trong block SERIESFORMAT, mang index chart group zero-based chứ không phải axis index. Ghi crt = 1 khi chỉ có một block CRT sẽ trỏ series tới chart group chưa từng được emit. Trực giác khiến người ta vấp là record name: AXESUSED ($1046) nghe như đếm axis nên bước tự nhiên tiếp theo là emit thêm axis. Nó đếm axis group, và mỗi axis group kéo theo một plot area hoàn chỉnh cùng chart group
Đánh dấu series cho secondary axis group
Phía HotXLS quy về một Boolean. TXLSChartSeriesInfo có field SecondaryAxis, và đặt nó trên bất kỳ series nào trong array truyền cho TXLSWorksheets.AddChartSheet sẽ chuyển toàn bộ builder sang mode hai group. Không có call riêng "enable secondary axis" cũng không có axis-count parameter vì count có thể suy ra: nếu bất kỳ series nào muốn secondary group, chart cần hai group
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... điền A1:C12 với category, revenue và margin ...
Series[0] := Default(TXLSChartSeriesInfo); // không bao giờ FillChar record này
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 trở thành 2
Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
'Revenue vs Margin', '', '', Series);
Wb.SaveAs('dual-axis.xls', xlExcel97);
finally
Wb.Free;
end;
end;
Dòng Default(TXLSChartSeriesInfo) không phải decoration. TXLSChartSeriesInfo trộn managed field (tên WideString, dynamic trendline và error-bar array) với member Boolean thường, còn Delphi chỉ bảo đảm managed field được clear tự động. Để SecondaryAxis chưa khởi tạo thì nó sẽ nhận bất cứ thứ gì đang ở stack, thực tế là cùng binary có thể tạo single-axis chart khi chạy từ console host và dual-axis chart dưới test runner. Trong khi đó category và value range được resolve qua EXTERNSHEET table của workbook trước khi builder nhìn thấy — cùng index machinery được nói trong cách HotXLS phân loại BIFF SupBook và XTI external link — nên range chỉ tới sheet không biết sẽ degrade thành placeholder BRAI rỗng thay vì fail build
HotXLS emit gì khi một series là secondary?
Emitter đổi shape chứ không chỉ đổi value. Không có secondary series, AXESUSED giữ 1 (hoặc 0 với pie và 3D pie vì chúng không có axis group) rồi theo sau là một block AxisParent. Có một secondary series, AXESUSED giữ 2 và builder chạy block hai lần, với iax — word đầu tiên của payload AxisParent ($1041) dài 18 byte — lần lượt bằng 0 và 1. Mỗi pass emit Pos, category axis và value axis (Axis, $101D), marker PlotArea ($1035), một Frame mặc định, rồi ChartFormat ($1014), chart type record, CrtLink ($1022) và hai marker End để đóng chart group cùng axis group. Secondary series bind bằng SerToCrt crt = 1, primary giữ crt = 0. Có một record cố ý không duplicate: legend chỉ emit trong group đầu vì Excel cho một chart một legend dù chart có bao nhiêu axis group. Hai property khác cũng cần nói rõ. Restructure emitter để đặt chart group trong axis-parent block không đổi output của chart thường — khi không có secondary series, substream byte-identical với version trước vì parameterize AddAxisParent với iax = 0 chính là code path cũ. Builder vẫn emit một cặp axis đầy đủ cho mỗi group, nên secondary group luôn có category axis riêng dù bạn chỉ quan tâm value scale
Chart inspection khôi phục binding của axis group bằng cách nào?
Reading là hai pass trên record list, bắt buộc phải vậy vì AXESUSED xuất hiện trước các block mà nó mô tả. Pass đầu chỉ tìm $1046 và đọc word đầu làm axis-group count. Value bắt đầu từ 1 và chỉ tăng chứ không giảm: HotXLS lấy maximum giữa count hiện tại và count được khai báo, nên AXESUSED malformed hoặc duplicate không thể làm chart đã thấy hai group bị tụt xuống. Pass thứ hai track axis group hiện tại, update tại mỗi AxisParent rồi stamp index đó lên mọi Axis record gặp được cho tới AxisParent tiếp theo
var
Model: TXLSChartModel;
i: Integer;
begin
// Sheets[1] là data worksheet, Sheets[2] là 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;
Có hai giới hạn đáng gọi tên. TXLSChartModel.AxisGroupCount báo điều file khai báo, không phải số block AxisParent thực sự tìm thấy; file nói 2 nhưng chỉ ship một block vẫn báo 2, còn AxisCount là nơi bạn nhận ra. TXLSChartAxis.AxisGroup là positional stamp: nó ghi block mà axis được đọc bên trong, đó là điều duy nhất format cho bạn biết. Phía series, decode SerToCrt chỉ được bật khi đang ở trong Series block, vì cùng record id xuất hiện trong context không phải series binding, còn decoder không gated sẽ vui vẻ overwrite nhầm series
Verify secondary axis mà không cần file Excel thật
Verification ở đây không cần file Excel có secondary axis, và đó là phần hữu ích của câu chuyện. Structural decoding là property của record sequence, nên sequence tổng hợp chứng minh tốt như sequence capture. Regression dựng AXESUSED với payload 2, rồi hai block AxisParent mỗi block bọc một category axis và một value axis, sau đó assert model trả về AxisGroupCount = 2, bốn axis stamp 0, 0, 1, 1 và axis type kỳ vọng trên cặp thứ hai
// Structural verification hoàn toàn không dùng file Excel
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED: hai 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;
Có hai note thực tế nếu bạn tự tổng hợp record. TXLSCustomChart.AddData(RecID, Len, nil) dereference payload khi Len khác zero, nên marker Begin ($1033) và End ($1034) phải được add với length zero, không dùng nil blob cùng stale length. Sequence tổng hợp chứng minh decoder chứ không chứng minh Excel chấp nhận output — write side được dựng từ ABNF rồi check bằng round-trip qua GetChartModel, assert per-axis stamp 0/0/1/1 và per-series chart group 0/1, còn path không-secondary byte-identical là safety net. Đây cũng là posture conservative mà phần còn lại của HotXLS chart, image và drawing support cho Delphi dựa vào: decode điều record nói và từ chối đoán binary layout chưa đọc trong spec. Delphi suite đầy đủ chạy 1650 trên 1650 ở Win32 và Win64 sau khi write side hoàn tất
Chart3d scene parameter và cái bẫy fAuto
Có hai chi tiết kề nhau sẽ cắn bạn khi đi xa hơn chart mặc định. Thứ nhất là Chart3d ($103A, [MS-XLS] 2.4.46), payload phẳng 14 byte được emit trong chart group cho biến thể 3D: anRot (rotation, 0 đến 360), anElev (elevation, signed, -90 đến 90), pcDist (perspective distance, 0 đến 100, bị bỏ qua trừ khi bật fPerspective), pcHeight và pcDepth (phần trăm chart width, 5 đến 500), pcGap (0 đến 500) cùng một grbit có bit fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 và f2DWalls $0020. Spec thêm constraint mà record layout không tự ép: trên transposed bar chart, anRot và anElev không được vượt 44, còn trên pie chart anElev không được âm
Thứ hai là bit fAuto, và nó tạo ra bug report "màu của tôi bị bỏ qua". LineFormat ($1007), AreaFormat ($100A) và MarkerFormat ($1009) đều mang fAuto ở bit 0 của grbit, và khi bit đó bật, Excel áp dụng automatic style rồi coi RGB tường minh, line style, marker shape và marker size bên cạnh chỉ là decoration. Emitter ghi custom series style phải clear bit 0; emitter mặc định giữ nó bật chính xác để Excel chọn palette. Nếu bạn edit workbook có sẵn thay vì build mới, preservation rule lại khác và được nói trong việc edit Excel chart mà không làm mất ChartML được giữ lại
Secondary axis group, binding SerToCrt và typed chart model trong bài này nằm trong HotXLS Delphi spreadsheet component cho Delphi và C++Builder, đọc và ghi BIFF8 chart mà không cần Excel cài đặt; product page có full chart record reference và danh sách overload AddChartSheet