Artigo Técnico

Eixo de datas no HotXLS: ticks fiéis ao calendário no Delphi

Um gráfico de saldo de fim de mês que começa em 31 de janeiro de 2026 deveria pôr o tick de abril em 30 de abril. Avance cada tick a partir do anterior com meses de calendário e os rótulos saem como 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28, com todo tick posterior preso no dia 28. Avance por 30 dias fixos e o tick de fevereiro cai em 2 de março, enquanto o de dezembro cai em 27 de dezembro. A regra que sobrevive às duas armadilhas é simples de enunciar: calcule cada tick a partir do mínimo do eixo, e clampe fins de mês ao calendário real

Num gráfico de fechamento trimestral o primeiro erro é o perigoso, porque a figura ainda parece certa. A linha sobe onde deve, os rótulos estão igualmente espaçados, e o saldo de março fica três dias antes do fim do trimestre. Quem descobre é o controlador comparando o gráfico com o ledger, e a essa altura o gráfico normalmente já foi colado num pacote de conselho

Por que os ticks de mês escorregam quando são acumulados?

Ticks de mês escorregam porque o clamp joga informação fora. Uma vez que 31 de janeiro foi clampeado para 28 de fevereiro, o fato de a série querer o dia 31 se perdeu, e qualquer passo tomado a partir do valor clampeado herda a perda. No HotXLS, o TXLSChartDateAxisTransform.BuildTicks na unidade lxChart nunca avança a partir de um tick anterior: o tick i é sempre AddUnits(MinValue, UnitKind, Step * i, ...), então março é dois meses depois de 31 de janeiro e sai como 31 de março. O AddUnits soma a contagem direto ao serial para unidades de dia; para unidades de mês e ano ele converte o serial numa data real, chama IncMonth ou IncYear e converte de volta

O loop é limitado por todos os lados. O array de resultado nunca passa de 4096 entradas, passe o chamador o máximo que passar, o Step * i é conferido contra MaxInt antes da multiplicação acontecer, e a geração para assim que um tick deixa de aumentar ou passa do máximo do eixo. Uma pasta de trabalho hostil com um vão de alguns milhões de anos produz um array curto e retorna

Duas maneiras de um eixo de datas de gráfico do HotXLS posicionar ticks de mês depois de uma âncora em 31 de janeiro de 2026: avançar cada tick a partir do valor clampeado anterior fica preso no dia 28, enquanto o BuildTicks calcula AddUnits(MinValue, xcduMonths, Step times i) do mínimo do eixo, então os ticks caem em 28 de fevereiro, 31 de março e 30 de abril
O clamp joga informação fora, então o TXLSChartDateAxisTransform nunca dá passo a partir de um tick anterior e recalcula toda posição do mínimo do eixo, clampeando fins de mês no calendário real

A transformação é pública, então você pode rodar a aritmética por conta própria antes de qualquer gráfico existir. Este trecho de console constrói um ano de ticks mensais para a série acima e imprime os rótulos que o gráfico vai usar

uses
  SysUtils, lxChart;

var
  Ticks: TXLSChartDateValues;
  MinSerial, MaxSerial: Double;
  I, Count: Integer;
begin
  // Sistema de datas 1900 (Dates1904 = False), série ancorada no fim de mês
  MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 1, 31), False);
  MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
    EncodeDate(2026, 12, 31), False);

  // Passo de um mês, mirar 12 ticks, nunca mais que 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;

Há exatamente uma aproximação no BuildTicks, e ela fica onde uma aproximação é inofensiva. Quando o passo pedido é zero, o método estima um passo tratando um mês como 30 dias e um ano como 365, depois arredonda o passo bruto para cima até um múltiplo 1, 2 ou 5 de uma potência de dez. Essa estimativa decide só quantas unidades ficam entre ticks. As posições ainda vêm de IncMonth e IncYear. Um comprimento de mês grosseiro pode deixar o eixo um pouco mais denso ou esparso, mas todo tick ainda aterrissa numa data que uma daquelas duas funções produziu

Seriais da pasta seguem como coordenada do eixo

Um eixo de datas no HotXLS mantém seriais da pasta de trabalho como espaço de coordenadas porque o TDateTime do Delphi não tem encaixe para o serial 60. No sistema de datas 1900 esse serial é 29 de fevereiro de 1900, um dia que nunca existiu e que o Excel mantém por compatibilidade com programas de planilha mais antigos. O TXLSChartDateAxisTransform.TrySerialToDateTime o trata do único jeito que um calendário real consegue: seriais abaixo de 60 deslocam um dia e o serial 60 dobra em 28 de fevereiro. Para rotular um único ponto isso está bem. Para geometria está errado, já que os seriais 59 e 60 viram o mesmo TDateTime, e 28 de fevereiro e 1º de março acabam a uma unidade de distância onde o Excel os desenha a duas

