Artikel Teknis

Sumbu Tanggal Chart HotXLS: Tick Akurat Kalender di Delphi

HotXLS merender sumbu tanggal chart Excel dengan aritmetika kalender, bukan hitungan hari tetap. Kelas TXLSChartDateAxisTransform di unit lxChart menyimpan setiap koordinat sebagai serial workbook, menghitung tiap tick bulan atau tahun langsung dari minimum sumbu lewat IncMonth dan IncYear, dan menjepit akhir bulan ke kalender sungguhan, sehingga seri yang bertumpu pada 31 Januari mendapat tick di 28 Februari, 31 Maret, dan 30 April alih-alih bergeser. Transform yang sama memilih satuan hari, bulan, atau tahun secara otomatis dan dipakai bersama oleh renderer chart HTML, SVG, dan PDF terpaginasi

Bug yang dicegah ini akrab bagi siapa pun yang pernah membuat chart penutupan akhir bulan. Renderer yang memperlakukan satu bulan sebagai 30 hari akan menyimpang lima hari lebih awal di akhir tahun pertama. Renderer yang lebih cerdas dan memakai bulan kalender, tapi menghitung tiap tick dari tick sebelumnya, gagal lebih senyap: 31 Januari menjadi 28 Februari, langkah berikutnya mendarat di 28 Maret, dan semua tick setelah itu terpaku di tanggal 28. Chart-nya tampak masuk akal, labelnya salah, dan tidak ada yang sadar sampai seorang controller bertanya kenapa saldo Maret digambar tiga hari sebelum akhir kuartal

Mengapa tick bulan menyimpang saat diakumulasi?

Tick bulan menyimpang karena clamping membuang informasi. Begitu 31 Januari dijepit menjadi 28 Februari, fakta bahwa seri menginginkan tanggal 31 hilang, dan langkah apa pun yang diambil dari nilai hasil jepitan mewarisi kehilangan itu. TXLSChartDateAxisTransform.BuildTicks tidak pernah melangkah dari tick sebelumnya. Tick i selalu dihitung sebagai AddUnits(MinValue, UnitKind, Step * i, ...), yaitu min + unit × indeks yang diukur dari minimum sumbu. AddUnits menambahkan hitungan langsung ke serial untuk satuan hari, dan untuk satuan bulan serta tahun, mengonversi serial ke tanggal sungguhan, memanggil IncMonth atau IncYear, lalu mengonversi balik. Loop-nya juga terbatas di segala sisi: array hasil dicapped di 4096 entri apa pun yang diminta caller, Step * i diperiksa terhadap MaxInt sebelum dikalikan, dan generasi berhenti begitu sebuah tick gagal bertambah atau melewati maksimum sumbu

Dua cara sumbu tanggal chart HotXLS menempatkan tick bulan setelah jangkar 31 Januari 2026: memajukan tiap tick dari nilai hasil jepitan sebelumnya terpaku di tanggal 28, sementara BuildTicks menghitung AddUnits(MinValue, xcduMonths, Step kali i) dari minimum sumbu sehingga tick mendarat di 28 Februari, 31 Maret, dan 30 April
Clamping membuang informasi, jadi TXLSChartDateAxisTransform tidak pernah melangkah dari tick sebelumnya dan menghitung ulang setiap posisi dari minimum sumbu, menjepit akhir bulan ke kalender sungguhan
uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // sistem tanggal 1900 (Dates1904 = False), seri bertumpu akhir bulan
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // langkah satu bulan, target 12 tick, tak pernah lebih dari 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;

Ada satu aproksimasi yang jujur di BuildTicks. Ketika langkah yang diminta nol, metode ini mengestimasi ukuran langkah dengan memperlakukan satu bulan sebagai 30 hari dan satu tahun sebagai 365, lalu membulatkan langkah mentah ke kelipatan 1, 2, atau 5 dari pangkat sepuluh. Estimasi itu hanya menentukan berapa satuan di antara tick. Posisi tick sendiri tetap berasal dari IncMonth dan IncYear, jadi aproksimasi bisa mengubah kepadatan tick tapi tidak pernah memindahkan tick keluar dari kalender

Mengapa mempertahankan serial workbook alih-alih mengonversi ke TDateTime?

