Tekninen artikkeli

Päivämääräakselin kalenteritarkat tickit HotXLS:ssä

HotXLS ei laske Excel-chartien päivämääräakseleita kiintein askelin vaan oikealla kalenteriaritmetiikalla. lxChart-yksikön TXLSChartDateAxisTransform-luokka pitää jokaisen koordinaatin työkirjan sarjanumerona, laskee jokaisen kuukausi- tai vuositickin suoraan akselin minimistä IncMonth- ja IncYear-kutsujen kautta ja katkaisee kuukauden loput todelliseen kalenteriin, joten 31. tammikuuta ankkuroitunut sarja saa tickit 28. helmikuuta, 31. maaliskuuta ja 30. huhtikuuta liukumisen sijaan. Sama transformi valitsee päivän, kuukauden tai vuoden yksiköt automaattisesti ja on yhteinen HTML-, SVG- ja sivutetun PDF:n chart-renderöijille

Virhe jonka tämä estää on tuttu jokaiselle joka on kuvannut kuukauden lopun sulkua. Renderöija joka käsittelee kuukautta 30 päivänä ajautuu viisi päivää liian aikaisin ensimmäisen vuoden loppuun mennessä. Renderöija joka on fiksumpi ja käyttää kalenterikuukausia mutta etenee jokaisesta tickistä edelliseen, epäonnistuu hiljaisemmin: 31. tammikuuta muuttuu 28. helmikuuta, seuraava askel osuu 28. maaliskuuta ja jokainen sitä seuraava tick jää kiinni 28:een. Chart näyttää uskottavalta, otsikot ovat väärin, eikä kukaan huomaa ennen kuin talouspäällikkö kysyy miksi maaliskun saldo on piirretty kolme päivää ennen neljännesvuoden loppua

Miksi kuukausitickit ajautuvat kun niitä kertyy?

Kuukausitickit ajautuvat, koska katkaisu heittää tietoa pois. Kun 31. tammikuuta on katkaistu arvoon 28. helmikuuta, tosiasia siitä että sarja halusi 31:nnen on poissa, ja mikä tahansa katkaistusta arvosta otettu askel perii menetyksen. TXLSChartDateAxisTransform.BuildTicks ei koskaan astu edellisestä tickistä. Tick i lasketaan aina muodossa AddUnits(MinValue, UnitKind, Step * i, ...), eli min + yksikkö × indeksi mitattuna akselin minimistä. AddUnits lisää määrän suoraan sarjanumeroon päiväyksiköille ja kuukausi- ja vuosiyksiköillä konvertoi sarjanumeron oikeaksi päivämääräksi, kutsuu IncMonthia tai IncYearia ja konvertoi takaisin. Silmukka on myös rajattu joka suunnasta: tulosaulokko on katkaistu 4096 merkintään riippumatta siitä mitä kutsuja pyytää, Step * i tarkistetaan MaxIntia vasten ennen kertolaskua, ja generointi pysähtyy heti kun tick ei enää kasva tai ohittaa akselin maksimin

Kaksi tapaa joilla HotXLS:n chart-päivämääräakseli voi sijoittaa kuukausitickit 31.1.2026 ankkurin jälkeen: jokaisen tickin eteneminen edellisestä katkaistusta arvosta jää kiinni 28:een, kun taas BuildTicks laskee AddUnits(MinValue, xcduMonths, Step times i) -kutsun akselin minimistä joten tickit osuvat 28. helmikuuta, 31. maaliskuuta ja 30. huhtikuuta
Katkaisu heittää tietoa pois, joten TXLSChartDateAxisTransform ei koskaan astu edellisestä tickistä vaan laskee jokaisen position uudelleen akselin minimistä katkaisten kuukauden loput todelliseen kalenteriin
uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // 1900-päivämääräjärjestelmä (Dates1904 = False), kuukauden loppuun ankkuroitu sarja
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // Yhden kuukauden askel, tavoitteena 12 tickiä, enintään 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;

BuildTicksissa on yksi rehellinen likiarvo. Kun pyydetty askel on nolla, metodi arvioi askelkoon käsittelemällä kuukautta 30 päivänä ja vuotta 365 päivänä ja pyöristää sitten raaka-askeleen kymmenen potenssin 1-, 2- tai 5-kertomaksi. Se arvio päättää vain kuinka monta yksikköä tickien välissä on. Tick-positiot itsessään tulevat yhä IncMonthista ja IncYearista, joten likiarvo voi muuttaa tickitiheyttä mutta ei koskaan siirrä tickiä kalenterista

Miksi pitää kiinni työkirjan sarjanumeroista TDateTime-konversion sijaan?

