Tehnički članak

Os datuma u HotXLS grafikonu: kalendarski točne oznake

HotXLS renderira datum-os Excel grafikona kalendarskom aritmetikom, ne fiksnim brojem dana. Klasa TXLSChartDateAxisTransform u jedinici lxChart svaku koordinatu drži kao serijski broj radne knjige, svaku mjesečnu ili godišnju oznaku računa izravno iz minimuma osi kroz IncMonth i IncYear i steže krajeve mjeseci na pravi kalendar, pa serija sidrena na 31. siječnja dobiva oznake na 28. veljače, 31. ožujka i 30. travnja umjesto da klizi. Isti transform automatski bira jedinice dana, mjeseca ili godine i dijele ga HTML, SVG i paginirani PDF rendereri grafikona

Bug koji ovo sprječava poznat je svakomu tko je ikad grafikonom prikazao mjesečno zatvaranje. Renderer koji mjesec tretira kao 30 dana odmiče se pet dana unaprijed do kraja prve godine. Renderer koji je pametniji i koristi kalendarske mjesece, ali svaku oznaku napreduje od prethodne, pada tiše: 31. siječnja postane 28. veljače, sljedeći korak slijeti na 28. ožujka, i svaka oznaka nakon toga ostaje pribijena na 28. Grafikon izgleda vjerodostojno, oznake su pogrešne, i nitko ne primijeti dok kontrolor ne pita zašto je ožujski saldo nacrtan tri dana prije kraja kvartala

Zašto mjesečne oznake odmicu kad se akumuliraju?

Mjesečne oznake odmicu jer stiskanje baca informaciju. Kad jednom 31. siječnja bude stegnut na 28. veljače, činjenica da je serija htjela 31. nestala je, i svaki korak iz stegnute vrijednosti nasljeđuje gubitak. TXLSChartDateAxisTransform.BuildTicks nikad ne koraka od prethodne oznake. Oznaka i uvijek se računa kao AddUnits(MinValue, UnitKind, Step * i, ...), što je min + jedinica × indeks mjereno od minimuma osi. AddUnits za jedinice dana dodaje broj izravno na serijski broj, a za jedinice mjeseca i godine pretvara serijski broj u pravi datum, zove IncMonth ili IncYear i pretvara natrag. Petlja je i omeđena sa svake strane: polje rezultata ograničeno je na 4096 unosa što god pozivatelj tražio, Step * i provjerava se prema MaxInt prije množenja, i generacija stane čim oznaka ne uspije rasti ili prijeđe maksimum osi

Dva načina na koje datum-os HotXLS grafikona može smjestiti mjesečne oznake nakon sidra 31. siječnja 2026: napredovanje svake oznake od prethodne stegnute vrijednosti ostaje pribijeno na 28., dok BuildTicks računa AddUnits(MinValue, xcduMonths, Step puta i) iz minimuma osi pa oznake slijeću na 28. veljače, 31. ožujka i 30. travnja
Stiskanje baca informaciju, pa TXLSChartDateAxisTransform nikad ne koraka od prethodne oznake i svaku poziciju preračunava iz minimuma osi, stežući krajeve mjeseci na pravi kalendar
uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // 1900 datumski sustav (Dates1904 = False), serija sidrena na kraj mjeseca
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // Korak od jednog mjeseca, cilj 12 oznaka, nikad više od 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;

U BuildTicks postoji jedna poštena aproksimacija. Kad je traženi korak nula, metoda procjenjuje veličinu koraka tretirajući mjesec kao 30 dana i godinu kao 365, a zatim sirovi korak zaokružuje na 1, 2 ili 5 puta dekadska potencija. Ta procjena odlučuje samo koliko jedinica leži između oznaka. Same pozicije oznaka i dalje dolaze iz IncMonth i IncYear, pa aproksimacija može promijeniti gustoću oznaka, ali nikad ne pomiče oznaku s kalendara

Zašto čuvati serijske brojeve radne knjige umjesto konverzije u TDateTime?