Três seriais mostram os dois lados do problema. O FormatValue trata o encaixe fantasma como caso especial e imprime exatamente o que o Excel imprime, enquanto um round trip por TDateTime o perde

var
  S: Integer;
  D: TDateTime;
begin
  for S := 59 to 61 do
  begin
    TXLSChartDateAxisTransform.TrySerialToDateTime(S, False, D);
    Writeln(S, '  axis label: ',
      TXLSChartDateAxisTransform.FormatValue(S, False),
      '  TDateTime: ', FormatDateTime('yyyy-mm-dd', D));
  end;
  // 59  rótulo do eixo: 1900-02-28  TDateTime: 1900-02-28
  // 60  rótulo do eixo: 1900-02-29  TDateTime: 1900-02-28
  // 61  rótulo do eixo: 1900-03-01  TDateTime: 1900-03-01
end;

Quase ninguém grafica dados do começo de 1900, então a lição prática é sobre o seu próprio código. Se você calcula limites de eixo ou posições de anotações, faça em seriais e converta com DateTimeToSerial na borda, como o primeiro trecho faz. Misture um eixo baseado em serial com aritmética TDateTime no chamador, e uma anotação datada antes de 1º de março de 1900 aterrissa a um dia do ponto dela. A segunda época importa com mais frequência: numa pasta 1904 o serial 0 é 1º de janeiro de 1904, e a transformação aplica o offset fixo de 1462 dias nas duas direções quando o argumento Dates1904 é verdadeiro. Ambas as épocas, e como formatos numéricos de célula interagem com elas, estão cobertos em nosso artigo sobre seriais de data do Excel, o sistema 1904 e formatos numéricos

Por que um eixo de datas do HotXLS mantém seriais da pasta em vez de converter para TDateTime: o serial 60 é o fantasma 29 de fevereiro de 1900, o TrySerialToDateTime desloca seriais abaixo de 60 e dobra o 60 em 28 de fevereiro, então os dias em volta acabam uma unidade mais próximos do que o Excel desenha, enquanto o FormatValue rotula o serial 60 como 1900-02-29
A dobra serve para rotular um ponto isolado, mas é fatal para a geometria, porque tudo plotado antes de março de 1900 se desloca contra tudo depois; uma pasta 1904 aplica o offset fixo de 1462 dias em vez disso

Como o HotXLS escolhe dias, meses ou anos automaticamente?

O TXLSChartDateAxisTransform.DetectUnit escolhe a menor unidade de calendário que os vãos nos dados realmente exigem. Ele descarta valores NaN e infinitos, ordena uma cópia do resto uma vez, e pula datas duplicadas. Se algum vão adjacente é menor que um mês de calendário, a unidade é dias; caso contrário, se algum vão é menor que um ano de calendário, meses; senão, anos. O teste de mês é IncMonth(Previous, 1) > Current, e essa escolha importa justamente para os dados deste artigo. Um limiar de 30 dias chamaria o vão de 31 de janeiro a 28 de fevereiro de "menos de um mês" e trocaria uma série mensal para dias. Com menos de duas datas válidas distintas não há vão a medir, e o método devolve dias

O ResolveUnits funde a unidade detectada com o que o gráfico declara. Uma unidade base explícita vence. Uma unidade major ou minor ausente herda a detectada quando ela é mais grossa que a base, e caso contrário recua para a base. A última regra é a que as pessoas perdem: se a unidade major ou minor efetiva é mais fina que a base, a base abaixa para acompanhar. A unidade base decide como os pontos são agrupados antes de qualquer tick existir, porque o Normalize prende um valor ao dia dele, ao primeiro do mês dele ou a 1º de janeiro. Uma base mensal sob uma unidade major diária dobraria um mês inteiro de pontos num único encaixe antes de o código de ticks sequer vê-los

Como o HotXLS escolhe unidades de eixo de datas automaticamente: o DetectUnit descarta valores NaN e infinitos, ordena uma vez e mede vãos de calendário com IncMonth em vez de um limiar de 30 dias, então uma série 31 de janeiro, 28 de fevereiro, 31 de março é detectada como mensal, e o ResolveUnits respeita uma base explícita enquanto abaixa a base quando a unidade major é mais fina
O Normalize ajeita cada valor para o seu dia, o primeiro do seu mês ou 1º de janeiro antes de qualquer tick existir, porque uma base mensal sob uma unidade major diária colapsaria um mês de pontos num único slot

Quantos ticks major automáticos aparecem depende de mais que o intervalo de datas. O comprimento da área de plotagem, a largura medida da fonte do eixo, o formato numérico dos rótulos e a largura projetada de rótulos rotacionados todos limitam a contagem, então um gráfico estreito com rótulos longos mmmm yyyy recebe menos ticks que um largo com mm/yy. Rótulos, marcas de tick e linhas de grade são todos desenhados de um único array de ticks, o que mantém a saída HTML, SVG e PDF em acordo, e um tick minor que cai num tick major é desenhado uma vez

