Műszaki cikk

HotXLS diagram dátumtengely: naptárhű beosztások Delphiben

A HotXLS naptári számtannal, nem rögzített napszámmal rajzolja meg az Excel-diagramok dátumtengelyeit. A lxChart egység TXLSChartDateAxisTransform osztálya minden koordinátát munkafüzet-dátumszámként tart, minden hónap- vagy évtick-et közvetlenül a tengelyminimumból számol IncMonth és IncYear segítségével, a hónapvégeket pedig a valódi naptárra igazítja, így a január 31-ére horgonyzott sorozat február 28-ai, március 31-ei és április 30-ai tick-eket kap, a csúszás helyett. Ugyanez a transzformáció nap-, hónap- vagy év egységet választ automatikusan, és közös a HTML, az SVG és a lapozható PDF diagramrajzolók számára

A hiba, amelyet ez megelőz, mindenki számára ismerős, aki már diagramozott hónapvégi zárást. A hónapot 30 napként kezelő rajzoló az első év végére öt nappal korábbra csúszik. Az okosabb, naptári hónapokat használó, de minden tick-et az előzőből léptető rajzoló csendesebben hibázik: a január 31-ből február 28 lesz, a következő lépés március 28-ra ér, és minden utána következő tick a 28-án ragad. A diagram hihetőnek tűnik, a címkék hibásak, és addig nem veszi észre senki, míg egy kontroller meg nem kérdezi, miért van a márciusi egyenleg a negyedév vége előtt három nappal felrajzolva

Miért csúsznak a hónaptick-ek, amikor összegezik őket?

A hónaptick-ek azért csúsznak, mert a levágás információt dob el. Amint a január 31-ből február 28 lett, elveszett az a tény, hogy a sorozat a 31-ét akarta, és a levágott értékből tett minden lépés örökli a veszteséget. A TXLSChartDateAxisTransform.BuildTicks soha nem lép egy előző tick-ből. Az i-edik tick mindig AddUnits(MinValue, UnitKind, Step * i, ...) alakban számolódik, ami a tengelyminimumtól mért min + egység × index. Az AddUnits a nap egységeknél a számot egyenesen a dátumszámhoz adja, hónap és év egységeknél a dátumszámot valódi dátummá alakítja, meghívja az IncMonth-ot vagy az IncYear-t, és visszakonvertál. A ciklus minden irányban korlátos: az eredménytömb a hívó kérésétől függetlenül 4096 bejegyzésnél tetőzik, a Step * i kifejezést szorzás előtt MaxInt ellenére ellenőrzik, a generálás pedig abban a pillanatban megáll, amikor egy tick nem növekszik, vagy átlépi a tengelymaximumot

Két mód, ahogy egy HotXLS diagram dátumtengelye elhelyezheti a hónaptick-eket egy 2026. január 31-i horgony után: ha minden tick-et az előző, levágott értékből léptetünk, a 28-án ragadunk, a BuildTicks viszont a tengelyminimumból számol AddUnits(MinValue, xcduMonths, Step times i) alakban, így a tick-ek február 28-ra, március 31-re és április 30-ra érnek
A levágás információt dob el, ezért a TXLSChartDateAxisTransform soha nem lép egy előző tick-ből, hanem minden pozíciót a tengelyminimumból számol újra, a hónapvégeket a valódi naptárra igazítva
uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // 1900-as dátumrendszer (Dates1904 = False), hónapvégre horgonyzott sorozat
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // Egyhónapos lépés, 12 tick a cél, soha nem több 64-nél
  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;

A BuildTicks-ban egyetlen becsületes közelítés van. Ha a kért lépés nulla, a metódus úgy becsül lépésméretet, hogy egy hónapot 30 napnak, egy évet 365-nek vesz, majd a nyers lépést tízhatvány 1, 2 vagy 5 szeresére kerekíti. Ez a becslés csak azt dönti el, hány egység esik két tick közé. Maguk a tick-pozíciók továbbra is az IncMonth-ból és az IncYear-ből jönnek, tehát a közelítés megváltoztathatja a tick-sűrűséget, de egy tick-et soha nem mozdít el a naptárból

Miért marad a tengely munkafüzet-dátumszámnál a TDateTime-ra konvertálás helyett?

