Un gráfico de balances de fin de mes que arranca el 31 de enero de 2026 debería poner su tick de abril el 30 de abril. Avance cada tick a partir del anterior con meses de calendario y las etiquetas salen 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28, con cada tick posterior clavado en el 28. Avance en cambio por 30 días planos y el tick de febrero cae el 2 de marzo, mientras que el de diciembre cae el 27 de diciembre. La regla que sobrevive a ambas trampas es fácil de enunciar: calcule cada tick desde el mínimo del eje, y ajuste los fines de mes al calendario real
En un gráfico de cierre de trimestre el primer error es el peligroso, porque la imagen todavía se ve bien. La línea sube donde debe, las etiquetas están espaciadas parejo, y el balance de marzo queda tres días antes del fin de trimestre. Quien lo encuentra es el controller que compara el gráfico con el mayor, y para entonces el gráfico normalmente ya fue pegado en un paquete de directorio
¿Por qué se desvían los ticks de mes cuando se acumulan?
Los ticks de mes se desvían porque el ajuste tira información. Una vez que el 31 de enero se ajustó al 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. En HotXLS, TXLSChartDateAxisTransform.BuildTicks en la unidad lxChart nunca avanza desde un tick anterior: el tick i es siempre AddUnits(MinValue, UnitKind, Step * i, ...), así que marzo es dos meses después del 31 de enero y sale 31 de marzo. AddUnits suma el conteo directo al serial para unidades de día; para unidades de mes y año convierte el serial a una fecha real, llama a IncMonth o IncYear, y convierte de vuelta
El bucle está acotado por todos lados. El arreglo de resultado nunca pasa de 4096 entradas sea lo que sea que quien llama pase como máximo, Step * i se verifica contra MaxInt antes de multiplicar, y la generación se detiene en cuanto un tick deja de crecer o pasa el máximo del eje. Un libro hostil con un tramo de unos cuantos millones de años produce un arreglo corto y regresa
La transformación es pública, así que puede correr la aritmética usted mismo antes de que exista cualquier gráfico. Este fragmento de consola construye un año de ticks mensuales para la serie de arriba e imprime las etiquetas que el gráfico usará
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 exactamente una aproximación en BuildTicks, y está donde una aproximación es inofensiva. Cuando el paso pedido es cero, el método estima un paso tratando el mes como 30 días y el año como 365, y luego redondea el paso crudo hacia arriba a un múltiplo 1, 2 o 5 de una potencia de diez. Esa estimación decide solo cuántas unidades hay entre ticks. Las posiciones siguen saliendo de IncMonth y IncYear. Una longitud de mes aproximada puede hacer el eje un poco más denso o más ralo, pero cada tick cae en una fecha que una de esas dos funciones produjo
Los seriales del libro se quedan como coordenada del eje
Un eje de fechas de HotXLS conserva los seriales del libro como su espacio de coordenadas porque el TDateTime de Delphi no tiene ranura para el serial 60. En el sistema de fechas 1900 ese serial es el 29 de febrero de 1900, un día que nunca existió y que Excel conserva por compatibilidad con programas de hojas de cálculo más viejos. TXLSChartDateAxisTransform.TrySerialToDateTime lo maneja de la única manera que un calendario real puede: los seriales por debajo de 60 se corren un día y el serial 60 se pliega sobre el 28 de febrero. Para etiquetar un punto suelto está bien. Para la geometría está mal, porque los seriales 59 y 60 se vuelven el mismo TDateTime, y el 28 de febrero y el 1 de marzo acaban a una unidad de distancia donde Excel los dibuja a dos
Tres seriales muestran ambas caras del problema. FormatValue trata la ranura fantasma como caso especial e imprime exactamente lo que Excel imprime, mientras que un viaje de ida y vuelta por TDateTime la pierde
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 etiqueta del eje: 1900-02-28 TDateTime: 1900-02-28
// 60 etiqueta del eje: 1900-02-29 TDateTime: 1900-02-28
// 61 etiqueta del eje: 1900-03-01 TDateTime: 1900-03-01
end;
Casi nadie grafica datos de principios de 1900, así que la lección práctica es sobre su propio código. Si calcula límites de eje o posiciones de anotaciones, hágalo en seriales y convierta con DateTimeToSerial en el borde, como hace el primer fragmento. Mezcle un eje basado en seriales con aritmética TDateTime en quien llama, y una anotación fechada antes del 1 de marzo de 1900 aterriza a un día de su punto. La segunda época importa más a menudo: 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 cuando el argumento Dates1904 es verdadero. Ambas épocas, y cómo interactúan con ellas los formatos numéricos de celda, están cubiertas 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. Descarta los valores NaN e infinitos, ordena una copia del resto una sola vez, y se 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, meses; y si no, años. La prueba de mes es IncMonth(Previous, 1) > Current, y esa elección importa para exactamente los datos de este artículo. Un umbral de 30 días llamaría al hueco del 31 de enero al 28 de febrero "menos de un mes" y cambiaría una serie mensual a días. Con menos de dos fechas válidas distintas no hay hueco que medir, y el método devuelve días
ResolveUnits fusiona la unidad detectada con lo que el gráfico declare. Una unidad base explícita manda. Una unidad mayor o menor ausente hereda la unidad detectada cuando esta es más gruesa que la base, y si no cae de vuelta a la base. La última regla es la que la gente pasa por alto: si la unidad mayor o menor efectiva es más fina que la base, la base baja para emparejar. La unidad base decide cómo se agrupan los puntos antes de que exista cualquier tick, porque Normalize ajusta un valor a su día, al primero de su mes o al 1 de enero. Una base mensual bajo una unidad mayor diaria plegaría un mes entero de puntos en una sola ranura antes de que el código de ticks los viera
Cuántos ticks mayores automáticos aparecen depende de más que el rango de fechas. El largo del área de trazado, el ancho medido de la fuente del eje, el formato numérico de las etiquetas y el ancho proyectado de las etiquetas rotadas acotan todos el conteo, así que un gráfico angosto con etiquetas largas mmmm yyyy recibe menos ticks que uno ancho con mm/yy. Las etiquetas, las marcas de tick y las líneas de cuadrícula se dibujan todas desde un único arreglo de ticks, lo que mantiene de acuerdo la salida HTML, SVG y PDF, y un tick menor que cae sobre uno mayor se dibuja 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 dice algo distinto de escribir su valor por defecto. El tipo CT_DateAx en ECMA-376 Part 1, §21.2 (DrawingML Charts) deja que baseTimeUnit, majorTimeUnit, minorTimeUnit y auto aparezcan o no. Un baseTimeUnit omitido le dice a Excel que decida cuando renderice; un days explícito lo fija. TXLSXChartAxis guarda 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, poner la bandera en False devuelve el elemento a la omisión, y el escritor XLSX emite un elemento solo cuando su bandera está activa. auto es fácil de malinterpretar: controla la detección automática de categoría contra fecha y no tiene nada que ver con unidades de ticks automáticas
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; // escribe c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // también activa BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // un valor > 0 marca majorUnit como fijado
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // grados
// AutoDateAxis se deja intacto: no se escribe ningún elemento c:auto
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
La unidad base aquí es días a propósito. Las fechas de fin de mes caen en números de día distintos, y una base mensual ajustaría cada punto al primero de su mes, lo que movería el balance del 31 de marzo al 1 de marzo. Fije la base a días siempre que el día exacto carry significado, y deje que la unidad mayor haga el espaciado mensual
Las etiquetas y la orientación siguen las mismas reglas de presencia. El elemento numFmt del eje deja sourceLinked en true por defecto, así que TXLSXChartAxis guarda por separado el código de formato, el valor de source-linked y NumberFormatSet, y un código vacío nunca se confunde con un elemento ausente. ChartML guarda la rotación de texto en 1/60000 de grado con valores positivos corriendo en sentido horario; TextStyle.Rotation toma grados comunes, y una etiqueta puesta en -45 se inclina igual en la salida SVG y PDF. Un eje invertido (maxMin) refleja juntos los ticks mayores y menores, las líneas de cuadrícula, los puntos de datos, las líneas de tendencia y las barras de error. La lectura funciona al revés: abra un libro escrito por otro y las banderas le dicen qué especificó realmente su 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[1].Charts[0].CategoryAxis; // Sheets[] es base 1, Charts[] es 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; // guárdelo de nuevo y los elementos omitidos siguen omitidos
end;
end;
Unidades de calendario en archivos ODS y XLS heredados
El modelo de calendario llega hasta donde cada formato de archivo puede expresarlo. ODF 1.2 describe el espaciado del eje con chart:interval-major, 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. HotXLS deja la unidad fuera del archivo ODS en lugar de inventar un atributo no estándar que las demás aplicaciones ODF ignorarían de todos modos. Si el espaciado mensual le importa a quienes abren el archivo, distribuya XLSX
Los libros BIFF8 guardan la configuración del eje de fechas en el record AxcExt, nueve palabras fijas de 16 bits. Sus banderas automáticas enmascaran campos sin eliminarlos, así que un record marcado "mínimo automático" todavía carga un valor de mínimo. HotXLS conserva el mínimo, máximo, intervalo, unidad y valor de cruce enmascarados al leer y los reescribe sin cambios, mantiene intactos los códigos de unidad desconocidos en el modelo de objetos XLS, y mapea solo las unidades de fecha válidas 0, 1 y 2 (días, meses, años) cuando un gráfico se convierte a XLSX
Un eje de fechas rara vez viaja solo. 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 explica cómo HotXLS reinterpreta el XML del gráfico original 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 llamadas de anclaje y de series usadas arriba. La página del HotXLS Delphi Component lista el conjunto completo de funcionalidades de gráficos para Delphi y C++Builder, incluida la lectura, escritura y renderización de gráficos XLS y XLSX sin Excel instalado