HotXLS محورهای تاریخ chart اکسل را با حساب تقویمی رندر میکند، نه با شمارش روز ثابت. کلاس TXLSChartDateAxisTransform در یونیت lxChart هر مختصات را بهصورت سریال ورکبوک نگه میدارد، هر tick ماهانه یا سالانه را مستقیماً از کمینهٔ محور از طریق IncMonth و IncYear حساب میکند، و انتهای ماه را به تقویم واقعی گیره میکند، پس یک سری که به 31 ژانویه لنگر شده tickهای 28 فوریه و 31 مارس و 30 آوریل میگیرد بهجای اینکه سر بخورد. همین transform واحد روز و ماه و سال را خودکار انتخاب میکند و بین رندرکنندههای chart مربوط به HTML و SVG و PDF صفحهبندیشده مشترک است
باگی که این جلویش را میگیرد برای هر کسی که chart بستن آخر ماه کشیده آشناست. رندرکنندهای که ماه را 30 روز میگیرد تا پایان سال اول پنج روز زودتر drift میکند. رندرکنندهٔ باهوشتری که ماههای تقویمی را به کار میبرد ولی هر tick را از قبلی جلو میبرد، بیسروصداتر شکست میخورد: 31 ژانویه میشود 28 فوریه، قدم بعدی روی 28 مارس میافتد، و هر tick بعدش به 28 چسبیده میماند. chart محتمل به نظر میرسد، لیبلها غلطاند، و هیچکس نمیفهمد تا وقتی کنترلر بپرسد چرا موجودی مارس سه روز قبل از پایان فصل ترسیم شده
چرا tickهای ماهانه وقتی تجمیع میشوند drift میکنند؟
tickهای ماهانه drift میکنند چون گیره کردن اطلاعات را دور میریزد. وقتی 31 ژانویه یکبار به 28 فوریه گیره شد، این واقعیت که سری 31ام را میخواسته از بین رفته، و هر قدمی که از مقدار گیرهشده برداشته شود همین ضایعات را به ارث میبرد. TXLSChartDateAxisTransform.BuildTicks هرگز از tick قبلی قدم برنمیدارد. tick i همیشه بهصورت AddUnits(MinValue, UnitKind, Step * i, ...) حساب میشود، یعنی min + واحد × اندیس که از کمینهٔ محور اندازهگیری شده. AddUnits برای واحدهای روزی تعداد را مستقیم به سریال اضافه میکند و برای واحدهای ماهانه و سالانه سریال را به یک تاریخ واقعی تبدیل میکند، IncMonth یا IncYear را صدا میزند و برمیگرداند. حلقه هم از هر طرف کراندار است: آرایهٔ نتیجه فارغ از اینکه فراخوان چه بخواهد سقف 4096 مدخل دارد، Step * i قبل از ضرب با MaxInt چک میشود، و تولید همینجا که یک tick دیگر افزایش نیابد یا از بیشینهٔ محور رد شود متوقف میشود
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// سیستم تاریخ 1900 (Dates1904 = False)، سری لنگرشده به آخر ماه
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// قدم یکماهه، هدف 12 tick، هرگز بیشتر از 64
Count := TXLSChartDateAxisTransform.BuildTicks(MinSerial, MaxSerial,
1, xcduMonths, False, 12, 64, Ticks);
for I := 0 to Count - 1 do
Writeln(TXLSChartDateAxisTransform.FormatValue(Ticks[I], False));
// 2026-01-31, 2026-02-28, 2026-03-31, 2026-04-30 ... 2026-12-31
end;
در BuildTicks یک تقریب صادقانه هست. وقتی قدم درخواستی صفر است، متد اندازهٔ قدم را با فرض 30 روز برای ماه و 365 روز برای سال تخمین میزند، بعد قدم خام را به مضربی از 1 یا 2 یا 5 از یک توان ده گرد میکند. این تخمین فقط تصمیم میگیرد چند واحد بین tickها فاصله باشد. خودِ موقعیت tickها همچنان از IncMonth و IncYear میآیند، پس تقریب میتواند تراکم tick را عوض کند ولی هرگز یک tick را از تقویم برنمیدارد
چرا سریالهای ورکبوک را نگه میداریم بهجای تبدیل به TDateTime؟
یک محور تاریخ در HotXLS سریالهای ورکبوک را بهعنوان فضای مختصاتش نگه میدارد، چون TDateTime دلفی برای سریال 60 جایی ندارد، یعنی 29 فوریهٔ 1900 خیالی که سیستم تاریخ 1900 بهعنوان یک ظرافت سازگاری به ارث برده. TrySerialToDateTime سریالهای زیر 60 را یک روز شیفت میدهد و سریال 60 را روی 28 فوریه میچسباند، که برای لیبل کردن یک نقطهٔ تکی اشکالی ندارد ولی برای هندسه کشنده است: همهٔ نقاط را اول به TDateTime تبدیل کنید و دو روز دو طرف روز خیالی یک واحد به هم نزدیکتر از ترسیم اکسل میشوند، پس هر چه قبل از مارس 1900 ترسیم شده نسبت به بعدش جابهجا میشود. TXLSChartDateAxisTransform.FormatValue همین اسلات را خاص میگیرد و آن را 1900-02-29 لیبل میزند، دقیقاً مثل اکسل. در یک ورکبوک 1904، سریال 0 یعنی 1 ژانویه 1904، و transform آفست ثابت 1462 روزه را در هر دو جهت اعمال میکند؛ داستان کامل هر دو epoch را مقالهٔ ما دربارهٔ سریالهای تاریخ اکسل، سیستم 1904 و فرمتهای عددی دارد
HotXLS چطور روز و ماه و سال را خودکار انتخاب میکند؟
TXLSChartDateAxisTransform.DetectUnit ریزترین واحد تقویمیای را انتخاب میکند که شکافهای داده واقعاً لازم دارند. متد اول مقادیر NaN و بینهایت را کنار میگذارد، بقیه را یکبار در O(n log n) مرتب میکند، و تاریخهای تکراری را رد میکند. اگر هر شکاف مجاوری از یک ماه تقویمی کوتاهتر باشد واحد روز است؛ وگرنه اگر هر شکافی از یک سال تقویمی کوتاهتر باشد واحد ماه است؛ وگرنه سال. آزمون IncMonth(Previous, 1) > Current است، نه یک آستانهٔ 30 روزه، پس سری آخر-ماهی که 31 ژانویه و 28 فوریه و 31 مارس میرود درست بهعنوان دادهٔ ماهانه شناسایی میشود. با کمتر از دو تاریخ معتبر متمایز شکافی برای اندازهگیری نیست و متد به روز برمیگردد
ResolveUnits بعد واحد شناساییشده را با هر چیزی که chart اعلان کرده ترکیب میکند. base unit صریح محترم شمرده میشود، و major یا minor غایب وقتی درشتتر از base است همان واحد شناساییشده را به ارث میبرد. قدم آخر همان است که مردم از دستش میدهند: اگر unit مؤثر major یا minor ریزتر از base باشد، base برای همخوانی پایین کشیده میشود. base unit همان چیزی است که نقاط داده قبل از وجود هر tickی به آن نرمال میشوند، و Normalize یک مقدار را به روزش یا اول ماهش یا 1 ژانویه میچسباند، پس base ماهانه زیر major روزانه یک ماه نقطه را قبل از اینکه کد tick اصلاً ببیندشان در یک اسلات فرو میریزد. تراکم tick هم کران دارد: به طول plot، عرض اندازهگیریشدهٔ فونت محور، فرمت عددی لیبل و تصویر لیبلهای چرخیده، و هر backend لیبلها و tick markها و gridlineها را از یک آرایهٔ tick مشترک و کراندار ترسیم میکند، و minor tickی که بر major منطبق باشد فقط یکبار کشیده میشود
المان dateAx چه چیزی ذخیره میکند و حذف شدن یعنی چه؟
در ChartML هر تنظیم تقویمی روی یک محور تاریخ اختیاری است، و حذف کردن یکی با نوشتن مقدار پیشفرضش فرق دارد. نوع CT_DateAx در ECMA-376 Part 1، §21.2 (DrawingML Charts) اجازه میدهد baseTimeUnit و majorTimeUnit و minorTimeUnit و auto هر کدام حاضر باشند یا نباشند، و baseTimeUnit غایب به اکسل میگوید خودش تصمیم بگیرد، که با days صریح فرق دارد. پس TXLSXChartAxis هر مقدار را کنار یک پرچم حضور ذخیره میکند: BaseTimeUnit با BaseTimeUnitSet، MajorTimeUnit با MajorTimeUnitSet، MinorTimeUnit با MinorTimeUnitSet، و AutoDateAxis با AutoDateAxisSet. مقدار دادن پرچم را ست میکند، پاک کردن پرچم المان را به حالت حذف برمیگرداند، و writer مربوط به XLSX المان را فقط وقتی پرچمش ست است بیرون میدهد. دقت کنید auto یعنی تشخیص خودکار category در برابر date، نه واحدهای tick خودکار
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
Axis: TXLSXChartAxis;
M: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Close');
Sheet.Cells[1, 1].Value := 'Month end';
Sheet.Cells[1, 2].Value := 'Balance';
for M := 1 to 12 do
begin
Sheet.Cells[M + 1, 1].Value :=
EncodeDate(2026, M, DaysInAMonth(2026, M)); // DateUtils
Sheet.Cells[M + 1, 2].Value := 1000 + M * 75;
end;
Chart := Sheet.AddLineChart('Month-end balance',
'Close!$A$2:$A$13', 'Close!$B$2:$B$13', 15, 1, 32, 9);
Axis := Chart.CategoryAxis;
Axis.Kind := xlsxAxisDate; // باعث نوشتن c:dateAx میشود
Axis.BaseTimeUnit := xlsxChartTimeDays; // BaseTimeUnitSet را هم ست میکند
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 یعنی majorUnit ست شده
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // درجه؛ بهصورت 1/60000 ذخیره میشود
// AutoDateAxis دست نخورده: المان c:auto نوشته نمیشود
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
لیبلها و جهتگیری هم از همان قواعد حضور-آگاه پیروی میکنند. numFmt محور بعد از title و قبل از tick markها مینشیند، اتریبیوت sourceLinked آن پیشفرض true دارد، و TXLSXChartAxis کد فرمت و مقدار source-linked و NumberFormatSet را جدا نگه میدارد، پس یک کد خالی هرگز با المان غایب اشتباه نمیشود. چرخش متن بهصورت 1/60000 درجه ذخیره میشود با مقادیر مثبت یعنی ساعتگرد؛ TextStyle.Rotation درجهٔ ساده میگیرد، خروجی SVG همان زاویه را به کار میبرد، و backend صفحهبندیشده که زوایای مثبتش پادساعتگرد است علامت را در یک مرز مشترک برمیگرداند. محور maxMin معکوس tickهای major و minor و gridlineها و نقاط داده و trendlineها و error barها را با هم آینه میکند، و هرگز فقط لیبلها را نه. وقتی ورکبوکی که جای دیگری نوشته شده را باز میکنید، پرچمها به شما میگویند نویسنده واقعاً چه چیزهایی را صریح گفته
var
Book: TXLSXWorkbook;
Axis: TXLSXChartAxis;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('forecast.xlsx') <> 1 then
raise Exception.Create('Cannot open workbook');
Axis := Book.Sheets[0].Charts[0].CategoryAxis;
if Axis.Kind = xlsxAxisDate then
begin
if Axis.BaseTimeUnitSet then
Writeln('baseTimeUnit = ', Ord(Axis.BaseTimeUnit))
else
Writeln('baseTimeUnit omitted: Excel chooses at render time');
if Axis.AutoDateAxisSet then
Writeln('auto = ', Axis.AutoDateAxis);
end;
finally
Book.Free; // دوباره ذخیره کنید و المانهای حذفشده حذفشده میمانند
end;
end;
مدل تقویمی کجا تمام میشود؟
مدل تقویمی در فرمتهایی تمام میشود که واحد تقویمی نمیفهمند، و HotXLS خودش را به آنها نمیفروشد. ODF 1.2 چیزی به نام chart:interval-major دارد که یک عدد ساده است و هیچ تصوری از روز و ماه و سال ندارد، پس یک محور ماهانهٔ اکسل که به ODS ذخیره شود واحدش را نمیتواند حمل کند و HotXLS برای فیک کردنش اتریبیوت غیراستانداردی نمیسازد. در سمت BIFF8 قدیمی، رکورد AxcExt نه واژهٔ ثابت 16 بیتی حمل میکند، و پرچمهای automatic آن فیلدها را ماسک میکنند نه حذف؛ HotXLS مقادیر ماسکشدهٔ کمینه و بیشینه و interval و unit و crossing را در خواندن و نوشتن دوباره حفظ میکند، کدهای واحد ناشناخته را در مدل Classic دستنخورده نگه میدارد، و فقط واحدهای تاریخ معتبر 0 و 1 و 2 (روز و ماه و سال) را به XLSX نگاشت میکند
محورهای تاریخ یک لایه از مدل chart هستند که باید فایلهایی که خودش نساخته را هم سالم عبور دهد. وقتی یک ورکبوک اکسل یک خط با محور تاریخ را با یک سری ستونی روی محور ثانویه ترکیب میکند، مقالهٔ ما دربارهٔ ChartML حفظشده و combination chartها نشان میدهد HotXLS وقتی چیزی عوض نشده XML اصلی chart را بایتبهبایت دوباره پخش میکند و وقتی چیزی عوض شده ویرایشهای تایپدار را داخلش ادغام میکند، و مرور ما بر chartها و imageها و drawingها APIهای لنگر و سری استفادهشده در بالا را پوشش میدهد. همهٔ اینها در HotXLS Delphi Component برای Delphi و C++Builder ارسال میشود که chartهای XLS و XLSX را بدون Excel automation میخواند و مینویسد و رندر میکند