A HotXLS dátumtengelye azért tartja koordinátatérként a munkafüzet-dátumszámokat, mert a Delphi TDateTime típusában nincs hely a 60-as sorszámnak, a kísértet február 29, 1900-nak, amelyet a 1900-as dátumrendszer kompatibilitási furcsaságként örökölt. A TrySerialToDateTime a 60 alatti sorszámokat egy nappal eltolja, a 60-ast pedig február 28-ra hajtja össze, ami egyetlen pont címkézéséhez jó, geometriához végzetes: ha előbb minden pontot TDateTime-ra konvertálunk, a kísértetnap két oldalán lévő napok egy egységgel közelebb kerülnek egymáshoz, mint ahogy az Excel rajzolja őket, így minden, 1900 márciusa előtt felrajzolt dolog elcsúszik mindahhoz képest, ami utána jön. A TXLSChartDateAxisTransform.FormatValue ugyanezt a helyet kezeli kivételként, és pontosan úgy címkézi 1900-02-29-ként, ahogy az Excel. Egy 1904-es munkafüzetben a 0. sorszám az 1904. január 1., a transzformáció pedig mindkét irányban alkalmazza a rögzített 1462 napos eltolást; mindkét korszak teljes története a Excel dátumsorszámokról, az 1904-es rendszerről és a számformátumokról szóló cikkünkben olvasható

Miért tart a HotXLS dátumtengelye munkafüzet-dátumszámoknál a TDateTime-ra konvertálás helyett: a 60-as sorszám a kísértet február 29, 1900, a TrySerialToDateTime a 60 alatti sorszámokat eltolja, a 60-ast pedig február 28-ra hajtja, így a két oldalán lévő napok egy egységgel közelebb kerülnek, mint ahogy az Excel rajzolja őket, a FormatValue viszont a 60-as sorszámot 1900-02-29-ként címkézi
Az összehajtás egyetlen pont címkézéséhez jó, geometriához végzetes, mert minden, 1900 márciusa előtt felrajzolt dolog elcsúszik mindahhoz képest, ami utána jön; egy 1904-es munkafüzet ehelyett a rögzített 1462 napos eltolást alkalmazza

Hogyan választ a HotXLS automatikusan nap, hónap vagy év közül?

A TXLSChartDateAxisTransform.DetectUnit a legfinomabb naptári egységet választja, amelyre az adathézagok ténylegesen szükségesek. A metódus előbb eldobja a NaN és a végtelen értékeket, a többit egyszer rendezi O(n log n) alatt, és átugorja a duplikált dátumokat. Ha bármely szomszédos rés rövidebb egy naptári hónapnál, az egység a nap; ellenkező esetben, ha bármely rés rövidebb egy naptári évnél, az egység a hónap; különben az év. A teszt az IncMonth(Previous, 1) > Current, nem egy 30 napos küszöb, ezért a január 31, február 28, március 31 sorozat helyesen hónapos adatként detektálódik. Kétnél kevesebb különböző érvényes dátum esetén nincs mérhető rés, és a metódus a napra esik vissza

A ResolveUnits ezután összeolvasztja a detektált egységet azzal, amit a diagram deklarál. Az explicit bázisegységet tiszteletben tartja, a hiányzó major vagy minor egység pedig akkor örökli a detektált egységet, ha az durvább a bázisnál. Az utolsó lépés az, amelyet a legtöbben kihagynak: ha a tényleges major vagy minor egység finomabb a bázisegységnél, a bázis leereszkedik hozzá. A bázisegység az, amelyhez az adatpontok normalizálódnak, még mielőtt egyetlen tick is létezne, a Normalize az értéket a napjára, a hónapja elsejére vagy január 1-jére pattintja, tehát egy havi bázis egy napi major egység alatt egy hónapnyi pontot zsúfolna egyetlen helyre, mielőtt a tick-kód egyáltalán meglátná őket. A tick-sűrűség is korlátos, a rajzterület hossza, a tengelybetűtípus mért szélessége, a címkék számformátuma és a forgatott címkék vetülete szab határt, és minden backend egyetlen, közös, korlátos tick-tömbből rajzol címkéket, tick-jeleket és rácsvonalakat, a major tick-kel egybeeső minor tick-et pedig csak egyszer

Hogyan választ a HotXLS automatikusan dátumtengely-egységeket: a DetectUnit eldobja a NaN és a végtelen értékeket, egyszer rendez, és a naptári réseket IncMonth-mal méri 30 napos küszöb helyett, így a január 31, február 28, március 31 sorozat hónaposként detektálódik, a ResolveUnits pedig tiszteletben tartja az explicit bázist, és leereszti azt, ha a major egység finomabb
A Normalize minden értéket a napjára, a hónapja elsejére vagy január 1-jére pattint, még mielőtt egyetlen tick létezne, mert egy havi bázis egy napi major egység alatt egy hónapnyi pontot zsúfolna egyetlen helyre

Mit tárol a dateAx elem, és mit jelent az elhagyása?

