HotXLS renderiza los ejes de fechas de gráficos de Excel con aritmética de calendario, no con conteos fijos de días. La clase TXLSChartDateAxisTransform de la unidad lxChart mantiene cada coordenada como un serial del libro, calcula cada tick de mes o de año directo desde el mínimo del eje mediante IncMonth e IncYear, y ajusta los fines de mes al calendario real, así que una serie anclada al 31 de enero recibe ticks el 28 de febrero, el 31 de marzo y el 30 de abril en lugar de irse deslizando. La misma transformación elige unidades de día, mes o año automáticamente y la comparten los renderizadores de gráficos HTML, SVG y PDF paginado
El defecto que esto previene le resulta familiar a cualquiera que haya graficado un cierre de fin de mes. Un renderizador que trata el mes como 30 días se adelanta cinco días para el primer año. Uno más listo que usa meses de calendario pero avanza cada tick a partir del anterior falla con menos ruido: el 31 de enero se convierte en 28 de febrero, el siguiente paso cae en 28 de marzo, y cada tick posterior queda clavado en el 28. El gráfico se ve plausible, las etiquetas están mal, y nadie lo nota hasta que un controller pregunta por qué el balance de marzo está graficado tres días antes del cierre de trimestre
¿Por qué se desvían los ticks de mes cuando se acumulan?
Los ticks de mes se desvían porque el clamp tira información. Una vez que el 31 de enero fue ajustado a 28 de febrero, el dato de que la serie quería el 31 se perdió, y cualquier paso tomado desde el valor ajustado hereda la pérdida. TXLSChartDateAxisTransform.BuildTicks nunca avanza desde un tick anterior. El tick i siempre se computa como AddUnits(MinValue, UnitKind, Step * i, ...), que es min + unidad × índice medido desde el mínimo del eje. AddUnits suma el conteo directo al serial para unidades de día y, para unidades de mes y año, convierte el serial a una fecha real, llama a IncMonth o IncYear, y convierte de vuelta. El loop además está acotado por todos lados: el arreglo de resultado se limita a 4096 entradas pida lo que pida quien llama, Step * i se verifica contra MaxInt antes de multiplicarse, y la generación se detiene en cuanto un tick deja de crecer o pasa el máximo del eje
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Sistema de fechas 1900 (Dates1904 = False), serie anclada a fin de mes
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Paso de un mes, apuntando a 12 ticks, nunca más 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;
Hay una aproximación honesta dentro de BuildTicks. Cuando el paso pedido es cero, el método estima un tamaño de paso tratando el mes como 30 días y el año como 365, y luego redondea el paso crudo a un múltiplo 1, 2 o 5 de una potencia de diez. Esa estimación solo decide cuántas unidades hay entre ticks. Las posiciones de los ticks siguen viniendo de IncMonth e IncYear, así que la aproximación puede cambiar la densidad de ticks pero jamás mueve un tick fuera del calendario
¿Por qué conservar seriales del libro en lugar de convertir a TDateTime?
Un eje de fechas en HotXLS conserva los seriales del libro como su espacio de coordenadas porque el TDateTime de Delphi no tiene ranura para el serial 60, el fantasma 29 de febrero de 1900 que el sistema de fechas 1900 heredó como rareza de compatibilidad. TrySerialToDateTime corre los seriales por debajo de 60 un día y pliega el serial 60 sobre el 28 de febrero, lo cual sirve para etiquetar un punto suelto pero es fatal para la geometría: convierta cada punto a TDateTime primero y los dos días a los lados del día fantasma quedan una unidad más cerca de lo que Excel los dibuja, así que todo lo graficado antes de marzo de 1900 se desplaza respecto de todo lo que viene después. TXLSChartDateAxisTransform.FormatValue trata la misma ranura como caso especial y la etiqueta 1900-02-29 exactamente igual que Excel. En un libro 1904, el serial 0 es el 1 de enero de 1904, y la transformación aplica el desfase fijo de 1462 días en ambas direcciones; la historia completa de las dos épocas está en nuestro artículo sobre seriales de fecha de Excel, el sistema 1904 y los formatos numéricos
¿Cómo elige HotXLS días, meses o años automáticamente?
TXLSChartDateAxisTransform.DetectUnit elige la unidad de calendario más fina que los huecos de los datos realmente necesitan. El método descarta primero los valores NaN e infinitos, ordena el resto una vez en O(n log n), y salta las fechas duplicadas. Si algún hueco adyacente es más corto que un mes de calendario, la unidad es días; si no, si algún hueco es más corto que un año de calendario, la unidad es meses; y si no, años. La prueba es IncMonth(Previous, 1) > Current, no un umbral de 30 días, así que una serie de fin de mes que corre 31 de enero, 28 de febrero, 31 de marzo se detecta correctamente como datos mensuales. Con menos de dos fechas válidas distintas no hay hueco que medir, y el método cae de vuelta a los días
ResolveUnits luego fusiona la unidad detectada con lo que el gráfico declare. Una unidad base explícita se respeta, y una unidad mayor o menor ausente hereda la unidad detectada cuando esta es más gruesa que la base. El último paso es el que la gente pasa por alto: si la unidad mayor o menor efectiva es más fina que la unidad base, la base se baja para emparejar. La unidad base es a lo que se normalizan los puntos de datos antes de que exista cualquier tick, con Normalize ajustando un valor a su día, al primero de su mes o al 1 de enero, de modo que una base mensual bajo una unidad mayor diaria colapsaría un mes de puntos en una sola ranura antes de que el código de ticks los viera. La densidad de ticks también está acotada, por el largo del área de trazado, el ancho medido de la fuente del eje, el formato numérico de las etiquetas y la proyección de etiquetas rotadas, y cada backend dibuja etiquetas, marcas de tick y líneas de cuadrícula desde un único arreglo de ticks compartido y acotado, con un tick menor que coincida con uno mayor dibujado una sola vez
¿Qué guarda el elemento dateAx y qué significa omitirlo?
En ChartML cada ajuste de calendario de un eje de fechas es opcional, y omitir uno no es lo mismo que escribir su valor por defecto. El tipo CT_DateAx en ECMA-376 Part 1, §21.2 (DrawingML Charts) permite que baseTimeUnit, majorTimeUnit, minorTimeUnit y auto aparezcan o no, y un baseTimeUnit omitido le dice a Excel que decida por su cuenta, que es distinto de un days explícito. TXLSXChartAxis guarda por tanto cada valor junto a una bandera de presencia: BaseTimeUnit con BaseTimeUnitSet, MajorTimeUnit con MajorTimeUnitSet, MinorTimeUnit con MinorTimeUnitSet, y AutoDateAxis con AutoDateAxisSet. Asignar un valor activa su bandera, limpiar la bandera devuelve el elemento a la omisión, y el escritor XLSX emite un elemento solo cuando su bandera está activa. Note que auto significa detección automática de categoría contra fecha, no unidades de ticks 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; // escribe c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // también activa BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 marca majorUnit como fijado
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // grados; guardado como 1/60000
// AutoDateAxis se deja intacto: no se escribe ningún elemento c:auto
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Las etiquetas y la orientación siguen las mismas reglas conscientes de presencia. El numFmt del eje va después del título y antes de las marcas de tick, su atributo sourceLinked viene true por defecto, y TXLSXChartAxis guarda el código de formato, el valor de source-linked y NumberFormatSet por separado, así que un código vacío nunca se confunde con un elemento ausente. La rotación de texto se guarda en 1/60000 de grado con valores positivos significando sentido horario; TextStyle.Rotation toma grados comunes, la salida SVG usa el ángulo tal cual, y el backend paginado, cuyos ángulos positivos corren en sentido antihorario, voltea el signo en una única frontera compartida. Un eje maxMin invertido refleja junto las marcas de tick mayores y menores, las líneas de cuadrícula, los puntos de datos, las líneas de tendencia y las barras de error, y jamás solo las etiquetas. Cuando usted abre un libro escrito por otro, las banderas le dicen qué especificó realmente el autor
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; // guárdelo de nuevo y los elementos omitidos siguen omitidos
end;
end;
¿Hasta dónde llega el modelo de calendario?
El modelo de calendario se detiene en los formatos que no pueden expresar unidades de calendario, y HotXLS no finge lo contrario. ODF 1.2 ofrece chart:interval-major, que es un número común sin noción de días, meses o años, así que un eje mensual de Excel guardado como ODS no puede cargar su unidad y HotXLS no inventa un atributo no estándar para simularla. Del lado BIFF8 heredado, el registro AxcExt contiene nueve palabras fijas de 16 bits, y sus banderas automáticas solo enmascaran campos en vez de eliminarlos; HotXLS conserva el mínimo, máximo, intervalo, unidad y valor de cruce enmascarados al leer y al reescribir, mantiene intactos los códigos de unidad desconocidos en el modelo Classic, y mapea solo las unidades de fecha válidas 0, 1 y 2 (días, meses, años) a XLSX
Los ejes de fechas son una capa de un modelo de gráficos que además tiene que sobrevivir a archivos que él no creó. Cuando un libro de Excel combina una línea con eje de fechas y una serie de columnas en un eje secundario, nuestro artículo sobre ChartML preservado y gráficos combinados muestra cómo HotXLS reinterpreta el XML del gráfico original byte a byte cuando nada cambió y fusiona ediciones tipadas en él cuando algo cambió, y nuestra visión general de gráficos, imágenes y drawings cubre las APIs de anclaje y de series usadas acá. Todo esto viene en el HotXLS Delphi Component para Delphi y C++Builder, que lee, escribe y renderiza gráficos XLS y XLSX sin automatización de Excel