مقال تقني

محور التواريخ في رسوم HotXLS: علامات مطابقة للتقويم

يرسم HotXLS محاور تواريخ رسوم Excel بحساب تقويمي لا بعدد أيام ثابت. يحفظ الصنف TXLSChartDateAxisTransform في وحدة lxChart كل إحداثية بوصفها مسلسل مصنف، ويحسب كل علامة شهر أو سنة مباشرة من حد المحرك الأدنى عبر IncMonth وIncYear، ويثبت نهايات الأشهر على التقويم الحقيقي، فتأخذ المتتالية المرساة على 31 يناير علامات عند 28 فبراير و31 مارس و30 أبريل بدل الانزلاق. والتحويل نفسه يختار وحدات اليوم أو الشهر أو السنة تلقائيًا، ويستعمله معًا مُصيّرات الرسوم إلى HTML وSVG وPDF ذات الصفحات

الخطأ الذي يمنعه هذا مألوف لمن رسم من قبل إغلاق نهاية شهر. المُصيّر الذي يعامل الشهر كأنه 30 يومًا ينحرف خمسة أيام مبكرًا بحلول نهاية السنة الأولى. والمُصيّر الأذكى الذي يستخدم أشهر التقويم لكنه يقدّم كل علامة من سابقتها يفشل بصوت أهدأ: يصير 31 يناير 28 فبراير، وتهبط الخطوة التالية على 28 مارس، وتبقى كل علامة بعدها مثبتة على 28. يبدو الرسم مقنعًا، والعناوين خاطئة، ولا أحد ينتبه حتى يسأل المراقب المالي لماذا رسم رصيد مارس قبل نهاية الربع بثلاثة أيام

لماذا تنحرف علامات الأشهر حين تتراكم؟

تنحرف علامات الأشهر لأن التثبيت يرمي معلومات بعيدًا. بمجرد تثبيت 31 يناير على 28 فبراير، ضاعت حقيقة أن المتتالية أرادت يوم 31، وأي خطوة تنطلق من القيمة المثبتة ترث الخسارة. إن TXLSChartDateAxisTransform.BuildTicks لا ينطلق من علامة سابقة أبدًا. العلامة i تحسب دائمًا بوصفها AddUnits(MinValue, UnitKind, Step * i, ...)، أي الحد الأدنى + الوحدة × المؤشر مقاسًا من حد المحرك الأدنى. وAddUnits يضيف العدد إلى المسلسل مباشرة لوحدات اليوم، وللوحدات الشهر والسنة يحوّل المسلسل إلى تاريخ حقيقي ويستدعي IncMonth أو IncYear ثم يحوّل مرة أخرى. والحلقة محدودة من كل جهة: مصفوفة النتيجة سقفها 4096 مدخلًا مهما طلب المستدعي، ويُفحص Step * i مقابل MaxInt قبل الضرب، ويتوقف التوليد فور فشل علامة في الزيادة أو تجاوزها الحد الأقصى للمحرك

طريقتان يضع بهما محور التواريخ في رسوم HotXLS علامات الأشهر بعد مرساة 31 يناير 2026: تقديم كل علامة من القيمة المثبتة السابقة يبقيها مثبتة على 28، بينما يحسب BuildTicks‏ AddUnits(MinValue, xcduMonths, Step times i) من حد المحرك الأدنى فتهبط العلامات على 28 فبراير و31 مارس و30 أبريل
التثبيت يرمي معلومات بعيدًا، لذا لا ينطلق TXLSChartDateAxisTransform من علامة سابقة قط ويعيد حساب كل موضع من حد المحرك الأدنى، مثبتًا نهايات الأشهر على التقويم الحقيقي
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 علامة، لا تتجاوز 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 لقوة عشرية. ذلك التقدير يقرر فقط كم وحدة تقع بين العلامات. أما مواضع العلامات ذاتها فتظل تأتي من IncMonth وIncYear، فيستطيع التقريب تغيير كثافة العلامات لكنه لا يحرك علامة عن التقويم أبدًا

لماذا يُبقى على مسلسلات المصنف بدل التحويل إلى TDateTime؟

