Technical Article

Crtanje grafikona u PDF-u pomoću HotPDF primitiva (Delphi)

PDF izveštaju je često potreban grafikon, a ako pregledate HotPDF paletu u potrazi za komponentom grafikona, nećete je naći. U biblioteci ne postoji objekat grafikona. Da biste postavili stubičasti, linijski ili kružni grafikon na stranicu, crtate ga sami koristeći vektorske primitive: Rectangle, LineTo i Circle. Iako to zvuči kao više posla nego postavljanje gotove komponente, pruža vam potpunu kontrolu nad veličinom fajla i renderovanjem. Eksterni motori za grafikone često izvoze tako što grafikon pretvore u rastersku sliku, što naduvava PDF i pikselizuje se pri štampanju. Crtanje pomoću primitiva proizvodi čistu vektorsku akustiku koja se skalira beskonačno i zauzima samo nekoliko kilobajta. Trud se ne ulaže u same pozive za crtanje, već u koordinatnu matematiku koja mapira vaše tačke podataka na stranicu.

Ovaj članak pokriva to koordinatno mapiranje i pruža kod za crtanje tri najčešća tipa grafikona.

Matematika mreže: mapiranje podataka u tačke

Pre nego što nacrtate ijednu liniju, morate definisati uokvirujući kvadrat (bounding box) na stranici gde će grafikon živeti, i izračunati faktore skaliranja za mapiranje vrednosti vaših podataka u tačke. Koordinatni sistem, kao što je objašnjeno u vodiču za crtanje po kanvasu, postavlja (0,0) u donji levi ugao stranice.

Za oblast grafikona definisanu sa Left, Bottom, Width i Height, prostor podataka se mapira na sledeći način:

  • X se mapira iz indeksa podataka (0 do N-1) u tačke: X_point = Left + (Index * (Width / (N - 1)))
  • Y se mapira iz vrednosti podataka (MinVal do MaxVal) u tačke: Y_point = Bottom + ((Val - MinVal) * (Height / (MaxVal - MinVal)))

Korišćenje namenske pomoćne funkcije za ovo mapiranje održava vaše petlje za crtanje čitljivim.

1. Crtanje stubičastog grafikona

Stubičasti grafikon predstavlja vrednosti podataka kao vertikalne stubce. Matematika mora izračunati širinu svakog stubca i razmak između njih (gap) tako da se tačno uklapaju u širinu grafikona.

procedure DrawBarChart(Pdf: THotPDF; const Data: array of Double;
  const Left, Bottom, Width, Height: Double);
var
  I: Integer;
  BarWidth, Gap, MaxVal, ValHeight: Double;
begin
  MaxVal := 100.0; // scale against a known maximum
  Gap := 10.0;     // spacing between bars
  BarWidth := (Width - (Gap * (Length(Data) + 1))) / Length(Data);

  Pdf.CurrentPage.FillColor := RGB(70, 130, 180); // steel blue
  Pdf.CurrentPage.StrokeColor := clBlack;
  Pdf.CurrentPage.LineWidth := 1;

  for I := 0 to High(Data) do
  begin
    ValHeight := (Data[I] / MaxVal) * Height;
    // X = Left + Gap + spacing for previous bars
    Pdf.CurrentPage.Rectangle(
      Left + Gap + (I * (BarWidth + Gap)),
      Bottom,
      BarWidth,
      ValHeight
    );
    Pdf.CurrentPage.FillAndStroke;
  end;
end;

Primetite kako se visina svakog stubca računa u odnosu na maksimalnu vrednost u skupu podataka, a pravougaonik se crta koristeći stanje popunjavanja i konture (fill-and-stroke) tako da ima crnu ivicu i plavu unutrašnjost.

2. Crtanje linijskog grafikona

Linijski grafikon povezuje tačke podataka pravim linijama. Koristan je za prikazivanje trendova tokom vremena. Matematika je slična kao kod stubičastog grafikona, ali umesto crtanja pravougaonika, pomerate se na prvu tačku i crtate linije do svake sledeće tačke.

procedure DrawLineChart(Pdf: THotPDF; const Data: array of Double;
  const Left, Bottom, Width, Height: Double);
var
  I: Integer;
  X, Y, Step, MaxVal: Double;
begin
  MaxVal := 100.0;
  Step := Width / High(Data);

  Pdf.CurrentPage.StrokeColor := RGB(220, 20, 60); // crimson red
  Pdf.CurrentPage.LineWidth := 2;

  // Move to the first data point
  Y := Bottom + ((Data[0] / MaxVal) * Height);
  Pdf.CurrentPage.MoveTo(Left, Y);

  // Draw lines to subsequent points
  for I := 1 to High(Data) do
  begin
    X := Left + (I * Step);
    Y := Bottom + ((Data[I] / MaxVal) * Height);
    Pdf.CurrentPage.LineTo(X, Y);
  end;
  Pdf.CurrentPage.Stroke;

  // Optional: Draw markers at each point
  Pdf.CurrentPage.FillColor := clBlack;
  for I := 0 to High(Data) do
  begin
    X := Left + (I * Step);
    Y := Bottom + ((Data[I] / MaxVal) * Height);
    Pdf.CurrentPage.Circle(X, Y, 3);
    Pdf.CurrentPage.Fill;
  end;
end;

LineWidth je podešen na 2 tačke kako bi se linija istakla, a mali krugovi se crtaju na svakoj tački podataka da označe vrednosti. Putanja se iscrtava u jednom neprekidnom pozivu Stroke.

3. Crtanje kružnog grafikona

Kružni grafikon (pita) prikazuje udele u celini. Umesto mapiranja koordinata na mrežu, procente podataka mapirate u uglove (od 0 do 360 stepeni). HotPDF crta lukove i isečke kruga koristeći metode za segmente kruga (Circle).

procedure DrawPieChart(Pdf: THotPDF; const Proportions: array of Double;
  const CX, CY, Radius: Double);
var
  I: Integer;
  Total, CurrentAngle, SliceAngle: Double;
  Colors: array[0..3] of TColor;
begin
  Colors[0] := RGB(70, 130, 180);
  Colors[1] := RGB(220, 20, 60);
  Colors[2] := RGB(46, 139, 87);
  Colors[3] := RGB(218, 165, 32);

  Total := 0.0;
  for I := 0 to High(Proportions) do
    Total := Total + Proportions[I];

  CurrentAngle := 0.0;
  Pdf.CurrentPage.StrokeColor := clBlack;
  Pdf.CurrentPage.LineWidth := 1;

  for I := 0 to High(Proportions) do
  begin
    SliceAngle := (Proportions[I] / Total) * 360.0;
    Pdf.CurrentPage.FillColor := Colors[I mod 4];
    
    // CX, CY, Radius, StartAngle, EndAngle
    Pdf.CurrentPage.DrawPieSlice(
      CX, CY, Radius,
      CurrentAngle,
      CurrentAngle + SliceAngle
    );
    
    CurrentAngle := CurrentAngle + SliceAngle;
  end;
end;

Isečci pite se crtaju pomoću metode DrawPieSlice. Uglovi se mere u stepenima, počevši sa desne strane (pozicija 3 sata) i krećući se u smeru suprotnom od kretanja kazaljke na satu. Morate pratiti StartAngle i dodavati ugao svakog isečka kako biste izračunali sledeći početni ugao.

Metode za vektorske primitive, boje i fontove prikazane ovde deo su standardnog HotPDF Component API-ja za Delphi i C++Builder.