מאמר טכני

ציר תאריכים בגרף HotXLS: סימונים מדויקים ללוח השנה ב-Delphi

‏HotXLS מצייר צירי תאריכים של גרפי Excel בחשבון לוח שנה, לא בספירת ימים קבועה. המחלקה TXLSChartDateAxisTransform ביחידת lxChart מחזיקה כל קואורדינטה כ-serial של חוברת עבודה, מחשבת כל tick של חודש או שנה ישירות מהמינימום של הציר דרך IncMonth ו-IncYear, ומצמידה סופי חודש ללוח השנה האמיתי, כך שסדרה שמעוגנת ב-31 בינואר מקבלת ticks ב-28 בפברואר, ב-31 במרץ וב-30 באפריל במקום להחליק. אותה טרנספורמציה בוחרת יחידות של יום, חודש או שנה אוטומטית, והיא משותפת לרנדררי הגרפים של HTML, SVG ו-PDF המדורג

הבאג שזה מונע מוכר לכל מי שתיקלדר פעם סגירת סוף חודש בגרף. רנדרר שמתייחס לחודש כאל 30 ימים סוטה חמישה ימים קדימה עד סוף השנה הראשונה. רנדרר חכם יותר שמשתמש בחודשי לוח שנה אבל מקדם כל tick מהקודם נכשל בשקט יותר: 31 בינואר הופך ל-28 בפברואר, הצעד הבא נוחת על 28 במרץ, וכל tick אחריו נשאר נעוץ על ה-28. הגרף נראה סביר, התוויות שגויות, ואף אחד לא שם לב עד שמנהל הכספים שואל למה יתרת מרץ משורטטת שלושה ימים לפני סוף הרבעון

למה ticks של חודשים סוטים כשהם נצברים?

ticks של חודשים סוטים כי היצמדות (clamping) משליכה מידע. ברגע ש-31 בינואר הוצמד ל-28 בפברואר, העובדה שהסדרה רצתה את ה-31 אבדה, וכל צעד שנעשה מהערך המוצמד יורש את האובדן. TXLSChartDateAxisTransform.BuildTicks לעולם לא צועד מ-tick קודם. tick i תמיד מחושב כ-AddUnits(MinValue, UnitKind, Step * i, ...), כלומר min + יחידה × אינדקס שנמדד מהמינימום של הציר. AddUnits מוסיף את המספר ישירות ל-serial עבור יחידות יום, ועבור יחידות חודש ושנה ממיר את ה-serial לתאריך אמיתי, קורא ל-IncMonth או ל-IncYear, וממיר בחזרה. הלולאה גם חסומה מכל כיוון: מערך התוצאה מוגבל ל-4096 ערכים לא משנה מה הקורא מבקש, Step * i נבדק מול MaxInt לפני הכפל, והייצור נעצר ברגע ש-tick לא מצליח לגדול או עובר את המקסימום של הציר

שתי דרכים שבהן ציר תאריכים של גרף HotXLS יכול למקם ticks חודשיים אחרי עוגן של 31 בינואר 2026: קידום כל tick מהערך המוצמד הקודם נשאר נעוץ על ה-28, בזמן ש-BuildTicks מחשב AddUnits(MinValue, xcduMonths, Step כפול i) מהמינימום של הציר, וה-ticks נוחתים על 28 בפברואר, 31 במרץ ו-30 באפריל
היצמדות משליכה מידע, ולכן TXLSChartDateAxisTransform לעולם לא צועד מ-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 ticks, לא יותר מ-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 של חזקה של עשר. ההערכה הזאת מכריעה רק כמה יחידות בין ticks. מיקומי ה-ticks עצמם עדיין מגיעים מ-IncMonth ו-IncYear, כך שהקירוב יכול לשנות את צפיפות ה-ticks אבל לעולם לא מזיז tick מחוץ ללוח השנה

למה לשמור serials של חוברת במקום להמיר ל-TDateTime?

ציר תאריכים ב-HotXLS משמר serials של חוברת עבודה בתור מרחב הקואורדינטות שלו, כי ל-TDateTime של דלפי אין משבצת ל-serial 60, ה-29 בפברואר 1900 הפנטום שמערכת התאריכים של 1900 ירשה כמוזרות תאימות. TrySerialToDateTime מזיז serials מתחת ל-60 ביום אחד ומקפל את serial 60 על 28 בפברואר, מה שתקין לתיוג נקודה בודדת אבל קטלני לגיאומטריה: המר קודם כל נקודה ל-TDateTime ושני הימים משני צידי היום הפנטום יגמרו קרובים יחידה אחת יותר ממה ש-Excel מצייר אותם, כך שכל מה ששורטט לפני מרץ 1900 זז ביחס לכל מה שאחריו. TXLSChartDateAxisTransform.FormatValue מתייחס במיוחד לאותה משבצת ומתייג אותה 1900-02-29 בדיוק כמו Excel. בחוברת 1904, serial 0 הוא 1 בינואר 1904, והטרנספורמציה מיישמת את היסט ה-1462 הימים הקבוע לשני הכיוונים; הסיפור המלא של שתי התקופות נמצא במאמר שלנו על serials של תאריכי Excel, מערכת ה-1904 ופורמטי מספרים

