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
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
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
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