يحفظ محور التواريخ في HotXLS مسلسلات المصنف فضاءَ إحداثيات له لأن TDateTime في Delphi ليس له خانة للمسلسل 60، أي يوم 29 فبراير 1900 الوهمي الذي ورثه نظام التواريخ 1900 غرابةً توافقية. يحرّك TrySerialToDateTime المسلسلات تحت 60 يومًا واحدًا ويطوي المسلسل 60 على 28 فبراير، وهو ما ينفع لتسمية نقطة واحدة لكنه قاتل للهندسة: حوّل كل نقطة إلى TDateTime أولًا وسيصير اليومان على جانبي اليوم الوهمي أقرب بعضهما بوحدة واحدة مما يرسمهما Excel، فيزاح كل ما رسم قبل مارس 1900 بالنسبة لكل ما بعده. ويعالج TXLSChartDateAxisTransform.FormatValue الخانة نفسها على نحو خاص ويسميها 1900-02-29 تمامًا كما يفعل Excel. وفي مصنف 1904 يكون المسلسل 0 هو 1 يناير 1904، ويطبق التحويل الإزاحة الثابتة البالغة 1462 يومًا في الاتجاهين؛ والقصة الكاملة للعصرين في مقالتنا عن مسلسلات تواريخ Excel ونظام 1904 وتنسيقات الأرقام

لماذا يحفظ محور التواريخ في HotXLS مسلسلات المصنف بدل التحويل إلى TDateTime: المسلسل 60 هو يوم 29 فبراير 1900 الوهمي، ويحرك TrySerialToDateTime المسلسلات تحت 60 ويطوي 60 على 28 فبراير فيصير اليومان على الجانبين أقرب بوحدة مما يرسمهما Excel، بينما يسمي FormatValue المسلسل 60 بالقيمة 1900-02-29
الطي ينفع لتسمية نقطة واحدة لكنه قاتل للهندسة، لأن كل ما رسم قبل مارس 1900 يزاح مقابل كل ما بعده؛ ومصنف 1904 يطبق بدلًا من ذلك الإزاحة الثابتة البالغة 1462 يومًا

كيف يختار HotXLS الأيام أو الأشهر أو السنوات تلقائيًا؟

يختار TXLSChartDateAxisTransform.DetectUnit أدق وحدة تقويم تحتاجه فجوات البيانات فعلًا. يطرح الأسلوب قيم NaN واللانهائية أولًا، ويرتب البقية مرة واحدة بـ O(n log n)، ويتخطى التواريخ المكررة. إن كانت أي فجوة متجاورة أقصر من شهر تقويمي واحد فالوحدة أيام؛ وإلا إن كانت أي فجوة أقصر من سنة تقويمية فالوحدة أشهر؛ وإلا فسنوات. الاختبار هو IncMonth(Previous, 1) > Current لا عتبة 30 يومًا، فتُكشف متتالية نهايات شهر تجري 31 يناير ثم 28 فبراير ثم 31 مارس بصورة صحيحة بوصفها بيانات شهرية. ومع أقل من تاريخين صحيحين مختلفين لا توجد فجوة تقاس، فيرجع الأسلوب إلى الأيام

ثم يدمج ResolveUnits الوحدة المكتشفة مع ما يصرح به الرسم. الوحدة الأساسية الصريحة تحترم كما هي، والوحدة الكبرى أو الصغرى الغائبة ترث الوحدة المكتشفة حين تكون أغلظ من الأساسية. والخطوة الأخيرة هي التي يفوتها الناس: إن كانت الوحدة الكبرى أو الصغرى الفعلية أدق من الوحدة الأساسية، خفضت الأساسية لتطابقها. فالوحدة الأساسية هي ما تنضبط إليه نقاط البيانات قبل وجود أي علامات، إذ يقرّب Normalize القيمة إلى يومها أو أول شهرها أو 1 يناير، وإلا لانهارت قاعدة شهرية تحت وحدة كبرى يومية شهرًا كاملًا من النقاط في خانة واحدة قبل أن يرى كود العلامات شيئًا منها. وكثافة العلامات محدودة كذلك بطول مساحة الرسم وعرض خط المحرك المقاس وتنسيق أرقام العناوين وإسقاط العناوين المائلة، وكل خلفية يرسم العناوين وعلامات التجزئة وخطوط الشبكة من مصفوفة علامات واحدة مشتركة محدودة، والعلامة الصغرى المتطابقة مع كبرى ترسم مرة واحدة