Sumbu tanggal di HotXLS mempertahankan serial workbook sebagai ruang koordinatnya karena TDateTime milik Delphi tidak punya slot untuk serial 60, 29 Februari 1900 fiktif yang diwarisi sistem tanggal 1900 sebagai keanehan kompatibilitas. TrySerialToDateTime menggeser serial di bawah 60 satu hari dan melipat serial 60 ke 28 Februari — tidak masalah untuk memberi label satu titik, tapi fatal untuk geometri: konversikan semua titik ke TDateTime lebih dulu dan dua hari di kedua sisi hari fiktif itu berakhir satu unit lebih rapat daripada yang digambar Excel, sehingga semua yang digambar sebelum Maret 1900 bergeser relatif terhadap semua yang sesudahnya. TXLSChartDateAxisTransform.FormatValue mengkhususkan slot yang sama dan melabelinya 1900-02-29 persis seperti Excel. Di workbook 1904, serial 0 adalah 1 Januari 1904, dan transform menerapkan offset tetap 1462 hari di kedua arah; kisah lengkap kedua epoch ada di artikel kami soal serial tanggal Excel, sistem 1904, dan number format

Mengapa sumbu tanggal HotXLS mempertahankan serial workbook alih-alih mengonversi ke TDateTime: serial 60 adalah 29 Februari 1900 yang fiktif, TrySerialToDateTime menggeser serial di bawah 60 dan melipat 60 ke 28 Februari sehingga dua hari di kedua sisinya berakhir satu unit lebih rapat daripada yang digambar Excel, sementara FormatValue melabeli serial 60 sebagai 1900-02-29
Pelipatan itu tidak masalah untuk memberi label satu titik tapi fatal untuk geometri, karena semua yang digambar sebelum Maret 1900 bergeser terhadap semua yang sesudahnya; workbook 1904 menerapkan offset tetap 1462 hari sebagai gantinya

Bagaimana HotXLS memilih hari, bulan, atau tahun secara otomatis?

TXLSChartDateAxisTransform.DetectUnit memilih satuan kalender terhalus yang benar-benar dibutuhkan oleh jarak-jarak di data. Metode ini membuang nilai NaN dan tak hingga lebih dulu, mengurutkan sisanya sekali dalam O(n log n), dan melewati tanggal duplikat. Bila ada jarak bersebelahan yang lebih pendek dari satu bulan kalender, satuannya hari; bila tidak, dan ada jarak yang lebih pendek dari satu tahun kalender, satuannya bulan; bila tidak, tahun. Ujiannya adalah IncMonth(Previous, 1) > Current, bukan ambang 30 hari, sehingga seri akhir bulan yang berjalan 31 Januari, 28 Februari, 31 Maret terdeteksi benar sebagai data bulanan. Dengan kurang dari dua tanggal valid yang berbeda, tak ada jarak untuk diukur, dan metode ini kembali ke hari

ResolveUnits lalu menggabungkan satuan yang terdeteksi dengan apa pun yang dideklarasikan chart. Satuan basis eksplisit dihormati, dan satuan major atau minor yang tak disetel mewarisi satuan terdeteksi bila satuan itu lebih kasar dari basis. Langkah terakhir adalah yang paling sering terlewat: bila satuan major atau minor efektif lebih halus dari satuan basis, basis diturunkan agar cocok. Satuan basis adalah tempat titik data dinormalisasi sebelum tick mana pun ada, dengan Normalize menjepit nilai ke hari-nya, tanggal satu bulannya, atau 1 Januari, sehingga basis bulanan di bawah satuan major harian akan menciutkan satu bulan titik menjadi satu slot sebelum kode tick sempat melihatnya. Kepadatan tick juga dibatasi, oleh panjang plot, lebar terukur font sumbu, number format label, dan proyeksi label yang diputar, dan setiap backend menggambar label, tick mark, dan gridline dari satu array tick bersama yang terbatas, dengan minor tick yang bertepatan dengan major tick hanya digambar sekali

Cara HotXLS memilih satuan sumbu tanggal secara otomatis: DetectUnit membuang nilai NaN dan tak hingga, mengurutkan sekali, dan mengukur jarak kalender dengan IncMonth alih-alih ambang 30 hari, sehingga seri 31 Januari, 28 Februari, 31 Maret terdeteksi sebagai bulanan, dan ResolveUnits menghormati basis eksplisit sambil menurunkan basis bila satuan major lebih halus
Normalize menjepit tiap nilai ke hari-nya, tanggal satu bulannya, atau 1 Januari sebelum tick mana pun ada, karena basis bulanan di bawah satuan major harian akan menciutkan satu bulan titik menjadi satu slot

