O HotXLS renderiza eixos de datas de gráficos do Excel com aritmética de calendário, não com contagens fixas de dias. A classe TXLSChartDateAxisTransform na unidade lxChart mantém todas as coordenadas como números de série do livro, calcula cada marca de mês ou de ano diretamente a partir do mínimo do eixo através de IncMonth e IncYear, e ajusta os fins de mês ao calendário real, pelo que uma série ancorada a 31 de janeiro recebe marcas a 28 de fevereiro, 31 de março e 30 de abril em vez de deslizar. A mesma transformação escolhe unidades de dia, mês ou ano automaticamente e é partilhada pelos renderizadores de gráficos HTML, SVG e PDF paginado
O bug que isto previne é familiar a quem já fez um gráfico de fecho de fim de mês. Um renderizador que trate um mês como 30 dias deriva cinco dias mais cedo até ao fim do primeiro ano. Um renderizador mais esperto que use meses de calendário, mas avance cada marca a partir da anterior, falha de forma mais silenciosa: 31 de janeiro torna-se 28 de fevereiro, o passo seguinte cai a 28 de março, e todas as marcas a seguir ficam presas ao dia 28. O gráfico parece plausível, os rótulos estão errados, e ninguém repara até que um diretor financeiro pergunte por que motivo o saldo de março está desenhado três dias antes do fim do trimestre
Porque é que as marcas mensais derivam quando são acumuladas?
As marcas mensais derivam porque o ajuste deita informação fora. Depois de 31 de janeiro ter sido ajustado a 28 de fevereiro, o facto de a série querer o dia 31 perdeu-se, e qualquer passo tirado do valor ajustado herda a perda. O TXLSChartDateAxisTransform.BuildTicks nunca avança a partir de uma marca anterior. A marca i é sempre calculada como AddUnits(MinValue, UnitKind, Step * i, ...), que é min + unidade × índice medido a partir do mínimo do eixo. O AddUnits acrescenta a contagem diretamente ao número de série para unidades de dia e, para unidades de mês e de ano, converte o número de série numa data real, chama o IncMonth ou o IncYear, e converte de volta. O ciclo é também limitado em todos os sentidos: a matriz resultado está limitada a 4096 entradas, peça o que peça quem chama, o Step * i é verificado contra MaxInt antes de ser multiplicado, e a geração para assim que uma marca deixa de aumentar ou passa o máximo do eixo
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Sistema de datas 1900 (Dates1904 = False), série ancorada ao 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, mira 12 marcas, nunca mais de 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á uma aproximação honesta no BuildTicks. Quando o passo pedido é zero, o método estima um tamanho de passo tratando um mês como 30 dias e um ano como 365, e depois arredonda o passo bruto para um múltiplo de 1, 2 ou 5 de uma potência de dez. Essa estimativa só decide quantas unidades ficam entre marcas. As posições das marcas em si continuam a vir do IncMonth e do IncYear, pelo que a aproximação pode mudar a densidade de marcas, mas nunca move uma marca para fora do calendário
Porque manter números de série do livro em vez de converter para TDateTime?
Um eixo de datas no HotXLS mantém números de série do livro como espaço de coordenadas porque o TDateTime do Delphi não tem lugar para o número de série 60, o fantasma 29 de fevereiro de 1900 que o sistema de datas 1900 herdou como uma peculiaridade de compatibilidade. O TrySerialToDateTime desloca números de série abaixo de 60 um dia e colapsa o número de série 60 em 28 de fevereiro, o que serve para rotular um ponto isolado mas é fatal para a geometria: converta primeiro todos os pontos para TDateTime e os dois dias a cada lado do dia fantasma acabam uma unidade mais perto um do outro do que o Excel os desenha, pelo que tudo o que está desenhado antes de março de 1900 se desloca em relação a tudo o que vem depois. O TXLSChartDateAxisTransform.FormatValue trata esse mesmo lugar como caso especial e rotula-o 1900-02-29 exatamente como o Excel faz. Num livro 1904, o número de série 0 é 1 de janeiro de 1904, e a transformação aplica o desvio fixo de 1462 dias em ambas as direções; a história completa das duas épocas está no nosso artigo sobre números de série de datas do Excel, o sistema 1904 e formatos numéricos
Como é que o HotXLS escolhe dias, meses ou anos automaticamente?
O TXLSChartDateAxisTransform.DetectUnit escolhe a unidade de calendário mais fina de que os intervalos nos dados realmente precisam. O método deita primeiro fora os valores NaN e infinitos, ordena os restantes uma vez em O(n log n), e salta datas duplicadas. Se algum intervalo adjacente for mais curto do que um mês de calendário, a unidade é dias; caso contrário, se algum intervalo for mais curto do que um ano de calendário, a unidade é meses; caso contrário, são anos. O teste é IncMonth(Previous, 1) > Current, não um limiar de 30 dias, pelo que uma série de fim de mês a correr 31 de janeiro, 28 de fevereiro, 31 de março é corretamente detetada como dados mensais. Com menos de duas datas válidas distintas não há intervalo a medir, e o método recua para dias
O ResolveUnits funde depois a unidade detetada com aquilo que o gráfico declara. Uma unidade base explícita é honrada, e uma unidade major ou minor em falta herda a unidade detetada quando esta é mais grosseira do que a base. O último passo é o que as pessoas perdem: se a unidade major ou minor efetiva for mais fina do que a unidade base, a base é baixada para acompanhar. A unidade base é aquilo a que os pontos de dados são normalizados antes de existirem marcas, com o Normalize a encaixar um valor no seu dia, no primeiro do seu mês ou em 1 de janeiro, pelo que uma base mensal sob uma unidade major diária colapsaria um mês de pontos numa única ranhura antes de o código de marcas algum dia os ver. A densidade de marcas também é limitada, pelo comprimento da área de desenho, pela largura medida do tipo de letra do eixo, pelo formato numérico dos rótulos e pela projeção de rótulos rodados, e todos os backends desenham rótulos, marcas e linhas de grelha a partir de uma única matriz de marcas partilhada e limitada, com uma marca minor que coincida com uma marca major desenhada uma só vez
O que guarda o elemento dateAx, e o que significa a omissão?
No ChartML todas as definições de calendário de um eixo de datas são opcionais, e omitir uma não é o mesmo que escrever o seu valor predefinido. O tipo CT_DateAx no ECMA-376 Parte 1, §21.2 (DrawingML Charts) deixa baseTimeUnit, majorTimeUnit, minorTimeUnit e auto aparecer ou não cada um, e um baseTimeUnit omitido diz ao Excel para decidir por si, o que é diferente de um days explícito. O TXLSXChartAxis guarda portanto 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 ativa a sua flag, limpar a flag devolve o elemento à omissão, e o escritor XLSX emite um elemento só quando a sua flag está ativa. Note que auto significa deteção automática de categoria contra data, não unidades de marcas automáticas
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; // escreve c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // também ativa BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 marca majorUnit como definido
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // graus; guardado como 1/60000
// AutoDateAxis fica intocado: nenhum elemento c:auto é escrito
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Os rótulos e a orientação seguem as mesmas regras conscientes de presença. O numFmt do eixo fica depois do título e antes das marcas, o seu atributo sourceLinked assume por predefinição true, e o TXLSXChartAxis guarda o código de formato, o valor source-linked e o NumberFormatSet separadamente, pelo que um código vazio nunca é confundido com um elemento ausente. A rotação de texto é guardada em 1/60000 de grau com valores positivos a significar sentido horário; o TextStyle.Rotation recebe graus simples, a saída SVG usa o ângulo tal como está, e o backend paginado, cujos ângulos positivos correm no sentido anti-horário, inverte o sinal numa única fronteira partilhada. Um eixo maxMin invertido espelha as marcas major e minor, as linhas de grelha, os pontos de dados, as linhas de tendência e as barras de erro em conjunto, e nunca só os rótulos. Quando abre um livro escrito por outra parte, as flags dizem-lhe o que o autor realmente 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[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; // grave outra vez e os elementos omitidos continuam omitidos
end;
end;
Onde pára o modelo de calendário?
O modelo de calendário pára nos formatos que não conseguem exprimir unidades de calendário, e o HotXLS não finge o contrário. O ODF 1.2 oferece o chart:interval-major, que é um número simples sem qualquer noção de dias, meses ou anos, pelo que um eixo mensal do Excel gravado como ODS não consegue transportar a sua unidade, e o HotXLS não inventa um atributo não padrão para fingir uma. Do lado do BIFF8 legado, o registo AxcExt contém nove palavras fixas de 16 bits, e as suas flags automáticas apenas mascaram campos em vez de os remover; o HotXLS preserva os valores mascarados de mínimo, máximo, intervalo, unidade e cruzamento na leitura e na escrita de volta, mantém intactos os códigos de unidade desconhecidos no modelo Classic, e mapeia apenas as unidades de data válidas 0, 1 e 2 (dias, meses, anos) para XLSX
Os eixos de datas são uma camada de um modelo de gráficos que também tem de sobreviver a ficheiros que não criou. Quando um livro do Excel combina 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 mostra como o HotXLS reprime o XML original do gráfico byte a byte 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 APIs de ancoragem e de séries usadas acima. Tudo isto vem no HotXLS Delphi Component para Delphi e C++Builder, que lê, escreve e renderiza gráficos XLS e XLSX sem automação do Excel