A ChartML-ben minden naptári beállítás opcionális egy dátumtengelyen, és az elhagyása nem egyenlő az alapérték kiírásával. Az ECMA-376 Part 1, §21.2 (DrawingML Charts) CT_DateAx típusában a baseTimeUnit, majorTimeUnit, minorTimeUnit és auto egyaránt megjelenhet vagy hiányozhat, és egy elhagyott baseTimeUnit azt üzeni az Excelnek, hogy döntsön maga — ez más, mint az explicit days. A TXLSXChartAxis ezért minden értéket egy jelenlét-jelző mellett tárol: BaseTimeUnit a BaseTimeUnitSet-tel, MajorTimeUnit a MajorTimeUnitSet-tel, MinorTimeUnit a MinorTimeUnitSet-tel, valamint az AutoDateAxis az AutoDateAxisSet-tel. Értékadás beállítja a jelzőt, a jelző törlése visszaadja az elemet az elhagyottnak, és az XLSX író csak akkor bocsát ki elemet, ha a jelzője áll. Figyeljen rá, hogy az auto automatikus kategória-vagy-dátum felismerést jelent, nem automatikus tick egységeket

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-t ír
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // a BaseTimeUnitSet-et is beállítja
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // > 0 a majorUnit beállítottnak jelzése
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // fokban; tárolva 1/60000-ként
    // Az AutoDateAxis érintetlen: nem íródik c:auto elem

    Book.SaveAs('month-end-balance.xlsx');
  finally
    Book.Free;
  end;
end;

A címkék és a tájolás ugyanazokat a jelenlét-tudatos szabályokat követik. A tengely numFmt-je a cím után és a tick jelek előtt ül, a sourceLinked attribútuma alapból true, a TXLSXChartAxis pedig külön tartja a formátumkódot, a source-linked értéket és a NumberFormatSet-et, így egy üres kódot soha nem tévesztenek össze hiányzó elemmel. A szövegforgatás a fok 1/60000-edében tárolódik, a pozitív érték az óramutató járásával egyező irányt jelent; a TextStyle.Rotation sima fokot vesz, az SVG kimenet a szöget úgy használja, ahogy van, a lapozott backend, amelynek pozitív szögei az óramutató járásával ellentétesek, egyetlen közös határon váltja az előjelet. Egy fordított maxMin tengely egyszerre tükrözi a major és minor tick-eket, a rácsvonalakat, az adatpontokat, a trendvonalakat és a hibasávokat, soha nem csak a címkéket. Amikor máshol írt munkafüzetet nyit meg, a jelzők megmondják, mit adott meg ténylegesen a szerző

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;   // mentse el újra, és az elhagyott elemek elhagyottak maradnak
  end;
end;

Hol ér véget a naptári modell?

A naptári modell olyan formátumoknál ér véget, amelyek nem tudnak naptári egységeket kifejezni, és a HotXLS nem színleli, hogy másképp lenne. Az ODF 1.2 kínál chart:interval-major-t, amely sima szám, napokról, hónapokról, évekről nincs fogalma, tehát egy ODS-ként mentett Excel havi tengely nem hordozhatja az egységét, és a HotXLS nem talál ki nem szabványos attribútumot, hogy színleljen egyet. A legacy BIFF8 oldalon az AxcExt rekord kilenc rögzített 16 bites szót tart, és az automatikus jelzői csupán maszkolják a mezőket, nem távolítják el őket; a HotXLS a maszkolt minimumot, maximumot, intervallumot, egységet és metszéspontot megőrzi olvasáskor és visszaíráskor, az ismeretlen egységkódokat érintetlenül tartja a Classic modellben, és csak az érvényes dátumegységeket, a 0, 1 és 2 értékeket (nap, hónap, év) képezi le XLSX-re

A dátumtengelyek annak a diagrammodellnek az egyik rétege, amelynek az általa nem készített fájlokat is túl kellett élnie. Amikor egy Excel munkafüzet egy dátumtengelyes vonalat kombinál egy oszlopsorozattal másodlagos tengelyen, a megőrzött ChartML-ről és a kombinációs diagramokról szóló cikkünk megmutatja, hogyan játssza vissza a HotXLS az eredeti chart XML-t bájtról bájtra, ha semmi nem változott, és hogyan fésüli bele a típusos szerkesztéseket, ha valami mégis, a diagramokról, képekről és rajzokról szóló áttekintésünk pedig fedeli a fent használt horgony- és sorozat-API-kat. Mindez a HotXLS Delphi Component csomagban érkezik Delphihez és C++Builderhez, amely XLS és XLSX diagramokat olvas, ír és rajzol Excel automatizálás nélkül