HotPDF no dispone de ningún objeto de gráfico. No existe TPDFChart, ni AddBarSeries, ni nada que reciba un array de números y devuelva un gráfico renderizado. Lo que ofrece en su lugar es un lienzo de página con el mismo vocabulario de bajo nivel que utiliza cualquier modelo de dibujo PDF: rectángulos, líneas, círculos, rellenos, trazos y texto colocado en coordenadas exactas. Un gráfico en un documento HotPDF es, por tanto, algo que tú construyes, no algo que solicitas. Esto parece más trabajo del que realmente es. Una vez que has escrito las matemáticas de coordenadas, un gráfico de barras es un bucle sobre rectángulos, un gráfico de líneas es una polilínea y un gráfico circular es un abanico de arcos, con control total sobre cada píxel del resultado.
Esto importa porque la alternativa a la que la gente recurre primero, rasterizar un control de gráfico en pantalla a un mapa de bits y pegarlo en la página, produce un gráfico bloqueado a la resolución de pantalla que se imprime con borrosidad y engrosa el fichero. Dibujar el gráfico con las primitivas vectoriales de HotPDF mantiene la salida nítida a cualquier zoom y cualquier DPI de impresión, porque las barras y los ejes son auténticos operadores de trayectoria PDF, no píxeles. El precio es que tú gestionas la maquetación. La mecánica se reduce a unos pocos pasos: el único giro de coordenadas que confunde a todos, un gráfico de barras elaborado, el truco de la polilínea para gráficos de líneas y las matemáticas de arcos para sectores circulares.
La única parte difícil es el sistema de coordenadas
Los gráficos en pantalla sitúan el origen en la esquina superior izquierda con Y creciendo hacia abajo. PDF hace lo contrario. El origen se encuentra en la esquina inferior izquierda de la página y Y crece hacia arriba, medido en puntos (1/72 de pulgada). Cada llamada de dibujo en HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, utiliza esa convención de origen inferior izquierdo con Y hacia arriba. Si arrastras los instintos de los gráficos en pantalla, tu primer gráfico se dibuja del revés y se sale por la parte inferior de la página.
El trabajo real de cualquier gráfico consiste en una única transformación: convertir un valor de dato en una coordenada Y que respete el sentido Y hacia arriba. Define un rectángulo de trazado, cuatro números para el área donde vive el gráfico, luego mapea el valor más pequeño de tus datos al borde inferior y el más grande al superior. Para una barra de valor V en una escala que va de 0 a MaxValue, el borde superior de la barra es PlotBottom + (V / MaxValue) * PlotHeight, y la barra crece hacia arriba desde PlotBottom. Si obtienes esa expresión correctamente, el resto es contabilidad. El auxiliar de abajo guarda la geometría del área de trazado y realiza la conversión, de modo que el código de dibujo nunca toca aritmética bruta dos veces:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // PDF points, bottom-left origin
MaxValue: Single; // top of the value scale
end;
// Map a data value to its Y coordinate inside the plot, Y growing upward.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Hay una decisión de juicio oculta en MaxValue. Si lo fijas al dato más grande exacto, la barra más alta toca el borde superior del área de trazado y parece recortada. Redondéalo al alza hasta un número limpio por encima del máximo, por ejemplo el siguiente múltiplo de 10 o 100, de modo que el gráfico tenga margen superior y las etiquetas de cuadrícula sean cifras redondas en lugar del valor exacto del pico de los datos.
Un gráfico de barras es un bucle sobre rectángulos
Una vez resuelta la transformación, un gráfico de barras se escribe solo. Divide el ancho del área de trazado en un hueco por categoría, deja un espacio entre barras para que no se toquen, y dibuja cada barra como un rectángulo relleno cuya altura proviene de ValueToY. El método Rectangle de HotPDF toma la esquina inferior izquierda más un ancho y una altura, lo que se ajusta exactamente a una barra que crece hacia arriba desde la línea base. Primero establece el color de relleno, traza el camino y luego llama a Fill para pintarlo. La etiqueta de categoría va por debajo de la línea base y el valor por encima de la barra:
procedure DrawBarChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single; const Labels: array of string);
var
I, Count: Integer;
SlotW, BarW, BarX, BarH, Gap: Single;
begin
Count := Length(Values);
SlotW := Plot.Width / Count;
Gap := SlotW * 0.25; // quarter-slot gap on each side
BarW := SlotW - Gap;
// Baseline (the X axis) along the bottom of the plot.
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left + Plot.Width, Plot.Bottom);
Page.Stroke;
Page.SetFont('Arial', [], 9);
for I := 0 to Count - 1 do
begin
BarX := Plot.Left + I * SlotW + Gap / 2;
BarH := ValueToY(Plot, Values[I]) - Plot.Bottom;
Page.SetRGBFillColor(RGB(56, 110, 219));
Page.Rectangle(BarX, Plot.Bottom, BarW, BarH); // X, Y, Width, Height
Page.Fill;
// Category label below the baseline, value above the bar.
Page.SetRGBFillColor(clBlack);
Page.TextOut(BarX, Plot.Bottom - 14, 0, Labels[I]);
Page.TextOut(BarX, Plot.Bottom + BarH + 4, 0,
FormatFloat('0', Values[I]));
end;
end;
Hay dos detalles que merecen la pena. El Gap es una fracción del hueco en lugar de un número fijo de puntos, de modo que las barras mantienen el espaciado proporcional tanto si trazas cuatro categorías como si trazas cuarenta. La etiqueta de valor se posiciona con la misma altura derivada de ValueToY que usa la barra, por lo que siempre queda justo encima de su propia barra en lugar de flotar a un desplazamiento estimado. Si quieres líneas de cuadrícula horizontales detrás de las barras, dibújalas antes del bucle: elige tres o cuatro valores redondos, aplica ValueToY a cada uno y traza una línea tenue a lo ancho del área de trazado en esa Y. Dibujarlas primero las sitúa detrás de las barras en el apilado de modelo del pintor que usa PDF.
Los ejes, las marcas y las etiquetas son solo más líneas y texto
El gráfico no está terminado hasta que un lector pueda entender qué significan las barras, y eso es todo trabajo de ejes. El eje vertical es una línea trazada hacia arriba por el borde izquierdo del área de trazado con unas pocas marcas de graduación y sus valores. Reutiliza ValueToY para que las marcas caigan en la misma escala que usan las barras; de lo contrario, una barra y su línea de cuadrícula no coinciden y el gráfico miente en silencio:
procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
TickCount: Integer);
var
I: Integer;
TickV, TickY: Single;
begin
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left, Plot.Bottom + Plot.Height);
Page.Stroke;
Page.SetFont('Arial', [], 8);
for I := 0 to TickCount do
begin
TickV := (Plot.MaxValue / TickCount) * I;
TickY := ValueToY(Plot, TickV);
Page.MoveTo(Plot.Left - 4, TickY); // short tick outside the axis
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Las etiquetas son donde los gráficos suelen fallar en producción, y el fallo es siempre el mismo: texto que cabe en pantalla se desborda en el PDF. Los nombres de categoría largos colisionan con sus vecinos, y los nombres de mes localizados como «septembre» o «Dezember» son más anchos que el «Sep» en inglés con el que probaste. No hay ajuste automático de tamaño que te rescate aquí, así que deja un margen real por debajo de la línea base, reduce la fuente un punto o dos para conjuntos de categorías densas, y si los nombres son realmente largos, rótalos. TextOut acepta un ángulo como tercer argumento, de modo que pasar 90 pone la etiqueta en vertical y ganas espacio sin solapamiento. Prueba la maquetación con la etiqueta más ancha que esperas, no con la más corta, antes de que el exportador salga a producción.
Gráficos de líneas: una polilínea por los puntos transformados
Un gráfico de líneas reutiliza toda la transformación de valores y solo cambia cómo se conectan los puntos. En lugar de un rectángulo por categoría, recorres los datos una vez, conviertes cada valor a su (X, Y) con ValueToY y unes los puntos con un único MoveTo seguido de llamadas LineTo, trazadas al final. El primer punto abre el camino; cada punto posterior lo extiende:
procedure DrawLineChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single);
var
I, Count: Integer;
StepX, X, Y: Single;
begin
Count := Length(Values);
if Count < 2 then Exit;
StepX := Plot.Width / (Count - 1);
Page.SetLineWidth(1.5);
Page.SetRGBStrokeColor(RGB(214, 92, 36));
for I := 0 to Count - 1 do
begin
X := Plot.Left + I * StepX;
Y := ValueToY(Plot, Values[I]);
if I = 0 then
Page.MoveTo(X, Y) // open the path at the first point
else
Page.LineTo(X, Y); // extend it through every later point
end;
Page.Stroke; // one stroke paints the whole polyline
end;
Nótese la diferencia de espaciado. Un gráfico de barras divide el ancho por el número de barras, porque cada barra ocupa un hueco. Un gráfico de líneas divide por el número de intervalos, Count - 1, porque el primer y el último punto se sitúan en los bordes del área de trazado y la línea abarca los espacios entre ellos. Confundir estos dos es la razón habitual por la que un gráfico de líneas se desplaza medio hueco respecto al gráfico de barras sobre el que se pretende superponer. Si quieres un marcador en cada punto de dato, añade un pequeño Circle con Fill en cada (X, Y) después de trazar la polilínea.
Gráficos circulares: arcos, o sectores si se simplifica
Los sectores circulares son la única forma que necesita trigonometría, porque un sector está delimitado por dos radios y un arco. La versión correcta barre el arco avanzando con pequeños segmentos de línea a lo largo de la circunferencia, lo que aproxima la curva con suficiente fidelidad para que ningún lector lo note. El ángulo de barrido de cada sector es su proporción del total, (Value / Total) * 2π, y se acumula el ángulo en curso a medida que se avanza:
procedure DrawPieChart(Page: THPDFPage; CX, CY, Radius: Single;
const Values: array of Single; const Colors: array of TColor);
var
I, Step, Steps: Integer;
Total, Start, Sweep, A: Single;
begin
Total := 0;
for I := 0 to High(Values) do Total := Total + Values[I];
Start := 0;
for I := 0 to High(Values) do
begin
Sweep := (Values[I] / Total) * 2 * Pi;
Steps := Round(Sweep / (Pi / 90)) + 1; // ~2 degrees per segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // wedge apex at the center
for Step := 0 to Steps do
begin
A := Start + Sweep * (Step / Steps);
Page.LineTo(CX + Radius * Cos(A), CY + Radius * Sin(A));
end;
Page.LineTo(CX, CY); // close back to the center
Page.Fill;
Start := Start + Sweep; // advance to the next slice
end;
end;
El camino desde el centro hacia afuera, primero el vértice, luego el arco y de vuelta al vértice, produce un sector cerrado que Fill pinta de forma sólida. El número de segmentos intercambia suavidad por tamaño del camino: aproximadamente dos grados por paso parece redondo a cualquier radio razonable sin generar un flujo de contenido enorme. Si no necesitas un círculo verdadero, puedes prescindir de la trigonometría por completo y representar los mismos datos como una única barra apilada horizontal, con el ancho de cada segmento proporcional a su parte. Eso suele ser más legible que un gráfico circular, y no es más que el código de barras con los rectángulos colocados uno a continuación del otro. Recurre a la versión con arcos solo cuando el diseño exija un círculo real.
Nada de esto depende de que haya ninguna biblioteca de gráficos instalada, que es la ventaja silenciosa de dibujar directamente con primitivas. Las mismas llamadas de dibujo en lienzo que colocan un logotipo o un recuadro de firma construyen estos gráficos, y el mismo TextOut que etiqueta un campo de formulario etiqueta un eje. Pon la geometría del área de trazado en un registro, mapea los valores a Y una vez, y un gráfico de barras, de líneas o circular es una rutina breve sobre Rectangle, LineTo y Circle que puedes insertar en cualquier informe. Las llamadas Rectangle, MoveTo, LineTo, Circle, Fill, Stroke y TextOut aquí usadas forman parte del HotPDF Component para Delphi y C++Builder.