Datum-os u HotXLS-u čuva serijske brojeve radne knjige kao svoj koordinatni prostor jer Delphi TDateTime nema mjesta za serijski broj 60, fantomski 29. veljače 1900. koji je 1900 datumski sustav naslijedio kao kuriozitet kompatibilnosti. TrySerialToDateTime serijske brojeve ispod 60 pomjera za jedan dan, a broj 60 savija na 28. veljače, što je u redu za označavanje jedne točke, ali fatalno za geometriju: pretvorite li sve točke prvo u TDateTime, dva dana oko fantomskog dana završe jednu jedinicu bliže nego što ih Excel crta, pa se sve nacrtano prije ožujka 1900. pomjeri u odnosu na sve nakon njega. TXLSChartDateAxisTransform.FormatValue isti slot tretira posebno i označava ga kao 1900-02-29 točno kao i Excel. U 1904 radnoj knjizi serijski broj 0 je 1. siječnja 1904, a transform primjenjuje fiksni pomak od 1462 dana u oba smjera; cijelu priču obje epohe prati naš članak o Excel serijskim brojevima datuma, 1904 sustavu i formatima brojeva

Zašto datum-os u HotXLS-u čuva serijske brojeve radne knjige umjesto konverzije u TDateTime: serijski broj 60 je fantomski 29. veljače 1900, TrySerialToDateTime pomjera brojeve ispod 60 i savija 60 na 28. veljače pa dva dana oko njega završe jednu jedinicu bliže nego što ih Excel crta, dok FormatValue broj 60 označava kao 1900-02-29
Savijanje je u redu za označavanje jedne točke ali fatalno za geometriju, jer se sve nacrtano prije ožujka 1900. pomjeri u odnosu na sve nakon njega; 1904 radna knjiga umjesto toga primjenjuje fiksni pomak od 1462 dana

Kako HotXLS automatski bira dane, mjesece ili godine?

TXLSChartDateAxisTransform.DetectUnit bira najfiniju kalendarsku jedinicu koju praznine u podacima stvarno traže. Metoda najprije baca NaN i beskonačne vrijednosti, ostatak jednom sortira u O(n log n) i preskače duplicirane datume. Ako je bilo koji susjedni razmak kraći od jednog kalendarskog mjeseca, jedinica su dani; inače, ako je bilo koji razmak kraći od jedne kalendarske godine, jedinica su mjeseci; inače su to godine. Test je IncMonth(Previous, 1) > Current, ne prag od 30 dana, pa se serija krajeva mjeseca 31. siječnja, 28. veljače, 31. ožujka ispravno detektira kao mjesečni podaci. S manje od dva različita valjana datuma nema razmaka za mjeriti, i metoda pada na dane

ResolveUnits zatim spaja detektiranu jedinicu s onim što grafikon deklarira. Eksplicitna bazna jedinica poštuje se, a nedostajuća glavna ili sporedna jedinica nasljeđuje detektiranu kad je ona grublja od bazne. Zadnji je korak onaj koji ljudi promaknu: ako je efektivna glavna ili sporedna jedinica finija od bazne, baza se spušta da se poklopi. Bazna jedinica je ono na što se podatkovne točke normaliziraju prije nego ikakve oznake postoje, pri čemu Normalize vrijednost zakači na njen dan, prvi dan njenog mjeseca ili 1. siječnja, pa bi mjesečna baza pod dnevnom glavnom jedinicom sažela mjesec točaka u jedan slot prije nego ih kod oznaka uopće vidio. Gustoća oznaka također je omeđena — duljinom crtanja, izmjerenom širinom fonta osi, formatom broja oznake i projekcijom rotiranih oznaka — a svaki backend crta oznake, crtice i mrežne linije iz jednog dijeljenog, omeđenog polja oznaka, pri čemu se sporedna oznaka koja se poklopi s glavnom crta samo jednom

Kako HotXLS automatski bira jedinice osi datuma: DetectUnit baca NaN i beskonačne vrijednosti, jednom sortira i kalendarske razmake mjeri s IncMonth umjesto praga od 30 dana, pa se serija 31. siječnja, 28. veljače, 31. ožujka detektira kao mjesečna, a ResolveUnits poštuje eksplicitnu bazu uz spuštanje baze kad je glavna jedinica finija
Normalize svaku vrijednost zakače na njen dan, prvi dan njenog mjeseca ili 1. siječnja prije nego ikakve oznake postoje, jer bi mjesečna baza pod dnevnom glavnom jedinicom sažela mjesec točaka u jedan slot