למה ציר תאריכים ב-HotXLS משמר serials של חוברת במקום להמיר ל-TDateTime: serial 60 הוא ה-29 בפברואר 1900 הפנטום, TrySerialToDateTime מזיז serials מתחת ל-60 ומקפל את 60 על 28 בפברואר כך שהימים משני הצדדים מגיעים קרובים יחידה אחת יותר ממה ש-Excel מצייר, בזמן ש-FormatValue מתייג את serial 60 כ-1900-02-29
הקיפול תקין לתיוג נקודה בודדת אבל קטלני לגיאומטריה, כי כל מה ששורטט לפני מרץ 1900 זז מול כל מה שאחריו; חוברת 1904 מיישמת במקום זאת את היסט ה-1462 הימים הקבוע

איך HotXLS בוחר ימים, חודשים או שנים אוטומטית?

‏TXLSChartDateAxisTransform.DetectUnit בוחר את יחידת לוח השנה העדינה ביותר שהפערים בנתונים באמת צריכים. המתודה משליכה קודם ערכי NaN ואינסוף, ממיינת את השאר פעם אחת ב-O(n log n), ומדלגת על תאריכים כפולים. אם פער צמוד כלשהו קצר מחודש לוח שנה אחד, היחידה היא ימים; אחרת, אם פער כלשהו קצר משנת לוח שנה אחת, היחידה היא חודשים; אחרת היא שנים. הבדיקה היא IncMonth(Previous, 1) > Current, לא סף של 30 ימים, כך שסדרת סוף חודש שרצה 31 בינואר, 28 בפברואר, 31 במרץ מזוהה נכון כנתונים חודשיים. עם פחות משני תאריכים תקפים נבדלים אין פער למדידה, והמתודה חוזרת לימים

‏ResolveUnits ממזג אז את היחידה שזוהתה עם מה שהגרף מכריז. יחידת base מפורשת מכובדת, ויחידת major או minor חסרה יורשת את היחידה שזוהתה כשהיא גסה יותר מה-base. הצעד האחרון הוא זה שאנשים מפספסים: אם יחידת ה-major או ה-minor האפקטיבית עדינה יותר מיחידת ה-base, ה-base מורד כדי להתאים. יחידת ה-base היא היעד שאליו נקודות הנתונים מנורמלות לפני שקיימים ticks כלשהם, כאשר Normalize מצמיד ערך ליום שלו, לראש החודש שלו או ל-1 בינואר, כך ש-base חודשי מתחת ליחידת major יומית היה מכווץ חודש שלם של נקודות למשבצת אחת לפני שקוד ה-ticks ראה אותן בכלל. גם צפיפות ה-ticks חסומה, לפי אורך אזור השרטוט, רוחב הגופן של הציר כפי שנמדד, פורמט המספר של התוויות וההקרנה של תוויות מסובבות, וכל backend מצייר תוויות, סימוני tick וקווי רשת ממערך ticks משותף וחסום אחד, כש-minor tick שמצטלב עם major tick מצויר פעם אחת בלבד

איך HotXLS בוחר יחידות ציר תאריך אוטומטית: DetectUnit משליך ערכי NaN ואינסוף, ממיין פעם אחת ומודד פערי לוח שנה עם IncMonth במקום סף של 30 ימים, כך שסדרה של 31 בינואר, 28 בפברואר, 31 במרץ מזוהה כחודשית, ו-ResolveUnits מכבד base מפורש תוך הורדת ה-base כשיחידת ה-major עדינה יותר
Normalize מצמיד כל ערך ליום שלו, לראש החודש שלו או ל-1 בינואר לפני שקיימים ticks כלשהם, כי base חודשי מתחת ליחידת major יומית היה מכווץ חודש שלם של נקודות למשבצת אחת

מה האלמנט 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 פירושו זיהוי אוטומטי של category מול תאריך, לא יחידות 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 של הציר יושב אחרי הכותרת ולפני סימוני ה-tick, התכונה sourceLinked שלו ברירת המחדל שלה true, ו-TXLSXChartAxis מחזיק את קוד הפורמט, את ערך ה-source-linked ואת NumberFormatSet בנפרד, כך שקוד ריק לעולם לא מתפרש בטעות כאלמנט נעדר. סיבוב טקסט נשמר ב-1/60000 של מעלה, כשערכים חיוביים משמעותם כיוון השעון; TextStyle.Rotation מקבל מעלות רגילות, פלט ה-SVG משתמש בזווית כמו שהיא, וה-backend המדורג, שבו זוויות חיוביות רצות נגד כיוון השעון, הופך את הסימן בגבול משותף אחד. ציר maxMin הפוך משקף יחד את ה-ticks ה-major וה-minor, קווי הרשת, נקודות הנתונים, קווי המגמה ופסי השגיאה, ולעולם לא רק את התוויות. כשאתה פותח חוברת שנכתבה במקום אחר, הדגלים אומרים לך מה היוצר באמת ציין

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 Delphi Component עבור Delphi ו-C++Builder, שקורא, כותב ומצייר גרפים של XLS ו-XLSX בלי אוטומציה של Excel