HotXLS:n päivämääräakseli pitää työkirjan sarjanumerot koordinaattiavaruutenaan, koska Delphin TDateTimeissa ei ole paikkaa sarjanumerolle 60, aaveelle 29. helmikuuta 1900 jonka 1900-päivämääräjärjestelmä peri yhteensopivuuskumipanena. TrySerialToDateTime siirtää 60:aa pienemmät sarjanumerot päivällä ja taittaa sarjanumeron 60 arvoon 28. helmikuuta, mikä sopii yksittäisen pisteen otsikoimiseen mutta on kohtalokasta geometrialle: konvertoi ensin jokainen piste TDateTimeiksi ja aavepäivän kummankin puolen päivät päätyvät yhden yksikön lähemmäksi toisiaan kuin Excel ne piirtää, joten kaikki ennen maaliskuuta 1900 piirretty siirtyy suhteessa kaikkeen sen jälkeen piirrettyyn. TXLSChartDateAxisTransform.FormatValue käsittelee saman paikan erikoistapauksena ja otsikoi sen 1900-02-29 täsmälleen kuten Excel. 1904-työkirjassa sarjanumero 0 on 1. tammikuuta 1904, ja transformi soveltaa kiinteää 1462 päivän siirtymää molempiin suuntiin; koko tarina molemmista aikakausista on artikkelissamme Excelin päivämääräsarjanumeroista, 1904-järjestelmästä ja lukumuodoista

Miksi HotXLS:n päivämääräakseli pitää kiinni työkirjan sarjanumeroista TDateTime-konversion sijaan: sarjanumero 60 on aave 29. helmikuuta 1900, TrySerialToDateTime siirtää 60:aa pienemmät sarjanumerot päivällä ja taittaa 60:n arvoon 28. helmikuuta joten aavepäivän kummankin puolen päivät päätyvät yhden yksikön lähemmäksi kuin Excel ne piirtää, kun taas FormatValue otsikoi sarjanumeron 60 arvoksi 1900-02-29
Taitos sopii yksittäisen pisteen otsikoimiseen mutta on kohtalokasta geometrialle, koska kaikki ennen maaliskuuta 1900 piirretty siirtyy suhteessa kaikkeen sen jälkeen piirrettyyn; 1904-työkirja soveltaakin kiinteää 1462 päivän siirtymää

Miten HotXLS valitsee päivät, kuukaudet tai vuodet automaattisesti?

TXLSChartDateAxisTransform.DetectUnit valitsee hienoimman kalenteriyksikön jonka daton aukot oikeasti tarvitsevat. Metodi pudottaa ensin NaN- ja äärettömät arvot, lajittelee loput kerran ajassa O(n log n) ja ohittaa tuplapäivämäärät. Jos jokin viereisten välinen aukko on lyhyempi kuin yksi kalenterikuukausi, yksikkö on päivät; muuten, jos jokin aukko on lyhyempi kuin yksi kalenterivuosi, yksikkö on kuukaudet; muuten se on vuodet. Testi on IncMonth(Previous, 1) > Current, ei 30 päivän kynnys, joten kuukauden loppua kulkeva sarja 31. tammikuuta, 28. helmikuuta, 31. maaliskuuta tunnistetaan oikein kuukausidatoksi. Vähemmällä kuin kahdella erillisellä kelvollisella päivämäärällä ei ole mitattavaa aukkoa, ja metodi palaa päiviin

ResolveUnits yhdistää sitten tunnistetun yksikön siihen mitä chart julistaa. Eksplisiittinen perusyksikkö kunnioitetaan, ja puuttuva major- tai minor-yksikkö perii tunnistetun yksikön kun se on karkeampi kuin perusyksikkö. Viimeinen askel on se jonka ihmiset ohittavat: jos efektivinen major- tai minor-yksikkö on hienompi kuin perusyksikkö, perusyksikkö lasketaan alas sen tasoon. Perusyksikkö on se mihin datapisteet normalisoidaan ennen yhtään tickiä, Normalize napsauttaa arvon sen päivälle, kuukautensa ensimmäiselle tai 1. tammikuuta, joten kuukausiperusyksikkö päivittäisen major-yksikön alla luhistaisi kuukauden verran pisteitä yhteen paikkaan ennen kuin tick-koodi näki niitä koskaan. Tick-tiheys on myös rajattu, kuvaajan pituudella, akselifontin mitatulla leveydellä, otsikoiden lukumuodolla ja käännettyjen otsikoiden projektion, ja jokainen backend piirtää otsikot, tick-merkit ja ruudukot yhdestä jaetusta, rajatusta tick-taulukosta, ja minor-tick joka osuu yhteen major-tickin kanssa piirretään vain kerran

Miten HotXLS valitsee päivämääräakselin yksiköt automaattisesti: DetectUnit pudottaa NaN- ja äärettömät arvot, lajittelee kerran ja mittaa kalenteriaukot IncMonth-kutsulla 30 päivän kynnyksen sijaan, joten sarja 31. tammikuuta, 28. helmikuuta, 31. maaliskuuta tunnistetaan kuukausittaiseksi, ja ResolveUnits kunnioittaa eksplisiittistä perusyksikköä laskien sen alas kun major-yksikkö on hienompi
Normalize napsauttaa jokaisen arvon sen päivälle, kuukautensa ensimmäiselle tai 1. tammikuuta ennen yhtään tickiä, koska kuukausiperusyksikkö päivittäisen major-yksikön alla luhistaisi kuukauden verran pisteitä yhteen paikkaan