Apa yang disimpan elemen dateAx, dan apa arti penghilangannya?

Di ChartML setiap setelan kalender pada sumbu tanggal bersifat opsional, dan menghilangkannya tidak sama dengan menulis nilai default-nya. Tipe CT_DateAx di ECMA-376 Part 1, §21.2 (DrawingML Charts) membolehkan baseTimeUnit, majorTimeUnit, minorTimeUnit, dan auto masing-masing muncul atau tidak, dan baseTimeUnit yang dihilangkan menyuruh Excel memutuskan sendiri, yang berbeda dari days eksplisit. TXLSXChartAxis karena itu menyimpan tiap nilai bersebelahan dengan flag kehadiran: BaseTimeUnit dengan BaseTimeUnitSet, MajorTimeUnit dengan MajorTimeUnitSet, MinorTimeUnit dengan MinorTimeUnitSet, dan AutoDateAxis dengan AutoDateAxisSet. Mengisi nilai menyetel flag-nya, menghapus flag mengembalikan elemen ke keadaan dihilangkan, dan writer XLSX hanya mengeluarkan elemen ketika flag-nya tersetel. Perhatikan bahwa auto berarti deteksi otomatis kategori-versus-tanggal, bukan satuan tick otomatis

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));   // dari unit 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;                  // menulis c:dateAx
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // sekalian menyetel BaseTimeUnitSet
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // > 0 menandai majorUnit terisi
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // derajat; disimpan sebagai 1/60000
    // AutoDateAxis dibiarkan tak disentuh: tidak ada elemen c:auto yang ditulis

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

Label dan orientasi mengikuti aturan sadar kehadiran yang sama. numFmt milik sumbu duduk setelah judul dan sebelum tick mark, atribut sourceLinked-nya default true, dan TXLSXChartAxis menyimpan kode format, nilai source-linked, dan NumberFormatSet secara terpisah, sehingga kode kosong tidak pernah disalahartikan sebagai elemen yang absen. Rotasi teks disimpan dalam 1/60000 derajat dengan nilai positif berarti searah jarum jam; TextStyle.Rotation menerima derajat biasa, output SVG memakai sudut apa adanya, dan backend terpaginasi yang sudut positifnya berlawanan arah jarum jam membalik tandanya di satu boundary bersama. Sumbu maxMin yang dibalik memantulkan major dan minor tick, gridline, titik data, trendline, dan error bar sekaligus, tidak pernah hanya labelnya saja. Ketika Anda membuka workbook yang ditulis di tempat lain, flag-flag itu memberi tahu apa yang benar-benar dispesifikasikan penulisnya

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;   // simpan lagi dan elemen yang dihilangkan tetap dihilangkan
  end;
end;

Di mana model kalender berhenti?

Model kalender berhenti di format yang tak mampu mengekspresikan satuan kalender, dan HotXLS tidak berpura-pura sebaliknya. ODF 1.2 menyediakan chart:interval-major, angka polos tanpa konsep hari, bulan, atau tahun, sehingga sumbu bulanan Excel yang disimpan sebagai ODS tidak bisa membawa satuannya dan HotXLS tidak merekayasa atribut non-standar untuk memalsukannya. Di sisi BIFF8 lawas, record AxcExt menampung sembilan kata 16-bit tetap, dan flag otomatisnya hanya menandai field yang tersamarkan alih-alih menghapusnya; HotXLS mempertahankan nilai minimum, maksimum, interval, satuan, dan crossing yang tersamarkan saat membaca dan menulis kembali, menjaga kode satuan tak dikenal tetap utuh di model Classic, dan hanya memetakan satuan tanggal valid 0, 1, dan 2 (hari, bulan, tahun) ke XLSX

Sumbu tanggal adalah satu lapisan dari model chart yang juga harus selamat dari berkas yang bukan ciptaannya. Ketika workbook Excel menggabungkan line sumbu tanggal dengan seri kolom di sumbu sekunder, artikel kami soal ChartML yang dipertahankan dan combination chart menunjukkan cara HotXLS memutar ulang XML chart asli byte demi byte ketika tidak ada yang berubah dan menggabungkan suntingan bertipe ke dalamnya ketika ada yang berubah, dan tinjauan kami soal chart, gambar, dan drawing membahas API anchoring dan seri yang dipakai di atas. Semuanya terkandung dalam HotXLS Delphi Component untuk Delphi dan C++Builder, yang membaca, menulis, dan merender chart XLS dan XLSX tanpa automasi Excel