Teknisk artikel

HotXLS datumaxel i diagram: kalenderkorrekta ticks i Delphi

HotXLS ritar Excel-diagrammens datumaxlar med kalenderaritmetik, inte fasta dagantal. Klassen TXLSChartDateAxisTransform i enheten lxChart håller varje koordinat som ett arbetsboksserienummer, räknar varje månads- eller årstick direkt från axelns minimivärde genom IncMonth och IncYear, och klämmer månadsslut till den riktiga kalendern, så en serie förankrad på 31 januari får ticks på 28 februari, 31 mars och 30 april i stället för att glida. Samma transform väljer dag-, månads- eller årsenheter automatiskt och delas av diagramrenderarna för HTML, SVG och sidindelad PDF

Buggen detta förhindrar är bekant för alla som plottat månadsbokslut i diagram. En renderare som behandlar en månad som 30 dagar driver fem dagar för tidigt vid slutet av första året. En renderare som är smartare och använder kalendermånader men stegar varje tick från det föregående misslyckas tystare: 31 januari blir 28 februari, nästa steg landar på 28 mars, och varje tick därefter förblir fastnålad på 28:e. Diagrammet ser trovärdigt ut, etiketterna är fel, och ingen märker det förrän en controller frågar varför saldot för mars plottas tre dagar före kvartalets slut

Varför driver månadsticks när de ackumuleras?

Månadsticks driver för att klämning kastar bort information. När 31 januari väl har klämts till 28 februari är det faktum att serien ville ha den 31:e borta, och varje steg som tas från det klämda värdet ärver förlusten. TXLSChartDateAxisTransform.BuildTicks stegar aldrig från ett tidigare tick. Tick i räknas alltid som AddUnits(MinValue, UnitKind, Step * i, ...), vilket är min + enhet × index mätt från axelns minimum. AddUnits lägger talet direkt på serienumret för dagenheter och konverterar för månads- och årsenheter serienumret till ett riktigt datum, anropar IncMonth eller IncYear och konverterar tillbaka. Loopen är också begränsad åt alla håll: resultatarrayen takas vid 4096 poster oavsett vad anroparen ber om, Step * i kontrolleras mot MaxInt innan den multipliceras, och genereringen stannar så snart ett tick inte ökar eller passerar axelns maximum

Två sätt för en HotXLS-diagramdatumaxel att placera månadsticks efter en förankring på 31 januari 2026: att stega varje tick från det föregående klämda värdet förblir fastnålat på 28:e, medan BuildTicks räknar AddUnits(MinValue, xcduMonths, Step gånger i) från axelns minimum så att ticks landar på 28 februari, 31 mars och 30 april
Klämning kastar bort information, så TXLSChartDateAxisTransform stegar aldrig från ett tidigare tick och räknar om varje position från axelns minimum, med månadsslut klämda till den riktiga kalendern
uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // 1900-datersystem (Dates1904 = False), serie förankrad på månadsslut
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // Steg om en månad, sikta på 12 ticks, aldrig fler ä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;

Det finns en ärlig approximation i BuildTicks. När det begärda steget är noll uppskattar metoden en stegstorlek genom att behandla en månad som 30 dagar och ett år som 365, och avrundar sedan råsteget till en 1-, 2- eller 5-multiple av en tiopotens. Uppskattningen bestämmer bara hur många enheter som ligger mellan ticks. Tickpositionerna själva kommer fortfarande från IncMonth och IncYear, så approximationen kan ändra ticktätheten men flyttar aldrig ett tick från kalendern

Varför behålla arbetsboksserienummer i stället för att konvertera till TDateTime?

En datumaxel i HotXLS behåller arbetsboksserienummer som sitt koordinatrum för att Delphis TDateTime inte har någon plats för serie 60, det spöklika 29 februari 1900 som 1900-datersystemet ärvde som en kompatibilitetskuriositet. TrySerialToDateTime skiftar serier under 60 en dag och vecker serie 60 på 28 februari, vilket är fint för att etikettera en enskild punkt men ödesdigert för geometrin: konvertera varje punkt till TDateTime först och de två dagarna på var sida om spökdagen hamnar en enhet närmare varandra än Excel ritar dem, så allt plottat före mars 1900 skiftar relativt allt efter. TXLSChartDateAxisTransform.FormatValue specialbehandlar samma plats och etiketterar den 1900-02-29 precis som Excel. I en 1904-arbetsbok är serie 0 den 1 januari 1904, och transformen tillämpar den fasta offseten på 1462 dagar i båda riktningarna; hela historien om båda epokerna finns i vår artikel om Excel-datumserier, 1904-systemet och talformat

Varför en HotXLS-datumaxel behåller arbetsboksserienummer i stället för att konvertera till TDateTime: serie 60 är det spöklika 29 februari 1900, TrySerialToDateTime skiftar serier under 60 och vecker 60 på 28 februari så att dagarna på var sida hamnar en enhet närmare än Excel ritar dem, medan FormatValue etiketterar serie 60 som 1900-02-29
Vekningen är fint för att etikettera en enskild punkt men ödesdigert för geometrin, för allt plottat före mars 1900 skiftar mot allt efter; en 1904-arbetsbok tillämpar i stället den fasta offseten på 1462 dagar

Hur väljer HotXLS dagar, månader eller år automatiskt?

TXLSChartDateAxisTransform.DetectUnit väljer den finaste kalenderenhet som luckorna i data faktiskt kräver. Metoden kastar NaN- och oändliga värden först, sorterar resten en gång i O(n log n) och hoppar över dublettdatum. Om något intilliggande gap är kortare än en kalendermånad är enheten dagar; annars, om något gap är kortare än ett kalenderår, är enheten månader; annars år. Testet är IncMonth(Previous, 1) > Current, inte en tröskel på 30 dagar, så en månadsslutsserie som löper 31 januari, 28 februari, 31 mars detekteras korrekt som månadsdata. Med färre än två distinkta giltiga datum finns inget gap att mäta, och metoden faller tillbaka på dagar