Mitä dateAx-elementti tallentaa ja mitä puuttuminen merkitsee?

ChartML:ssä jokainen päivämääräakselin kalenteriasetus on valinnainen, ja yhden jättäminen pois ei ole sama kuin oletusarvon kirjoittaminen. CT_DateAx-tyyppi ECMA-376 Part 1:n §21.2 (DrawingML Charts) sallii baseTimeUnitin, majorTimeUnitin, minorTimeUnitin ja auton kunkin esiintyä tai ei, ja poisjätetty baseTimeUnit käskee Exceliä päättämään itse, mikä on eri asia kuin eksplisiittinen days. TXLSXChartAxis tallentaa siksi jokaisen arvon läsnäololipun viereen: BaseTimeUnit yhdessä BaseTimeUnitSetin kanssa, MajorTimeUnit yhdessä MajorTimeUnitSetin kanssa, MinorTimeUnit yhdessä MinorTimeUnitSetin kanssa sekä AutoDateAxis yhdessä AutoDateAxisSetin kanssa. Arvon sijoittaminen asettaa sen lipun, lipun tyhjentäminen palauttaa elementin poisjätetyksi ja XLSX-kirjoittaja emittoi elementin vain kun sen lippu on asetettu. Huomaa että auto tarkoittaa automaattista kategoria-versus-päivämäärä-tunnistusta, ei automaattisia tick-yksiköitä

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;                  // kirjoittaa c:dateAx-elementin
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // asettaa samalla BaseTimeUnitSetin
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // > 0 merkitsee majorUnitin asetetuksi
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // astetta; tallennetaan 1/60000-osina
    // AutoDateAxis jätetään koskematta: c:auto-elementtiä ei kirjoiteta

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

Otsikot ja suuntaus noudattavat samoja läsnäolotietoisia sääntöjä. Akselin numFmt sijaitsee otsikon jälkeen ja ennen tick-merkkejä, sen sourceLinked-attribuutin oletus on tosi, ja TXLSXChartAxis pitää muotokoodin, lähdekytketyn arvon ja NumberFormatSetin erillään, joten tyhjää koodia ei koskaan luulla puuttuvaksi elementiksi. Tekstin kierto tallennetaan asteen 1/60000-osina positiivisten arvojen tarkoittaessa myötäpäivään; TextStyle.Rotation ottaa tavalliset asteet, SVG-tuotos käyttää kulmaa sellaisenaan, ja sivutettu backend jonka positiiviset kulmat kulkevat vastapäivään kääntää etumerkin yhdessä jaetussa rajapinnassa. Käänteinen maxMin-akseli peilaa major- ja minor-tickit, ruudukot, datapisteet, trendiviivat ja virhepalkit yhdessä, ei koskaan pelkkiä otsikoita. Kun avaat muualla kirjoitetun työkirjan, liput kertovat mitä tekijä oikeasti määritteli

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;   // tallenna uudelleen ja poisjätetyt elementit pysyvät poissa
  end;
end;

Missä kalenterimalli pysähtyy?

Kalenterimalli pysähtyy muotoihin jotka eivät osaa ilmaista kalenteriyksiköitä, eikä HotXLS esitä muuta. ODF 1.2 tarjoaa chart:interval-majorin joka on pelkkä luku ilman käsitystä päivistä, kuukausista tai vuosista, joten ODS:ksi tallennettu Excelin kuukausiakseli ei voi kantaa yksikköään eikä HotXLS keksi epästandardia attribuuttia väärentääkseen sellaista. Vanhalla BIFF8-puolella AxcExt-tietue pitää sisällään yhdeksän kiinteää 16-bittistä sanaa, ja sen automaattiset liput vain maskoivat kenttiä sen sijaan että poistaisivat ne; HotXLS säilyttää maskatut minimin, maksimin, intervallin, yksikön ja leikkausarvot lukemisessa ja takaisinkirjoituksessa, pitää tuntemattomat yksikkökoodit ehjinä Classic-mallissa ja kuvaa vain kelvolliset päivämääräyksiköt 0, 1 ja 2 (päivät, kuukaudet, vuodet) XLSX:ksi

Päivämääräakselit ovat yksi kerros chart-mallissa jonka täytyy myös selvitä tiedostoista joita se ei itse luonut. Kun Excel-työkirja yhdistää päivämääräakselin viivan sarakesarjaan toissijaisella akselilla, artikkelimme säilytetystä ChartML:stä ja yhdistelmächarteista näyttää miten HotXLS toistaa alkuperäisen chart-XML:n tavu tavulta kun mikään ei muuttunut ja yhdistää tyypitetyt muokkaukset siihen kun jotain muuttui, ja yleiskatsauksemme charteista, kuvista ja piirroksista kattaa yllä käytetyt ankkurointi- ja sarja-API:t. Kaikki tämä toimitetaan HotXLS Delphi Component -komponentissa Delphille ja C++Builderille, joka lukee, kirjoittaa ja renderöi XLS- ja XLSX-chartit ilman Excel-automaatiota