كيف يختار HotXLS وحدات محور التواريخ تلقائيًا: يطرح DetectUnit قيم NaN واللانهائية ويرتب مرة واحدة ويقيس فجوات التقويم بـ IncMonth بدل عتبة 30 يومًا، فتُكشف متتالية 31 يناير و28 فبراير و31 مارس بوصفها شهرية، ويحترم ResolveUnits قاعدة صريحة مع خفضها حين تكون الوحدة الكبرى أدق
يقرّب Normalize كل قيمة إلى يومها أو أول شهرها أو 1 يناير قبل وجود أي علامات، لأن قاعدة شهرية تحت وحدة كبرى يومية ستنهار شهرًا من النقاط في خانة واحدة

ماذا يخزن عنصر dateAx، وماذا يعني غيابه؟

في ChartML كل ضبط تقويمي على محور التواريخ اختياري، وترك أحدها غير موجود ليس مثل كتابة قيمته الافتراضية. يتيح النوع CT_DateAx في ECMA-376 Part 1، §21.2 (DrawingML Charts) لكل من baseTimeUnit وmajorTimeUnit وminorTimeUnit وauto أن يحضر أو لا، وbaseTimeUnit الغائب يخبر Excel أن يقرر بنفسه، وهو ما يخالف days الصريحة. لذا يخزن TXLSXChartAxis كل قيمة بجوار علم حضور: BaseTimeUnit مع BaseTimeUnitSet، وMajorTimeUnit مع MajorTimeUnitSet، وMinorTimeUnit مع MinorTimeUnitSet، وAutoDateAxis مع AutoDateAxisSet. إسناد قيمة يرفع علمها، ومسح العلم يعيد العنصر إلى الغياب، وكاتب XLSX يصدر عنصرًا فقط حين يكون علمه مرفوعًا. ولاحظ أن auto تعني الكشف التلقائي بين فئة وتاريخ، لا وحدات علامات تلقائية

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 للمحرك بعد العنوان وقبل علامات التجزئة، وخاصيته sourceLinked افتراضها true، ويحفظ TXLSXChartAxis كود التنسيق وقيمة الربط بالمصدر وNumberFormatSet منفصلة، فلا يخل أبدًا بين كود فارغ وعنصر غائب. دوران النص يخزن بوحدات 1/60000 من الدرجة والقيم الموجبة تعني اتجاه عقارب الساعة؛ وTextStyle.Rotation تأخذ درجات عادية، ومخرج SVG يستخدم الزاوية كما هي، والخلفية الصفحاتية التي تجري زواياها الموجبة عكس عقارب الساعة تقلب الإشارة عند حد مشترك واحد. والمحرك المعكوس maxMin يعكس العلامات الكبرى والصغرى وخطوط الشبكة ونقاط البيانات وخطوط الاتجاه وأشرطة الخطأ معًا، لا العناوين وحدها أبدًا. وحين تفتح مصنفًا كتبه غيرك، تخبرك الأعلام عمّا حدده المؤلف فعلًا

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، وهو عدد مجرد لا يعرف أيامًا ولا أشهرًا ولا سنوات، فمحور Excel شهري محفوظ بصيغة ODS لا يستطيع حمل وحدته، وHotXLS لا يخترع خاصية غير قياسية لتمثيلها زورًا. وعلى الجانب القديم BIFF8، يحمل سجل AxcExt تسع كلمات ثابتة بعرض 16 بت، وأعلامه التلقائية تحجب الحقول فقط لا تحذفها؛ يحفظ HotXLS القيم المحجوبة للحد الأدنى والأقصى والفترة والوحدة ونقطة التقاطع عند القراءة وإعادة الكتابة، ويبقي أكواد الوحدات المجهولة سليمة في نموذج Classic، ويحول فقط وحدات التواريخ الصحيحة 0 و1 و2 (أيام، أشهر، سنوات) إلى XLSX

محاور التواريخ طبقة واحدة من نموذج رسم يتعين عليه أيضًا أن ينجو من ملفات لم يخلقها. حين يجمع مصنف Excel خطًا بمحور تواريخ مع متتالية أعمدة على محور ثانوي، يبين مقالتنا عن ChartML المحفوظ والرسوم المركبة كيف يعيد HotXLS تشغيل XML الرسم الأصلي بايتًا بايتًا حين لم يتغير شيء، ويدمج التعديلات المكتوبة الأنواع فيه حين تغير شيء، ويغطي مقالتنا الشاملة عن الرسوم والصور والرسومات واجهات الإرساء والمتتاليات المستعملة أعلاه. وكل ذلك يأتي في مكوّن HotXLS لدلفي المتاح لدلفي وC++Builder، والذي يقرأ ويكتب ويرسم رسوم XLS وXLSX بلا أتمتة Excel