Što element dateAx čuva, i što izostavljanje znači?

U ChartML-u je svaka kalendarska postavka na osi datuma neobavezna, i izostaviti je nije isto što i zapisati njezinu zadanu vrijednost. Tip CT_DateAx u ECMA-376 Part 1, §21.2 (DrawingML Charts) pušta da se baseTimeUnit, majorTimeUnit, minorTimeUnit i auto pojave ili ne, a izostavljeni baseTimeUnit govori Excelu da sam odluči, što nije isto kao eksplicitni days. TXLSXChartAxis zato svaku vrijednost čuva uz zastavicu prisutnosti: BaseTimeUnit s BaseTimeUnitSet, MajorTimeUnit s MajorTimeUnitSet, MinorTimeUnit s MinorTimeUnitSet, i AutoDateAxis s AutoDateAxisSet. Dodjela vrijednosti postavlja zastavicu, čišćenje zastavice vraća element u izostanak, a XLSX pisac ispisuje element samo kad mu je zastavica postavljena. Napominjemo da auto znači automatsku detekciju kategorija-ili-datum, ne automatske jedinice oznaka

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;                  // piše c:dateAx
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // postavlja i BaseTimeUnitSet
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // > 0 označava majorUnit kao postavljen
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // stupnjevi; čuva se kao 1/60000
    // AutoDateAxis netaknut: element c:auto se ne piše

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

Oznake i orijentacija slijede ista pravila svjesna prisutnosti. Osovinski numFmt sjedi iza naslova i pred crtama oznaka, njegov atribut sourceLinked zadan je na true, a TXLSXChartAxis format kod, source-linked vrijednost i NumberFormatSet čuva odvojeno, pa prazan kod nikad se ne zamijeni za odsutan element. Rotacija teksta čuva se u 1/60000 stupnja, uz pozitivne vrijednosti u smjeru kazaljke; TextStyle.Rotation prima obične stupnjeve, SVG izlaz koristi kut kakav jest, a paginirani backend, kojemu pozitivni kutovi idu obrnuto od kazaljke, okreće predznak na jednoj zajedničkoj granici. Obrnuti maxMin os zrcali glavne i sporedne oznake, mrežne linije, podatkovne točke, trend linije i crte grešaka zajedno, i nikad samo oznake. Kad otvorite radnu knjigu pisanu negdje drugdje, zastavice Vam govore što je autor stvarno specificirao

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;   // spremite li ponovno, izostavljeni elementi ostaju izostavljeni
  end;
end;

Gdje se kalendarski model staje?

Kalendarski model staje kod formata koji ne mogu izraziti kalendarske jedinice, i HotXLS se ne pretvara da je drukčije. ODF 1.2 nudi chart:interval-major, običan broj bez pojma dana, mjeseci ili godina, pa Excel mjesečna os spremljena kao ODS ne može nositi svoju jedinicu, a HotXLS ne izmišlja nestandardni atribut da je imitira. Na legacy BIFF8 strani zapis AxcExt drži devet fiksiranih 16-bitnih riječi, a njegove automatske zastavice samo maskiraju polja umjesto da ih uklone; HotXLS maskirane vrijednosti minimuma, maksimuma, intervala, jedinice i sječišta čuva pri čitanju i povratnom pisanju, nepoznate kodove jedinica ostavlja netaknutima u Classic modelu, i na XLSX preslikava samo valjane datumske jedinice 0, 1 i 2 (dani, mjeseci, godine)

Datum-os jedan je sloj modela grafikona koji također mora preživjeti datoteke koje nije sam stvorio. Kad Excel radna knjiga spoji datumsku linijsku seriju sa stupčastom serijom na sekundarnoj osi, naš članak o sačuvanom ChartML-u i kombiniranim grafikonima pokazuje kako HotXLS originalni chart XML reproducira bajt po bajt kad se ništa nije promijenilo, a tipizirane izmjene u njega spaja kad se nešto ipak promijenilo, a naš pregled grafikona, slika i crteža pokriva API-e za anchoring i serije korištene gore. Sve to isporučuje HotXLS Delphi Component za Delphi i C++Builder, koji čita, piše i renderira XLS i XLSX grafikone bez Excel automatizacije