O que o elemento dateAx guarda, e o que a omissão significa?

No ChartML toda configuração de calendário num eixo de datas é opcional, e omitir uma diz algo diferente de gravar o padrão dela. O tipo CT_DateAx no ECMA-376 Part 1, §21.2 (DrawingML Charts) deixa baseTimeUnit, majorTimeUnit, minorTimeUnit e auto aparecerem ou não. Um baseTimeUnit omitido diz ao Excel para decidir quando renderizar; um days explícito o fixa. O TXLSXChartAxis guarda cada valor ao lado de uma flag de presença: BaseTimeUnit com BaseTimeUnitSet, MajorTimeUnit com MajorTimeUnitSet, MinorTimeUnit com MinorTimeUnitSet, e AutoDateAxis com AutoDateAxisSet. Atribuir um valor define a flag dele, definir a flag como False devolve o elemento à omissão, e o escritor XLSX emite um elemento só quando a flag dele está ligada. O auto é fácil de ler errado: ele controla a detecção automática de categoria versus data e não tem nada a ver com unidades automáticas de tick

uses
  SysUtils, DateUtils, lxHandleX;

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));
      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;                  // grava c:dateAx
    Axis.BaseTimeUnit := xlsxChartTimeDays;     // também define BaseTimeUnitSet
    Axis.MajorTimeUnit := xlsxChartTimeMonths;
    Axis.MajorUnit := 1;                        // um valor > 0 marca majorUnit como definido
    Axis.NumberFormat := 'mmm yyyy';
    Axis.NumberFormatSourceLinked := False;
    Axis.TextStyle.Rotation := -45;             // graus
    // AutoDateAxis deixado intocado: nenhum elemento c:auto é gravado

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

A unidade base aqui é dias de propósito. Datas de fim de mês caem em números de dia diferentes, e uma base mensal prenderia cada ponto ao primeiro do mês dele, o que leva o saldo de 31 de março para 1º de março. Fixe a base em dias sempre que o dia exato carrega significado, e deixe a unidade major fazer o espaçamento mensal

Rótulos e orientação seguem as mesmas regras de presença. O elemento numFmt do eixo tem como padrão sourceLinked verdadeiro, então o TXLSXChartAxis mantém separados o código de formato, o valor source-linked e o NumberFormatSet, e um código vazio nunca é confundido com elemento ausente. O ChartML guarda rotação de texto em 1/60000 de grau com valores positivos correndo no sentido horário; TextStyle.Rotation recebe graus simples, e um rótulo definido como -45 inclina do mesmo jeito na saída SVG e PDF. Um eixo invertido (maxMin) espelha ticks major e minor, linhas de grade, pontos de dados, linhas de tendência e barras de erro juntos. A leitura funciona ao contrário: abra uma pasta gravada por outra pessoa e as flags dizem o que o autor dela de fato especificou

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[1].Charts[0].CategoryAxis;  // Sheets[] é base 1, Charts[] é base 0
    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;   // salve de novo e os elementos omitidos continuam omitidos
  end;
end;

Unidades de calendário em arquivos ODS e XLS legados

O modelo de calendário alcança até onde cada formato de arquivo consegue expressá-lo. O ODF 1.2 descreve o espaçamento do eixo com chart:interval-major, um número simples sem noção de dias, meses ou anos, então um eixo mensal do Excel salvo como ODS não pode carregar a unidade dele. O HotXLS deixa a unidade de fora do arquivo ODS em vez de inventar um atributo não padrão que outras aplicações ODF ignorariam de qualquer jeito. Se o espaçamento mensal importa para quem vai abrir o arquivo, distribua XLSX

Pastas BIFF8 guardam as configurações de eixo de datas no registro AxcExt, nove words fixas de 16 bits. As flags automáticas dele mascaram campos sem removê-los, então um registro marcado como "mínimo automático" ainda carrega um valor de mínimo. O HotXLS mantém o mínimo, o máximo, o intervalo, a unidade e os valores de cruzamento mascarados na leitura e os regrava inalterados, mantém códigos de unidade desconhecidos intactos no modelo de objetos XLS, e mapeia só as unidades de data válidas 0, 1 e 2 (dias, meses, anos) quando um gráfico é convertido para XLSX

Um eixo de datas raramente viaja sozinho. Quando uma pasta do Excel emparelha uma linha de eixo de datas com uma série de colunas num eixo secundário, o nosso artigo sobre ChartML preservado e gráficos de combinação explica como o HotXLS reprisa o XML original do gráfico quando nada mudou e funde edições tipadas nele quando algo mudou, e a nossa visão geral de gráficos, imagens e drawings cobre as chamadas de ancoragem e de série usadas acima. A página do HotXLS Delphi Component lista o conjunto completo de recursos de gráfico para Delphi e C++Builder, incluindo leitura, escrita e renderização de gráficos XLS e XLSX sem Excel instalado