Artikel Teknis

Menggambar Grafik di PDF dengan Primitif HotPDF

HotPDF tidak menyertakan komponen grafik khusus, tetapi primitif drawing vektor yang disediakannya: path, persegi panjang, lingkaran, teks, warna, dan lebar garis, sepenuhnya cukup untuk membangun grafik yang bersih dan dapat diskalakan langsung di PDF. Pendekatan ini memberikan kontrol penuh atas setiap piksel tanpa ketergantungan pada library grafik eksternal

HotPDF tidak memiliki objek chart siap pakai, sehingga layout dibangun dari primitive vektor. Bar chart menjadi perulangan atas persegi panjang, line chart menjadi polyline, dan pie chart menjadi irisan berbasis sudut. Hasilnya tetap tajam saat dicetak karena bar dan sumbu adalah operator path PDF, bukan piksel layar

Record TPlotArea

Berguna untuk mendefinisikan record yang menyimpan geometri area plot sebagai satu unit, lalu meneruskannya ke prosedur drawing:

type
  TPlotArea = record
    X, Y: Double;           // sudut kiri-bawah area plot
    Width, Height: Double;  // dimensi dalam poin
    MinValue, MaxValue: Double; // rentang nilai data
  end;

Fungsi ValueToY

Semua jenis grafik membutuhkan pemetaan dari nilai data ke koordinat halaman. Fungsi berikut melakukan normalisasi linier:

function ValueToY(const Plot: TPlotArea; Value: Double): Double;
begin
  Result := Plot.Y + ((Value - Plot.MinValue) /
    (Plot.MaxValue - Plot.MinValue)) * Plot.Height;
end;

Dengan MinValue = 0 dan MaxValue = 100, nilai 50 memetakan ke tengah-tengah area plot secara vertikal. Fungsi ini mengasumsikan Y bertambah ke atas (konvensi koordinat HotPDF)

DrawBarChart

procedure DrawBarChart(
  PDF: THotPDF;
  const Plot: TPlotArea;
  const Values: array of Double;
  const Labels: array of string);
var
  I: Integer;
  BarWidth, BarX, BarY, BarH: Double;
  Page: THPDFPage;
begin
  Page := PDF.CurrentPage;
  BarWidth := Plot.Width / Length(Values) * 0.6;

  for I := 0 to High(Values) do
  begin
    BarX := Plot.X + (Plot.Width / Length(Values)) * I +
            (Plot.Width / Length(Values)) * 0.2;
    BarY := Plot.Y;
    BarH := ValueToY(Plot, Values[I]) - Plot.Y;

    Page.SetRGBFillColor(0.2, 0.4, 0.8);
    Page.Rectangle(BarX, BarY, BarWidth, BarH);
    Page.Fill;

    // label
    Page.SetFont('Arial', [], 8);
    Page.TextOut(BarX, Plot.Y - 12, 0, Labels[I]);
  end;
end;

DrawValueAxis

procedure DrawValueAxis(PDF: THotPDF; const Plot: TPlotArea; Steps: Integer);
var
  I: Integer;
  StepValue, YPos: Double;
  Page: THPDFPage;
begin
  Page := PDF.CurrentPage;
  Page.SetRGBStrokeColor(0.5, 0.5, 0.5);
  Page.SetLineWidth(0.5);

  for I := 0 to Steps do
  begin
    StepValue := Plot.MinValue +
      (Plot.MaxValue - Plot.MinValue) / Steps * I;
    YPos := ValueToY(Plot, StepValue);

    // garis grid horizontal
    Page.MoveTo(Plot.X, YPos);
    Page.LineTo(Plot.X + Plot.Width, YPos);
    Page.Stroke;

    // label sumbu
    Page.SetFont('Arial', [], 7);
    Page.TextOut(Plot.X - 30, YPos - 3, 0, FormatFloat('0.#', StepValue));
  end;
end;

DrawLineChart

procedure DrawLineChart(
  PDF: THotPDF;
  const Plot: TPlotArea;
  const Values: array of Double);
var
  I: Integer;
  XStep, PX, PY: Double;
  Page: THPDFPage;
begin
  Page := PDF.CurrentPage;
  Page.SetRGBStrokeColor(0.8, 0.2, 0.2);
  Page.SetLineWidth(1.5);
  XStep := Plot.Width / (Length(Values) - 1);

  for I := 0 to High(Values) do
  begin
    PX := Plot.X + XStep * I;
    PY := ValueToY(Plot, Values[I]);
    if I = 0 then
      Page.MoveTo(PX, PY)
    else
      Page.LineTo(PX, PY);
  end;
  Page.Stroke;
end;

DrawPieChart

Grafik lingkaran memerlukan pengiriman setiap irisan sebagai path kurva Bezier yang mendekati busur. Pendekatan yang lebih sederhana adalah menggunakan beberapa panggilan LineTo untuk mendekati busur dengan polygon segmen banyak:

procedure DrawPieChart(
  PDF: THotPDF;
  CX, CY, Radius: Double;
  const Values: array of Double);
const
  Colors: array[0..4] of record R, G, B: Double end = (
    (R: 0.2; G: 0.4; B: 0.8),
    (R: 0.8; G: 0.2; B: 0.2),
    (R: 0.2; G: 0.7; B: 0.3),
    (R: 0.9; G: 0.7; B: 0.1),
    (R: 0.6; G: 0.2; B: 0.8)
  );
var
  I, J, Segments: Integer;
  Total, StartAngle, EndAngle, Angle, Fraction: Double;
  Page: THPDFPage;
begin
  Page := PDF.CurrentPage;
  Total := 0;
  for I := 0 to High(Values) do
    Total := Total + Values[I];

  StartAngle := 0;
  Segments := 36; // 36 segmen per setengah lingkaran untuk perkiraan yang mulus

  for I := 0 to High(Values) do
  begin
    Fraction := Values[I] / Total;
    EndAngle := StartAngle + Fraction * 2 * Pi;

    Page.SetRGBFillColor(
      Colors[I mod 5].R,
      Colors[I mod 5].G,
      Colors[I mod 5].B);
    Page.MoveTo(CX, CY);
    for J := 0 to Segments do
    begin
      Angle := StartAngle + (EndAngle - StartAngle) * J / Segments;
      Page.LineTo(CX + Cos(Angle) * Radius, CY + Sin(Angle) * Radius);
    end;
    Page.ClosePath;
    Page.Fill;
    StartAngle := EndAngle;
  end;
end;

Menyusun grafik ke halaman

var
  Plot: TPlotArea;
  Data: array of Double;
  Labels: array of string;
begin
  Plot.X := 72; Plot.Y := 300;
  Plot.Width := 400; Plot.Height := 250;
  Plot.MinValue := 0; Plot.MaxValue := 100;

  Data := [45, 72, 38, 91, 60];
  Labels := ['Jan', 'Feb', 'Mar', 'Apr', 'May'];

  DrawValueAxis(PDF, Plot, 5);
  DrawBarChart(PDF, Plot, Data, Labels);
end;

Setiap prosedur dapat dikombinasikan atau dilapisi. Grafik vektor yang dihasilkan diskalakan dengan sempurna ke resolusi printer apa pun karena disimpan sebagai instruksi path PDF, bukan raster bitmap