ResolveUnits slår sedan ihop den detekterade enheten med vad diagrammet deklarerar. En explicit basenhet respekteras, och en saknad huvud- eller underordnad enhet ärver den detekterade enheten när den är grövre än basen. Sista steget är det folk missar: om den effektiva huvud- eller underordnade enheten är finare än basenheten sänks basen för att matcha. Basenheten är det datapunkter normaliseras till innan några ticks finns, med Normalize som snäpper ett värde till sin dag, sin månads första dag eller 1 januari, så en månadsbas under en daglig huvudenhet skulle kollapsa en månads punkter till en enda plats innan tickkoden ens såg dem. Ticktätheten är också begränsad, av plottlängden, den uppmätta bredden av axeltypsnittet, etiketternas talformat och projektionen av roterade etiketter, och varje backend ritar etiketter, tickmarkeringar och rutnät från en gemensam, taktad tickarray, med ett underordnat tick som sammanfaller med ett huvudtick ritat bara en gång

Hur HotXLS väljer datumaxelenheter automatiskt: DetectUnit kastar NaN- och oändliga värden, sorterar en gång och mäter kalenderluckor med IncMonth i stället för en tröskel på 30 dagar, så en serie 31 januari, 28 februari, 31 mars detekteras som månadsvis, och ResolveUnits respekterar en explicit bas medan den sänker basen när huvudenheten är finare
Normalize snäpper varje värde till sin dag, sin månads första dag eller 1 januari innan några ticks finns, för en månadsbas under en daglig huvudenhet skulle kollapsa en månads punkter till en enda plats

Vad lagrar dateAx-elementet, och vad betyder utelämnandet?

I ChartML är varje kalenderinställning på en datumaxel valfri, och att utelämna en är inte samma sak som att skriva dess standardvärde. Typen CT_DateAx i ECMA-376 Part 1, §21.2 (DrawingML Charts) låter baseTimeUnit, majorTimeUnit, minorTimeUnit och auto var för sig dyka upp eller inte, och en utelämnad baseTimeUnit säger till Excel att bestämma själv, vilket skiljer sig från en explicit days. TXLSXChartAxis lagrar därför varje värde bredvid en närvaroflagga: BaseTimeUnit med BaseTimeUnitSet, MajorTimeUnit med MajorTimeUnitSet, MinorTimeUnit med MinorTimeUnitSet, samt AutoDateAxis med AutoDateAxisSet. Att tilldela ett värde sätter dess flagga, att rensa flaggan returnerar elementet till utelämnande, och XLSX-skrivaren emitterar ett element bara när dess flagga är satt. Notera att auto betyder automatisk detektering av kategori kontra datum, inte automatiska tickenheter

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;                  // skriver c:dateAx
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // sätter också BaseTimeUnitSet
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // > 0 markerar majorUnit som satt
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // grader; lagras som 1/60000
    // AutoDateAxis lämnas orörd: inget c:auto-element skrivs

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

Etiketter och orientering följer samma närvaromedvetna regler. Axelns numFmt ligger efter titeln och före tickmarkeringarna, dess attribut sourceLinked har true som standard, och TXLSXChartAxis håller formatkoden, källkopplingsvärdet och NumberFormatSet isär, så en tom kod misstolkas aldrig som ett frånvarande element. Textrotation lagras i 1/60000 av en grad med positiva värden medurs; TextStyle.Rotation tar vanliga grader, SVG-utdata använder vinkeln som den är, och den sidindelade backenden, vars positiva vinklar går moturs, växlar tecknet vid en gemensam gräns. En omvänd maxMin-axel speglar huvud- och underticks, rutnät, datapunkter, trendlinjer och felfält tillsammans, och aldrig bara etiketterna. När du öppnar en arbetsbok skriven någon annanstans berättar flaggorna vad upphovspersonen faktiskt angav

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;   // spara den igen och de utelämnade elementen förblir utelämnade
  end;
end;

Var tar kalendermodellen slut?

Kalendermodellen tar slut vid format som inte kan uttrycka kalenderenheter, och HotXLS låtsas inte annorlunda. ODF 1.2 erbjuder chart:interval-major, som är ett vanligt tal utan någon uppfattning om dagar, månader eller år, så en Excel-månadsaxel sparad som ODS kan inte bära sin enhet, och HotXLS hittar inte på något icke-standardiserat attribut för att fejka en. På den äldre BIFF8-sidan håller AxcExt-posten nio fasta 16-bitarsord, och dess automatikflaggor maskerar bara fält i stället för att ta bort dem; HotXLS bevarar de maskerade värdena för minimum, maximum, intervall, enhet och korsning vid inläsning och när data skrivs tillbaka, behåller okända enhetskoder intakta i Classic-modellen, och mappar bara de giltiga datumenheterna 0, 1 och 2 (dagar, månader, år) till XLSX

Datumaxlar är ett lager i en diagrammodell som också måste överleva filer den inte skapade själv. Kombinerar en Excel-arbetsbok en datumaxel-linjeserie med en kolumnserie på en sekundär axel visar vår artikel om bevarad ChartML och kombinationsdiagram hur HotXLS spolar upp den ursprungliga diagram-XML:en byte för byte när inget ändrats och slår samman typade redigeringar i den när något gjorde det, och vår översikt av diagram, bilder och ritningar täcker förankrings- och serie-API:erna som används ovan. Allt detta levereras i HotXLS Delphi Component för Delphi och C++Builder, som läser, skriver och renderar XLS- och XLSX-diagram utan Excel-automation