O HotPDF não possui um objeto de gráfico. Não há TPDFChart, nem AddBarSeries, nada que pegue um array de números e devolva um gráfico renderizado. O que ele lhe dá, em vez disso, é uma tela de página com o mesmo vocabulário de baixo nível que todo modelo de desenho em PDF usa: retângulos, linhas, círculos, preenchimentos, traços e texto colocados em coordenadas exatas. Um gráfico em um documento HotPDF é, portanto, algo que você constrói, não algo que você solicita. Isso soa como mais trabalho do que realmente é. Depois que você escreve a matemática das coordenadas uma vez, um gráfico de barras é um loop sobre retângulos, um gráfico de linhas é uma polilinha e um gráfico de pizza é um leque de arcos, e você controla cada pixel do resultado
Isso é importante porque a alternativa que as pessoas buscam primeiro, rasterizar um controle de gráfico de tela para um bitmap e colar a imagem na página, dá a você um gráfico preso à resolução da tela, que imprime de forma confusa e incha o arquivo. Desenhar o gráfico com as primitivas vetoriais do HotPDF mantém a saída nítida em qualquer zoom e em qualquer DPI de impressão, porque as barras e os eixos são operadores de caminho em PDF reais, não pixels. O custo é que você é o dono do layout. A mecânica se resume a alguns movimentos: a única inversão de coordenadas que pega todo mundo, um gráfico de barras trabalhado, o truque da polilinha para gráficos de linhas e a matemática de arcos para as fatias de pizza
A única parte difícil é o sistema de coordenadas
Os gráficos de tela colocam a origem no canto superior esquerdo com Y crescendo para baixo. O PDF faz o oposto. A origem fica no canto inferior esquerdo da página e Y cresce para cima, medido em pontos (1/72 de polegada). Todas as chamadas de desenho no HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, usam essa convenção inferior esquerda, com Y para cima. Se você trouxer instintos de gráficos de tela, seu primeiro gráfico desenha de cabeça para baixo e foge pela parte inferior da página
Portanto, o verdadeiro trabalho em qualquer gráfico é um mapeamento: transforme um valor de dados em uma coordenada Y que respeite Y para cima. Decida sobre um retângulo de plotagem, quatro números para a área em que o gráfico vive e, em seguida, mapeie o menor valor em seus dados para a borda inferior e o maior para a superior. Para uma barra de valor V em uma escala que vai de 0 a MaxValue, a borda superior da barra é PlotBottom + (V / MaxValue) * PlotHeight, e a barra cresce para cima a partir do PlotBottom. Acerte essa única expressão e todo o resto será contabilidade. O auxiliar abaixo contém a geometria de plotagem e faz a conversão, portanto, o código de desenho nunca toca na aritmética bruta duas vezes:
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;
Uma decisão de julgamento está escondida em MaxValue. Se você defini-lo para o maior ponto de dados exato, a barra mais alta tocará a borda superior do gráfico e parecerá cortada. Arredonde-o para um número limpo acima do máximo, digamos, o próximo múltiplo de 10 ou 100, para que o gráfico tenha espaço livre e os rótulos da linha de grade sejam lidos como números redondos em vez do pico em que os dados acidentalmente se situaram
Um gráfico de barras é um loop sobre retângulos
Com o mapeamento definido, um gráfico de barras se escreve sozinho. Divida a largura do gráfico em um espaço por categoria, deixe um vão entre as barras para que elas não se toquem e desenhe cada barra como um retângulo preenchido, cuja altura venha de ValueToY. O Rectangle do HotPDF pega o canto inferior esquerdo mais uma largura e uma altura, o que se alinha perfeitamente com uma barra que cresce para cima a partir da linha de base. Defina a cor de preenchimento primeiro, estabeleça o caminho e, em seguida, chame Fill para pintá-lo. O rótulo da categoria fica abaixo da linha de base, o valor, acima da 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;
Dois detalhes justificam sua presença. O Gap é uma fração do espaço, em vez de uma contagem fixa de pontos, para que as barras fiquem proporcionalmente espaçadas, independentemente de você traçar quatro categorias ou quarenta. E o rótulo de valor é posicionado com a mesma altura derivada de ValueToY que a barra usa, portanto, ele sempre fica um pouco acima de sua própria barra, em vez de flutuar em um deslocamento adivinhado. Se você quiser linhas de grade horizontais atrás das barras, desenhe-as antes do loop: escolha três ou quatro valores redondos, execute ValueToY em cada um e trace uma linha tênue no gráfico nesse Y. Desenhá-los primeiro os coloca atrás das barras no empilhamento do modelo de pintor que o PDF usa
Eixos, marcas e rótulos são apenas mais linhas e texto
O gráfico não estará concluído até que um leitor possa dizer o que as barras significam, e isso é inteiramente trabalho de eixo. O eixo vertical é uma linha traçada na borda esquerda do gráfico com algumas marcas de graduação (ticks) e seus valores. Reutilize ValueToY para que os ticks fiquem na mesma escala que as barras usam, caso contrário, uma barra e sua linha de grade divergem e o gráfico mente silenciosamente:
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;
Rótulos são onde os gráficos geralmente quebram em produção, e a falha é sempre a mesma: texto que se ajusta na tela ultrapassa seu espaço no PDF. Nomes de categoria longos colidem com os vizinhos e nomes de meses localizados, como "septembre" ou "Dezember", são mais largos que o inglês "Sep" com o qual você testou. Não há nenhum dimensionamento automático para resgatá-lo aqui, então deixe uma margem real sob a linha de base, diminua a fonte em um ou dois pontos para conjuntos de categorias densos e, se os nomes forem genuinamente longos, gire-os. O TextOut recebe um ângulo como seu terceiro argumento, então passar 90 coloca o rótulo na vertical e lhe dá espaço sem sobreposição. Teste o layout com o seu rótulo esperado mais largo, não o mais curto, antes de enviar a exportação
Gráficos de linhas: uma polilinha através de pontos mapeados
Um gráfico de linhas reutiliza todo o mapeamento de valor e muda apenas a forma como os pontos se conectam. Em vez de um retângulo por categoria, você percorre os dados uma vez, converte cada valor para o seu (X, Y) com ValueToY e costura os pontos com uma única chamada de MoveTo, seguida por chamadas de LineTo, tracejadas no final. O primeiro ponto abre o caminho; todo ponto posterior o estende:
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;
Observe a diferença de espaçamento. Um gráfico de barras divide a largura pelo número de barras, porque cada barra possui um espaço. Um gráfico de linhas divide pelo número de intervalos, Count - 1, porque o primeiro e o último ponto se situam nas bordas do gráfico e a linha abrange as lacunas entre eles. Confundir esses dois é a razão usual de um gráfico de linhas se desviar de meio espaço em relação ao gráfico de barras ao qual ele se destina sobrepor. Se você quiser um marcador em cada ponto de dados, solte um pequeno Circle e dê um Fill em cada (X, Y) após a polilinha ser traçada
Gráficos de pizza: arcos ou fatias, se você mantiver isso simples
Fatias de pizza são a única forma que precisa de trigonometria, porque um setor circular é limitado por dois raios e um arco. A versão honesta desenha o arco pisando pequenos segmentos de linha ao longo da circunferência, o que se aproxima muito da curva para que nenhum leitor perceba. O ângulo de varredura de cada fatia é a sua parte do total, (Value / Total) * 2π, e você acumula o ângulo corrido à medida que avança:
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;
O caminho de dentro para fora, o ápice primeiro, depois o arco e depois de volta ao ápice, dá a você um setor fechado que Fill pinta de forma sólida. A contagem de segmentos troca suavidade por tamanho de caminho: aproximadamente dois graus por passo parece redondo em qualquer raio sensato, sem produzir um fluxo de conteúdo enorme. Se você não precisar de um círculo verdadeiro, poderá ignorar a trigonometria inteiramente e renderizar os mesmos dados como uma única barra horizontal empilhada, a largura de cada segmento proporcional à sua parte. Isso geralmente é mais legível do que uma pizza de qualquer maneira, e é apenas o código de barra com os retângulos colocados de ponta a ponta. Recorra à versão em arco somente quando o design exigir um círculo real
Nada disso depende da instalação de nenhuma biblioteca de gráficos, o que é a vantagem silenciosa de desenhar as primitivas diretamente. As mesmas chamadas de desenho em tela que colocam um logotipo ou uma caixa de assinatura constroem esses gráficos, e o mesmo TextOut que rotula um campo de formulário rotula um eixo. Coloque a geometria do gráfico em um record, mapeie valores para Y uma vez e um gráfico de barras, de linhas ou de pizza passa a ser uma rotina curta sobre Rectangle, LineTo e Circle que você pode colocar em qualquer relatório. As chamadas Rectangle, MoveTo, LineTo, Circle, Fill, Stroke e TextOut usadas aqui fazem parte do HotPDF Component para Delphi e C++Builder