Artikel Teknis

Menggambar Bagan dalam PDF dengan Primitif HotPDF (Delphi)

HotPDF tidak memiliki objek bagan. Tidak ada TPDFChart, tidak ada AddBarSeries, tidak ada yang mengambil array angka dan mengembalikan grafik yang sudah dirender. Yang diberikannya sebagai gantinya adalah kanvas halaman dengan kosakata tingkat rendah yang sama yang digunakan setiap model gambar PDF: persegi panjang, garis, lingkaran, isian, garis luar, dan teks yang ditempatkan pada koordinat yang tepat. Bagan dalam dokumen HotPDF karena itu adalah sesuatu yang Anda bangun, bukan sesuatu yang Anda minta. Itu terdengar seperti lebih banyak pekerjaan dari yang sebenarnya. Setelah Anda menulis matematika koordinatnya sekali, bagan batang adalah loop atas persegi panjang, bagan garis adalah polyline, dan bagan pai adalah kipas busur, dan Anda mengontrol setiap piksel hasilnya

Ini penting karena alternatif yang pertama kali dijangkau orang, meraster kontrol bagan layar ke bitmap dan menempelkan gambar ke halaman, memberi Anda bagan yang terkunci ke resolusi layar yang tercetak kabur dan membengkakkan file. Menggambar bagan dengan primitif vektor HotPDF menjaga keluaran tetap tajam pada zoom dan DPI cetak apa pun, karena batang dan sumbunya adalah operator jalur PDF yang nyata, bukan piksel. Harganya adalah Anda memiliki tata letaknya. Mekaniknya menyempit pada beberapa langkah: satu pembalikan koordinat yang menjebak semua orang, bagan batang yang sudah dikerjakan, trik polyline untuk bagan garis, dan matematika busur untuk irisan pai

Satu-satunya bagian sulit adalah sistem koordinat

Grafik layar menempatkan origin di kiri-atas dengan Y tumbuh ke bawah. PDF melakukan sebaliknya. Origin berada di sudut kiri-bawah halaman dan Y tumbuh ke atas, diukur dalam poin (1/72 inci). Setiap panggilan gambar di HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, menggunakan konvensi kiri-bawah, Y-naik itu. Jika Anda membawa insting grafik-layar, bagan pertama Anda tergambar terbalik dan keluar dari bagian bawah halaman

Jadi pekerjaan nyata dalam bagan apa pun adalah satu pemetaan: ubah nilai data menjadi koordinat Y yang menghormati Y-naik. Putuskan sebuah persegi panjang plot, empat angka untuk area tempat bagan berada, lalu petakan nilai terkecil dalam data Anda ke tepi bawah dan yang terbesar ke atas. Untuk batang bernilai V pada skala yang berjalan dari 0 ke MaxValue, tepi atas batang adalah PlotBottom + (V / MaxValue) * PlotHeight, dan batang tumbuh ke atas dari PlotBottom. Dapatkan satu ekspresi itu dengan benar dan selebihnya adalah pembukuan. Pembantu di bawah ini menahan geometri plot dan melakukan konversi, sehingga kode gambar tidak pernah menyentuh aritmetika mentah dua kali:

HotPDF: Koordinat layar menempatkan titik asal di kiri-atas dengan Y bertambah ke bawah sementara koordinat PDF mulai di kiri-bawah dengan Y bertambah ke atas, dan ValueToY memetakan nilai data ke ruang Y-ke-atas itu
PDF membalik bingkai layar yang akrab: origin duduk di kiri-bawah dan Y tumbuh ke atas, dan satu ekspresi ValueToY memetakan setiap nilai data ke ruang itu
type
  TPlotArea = record
    Left, Bottom, Width, Height: Single;  // poin PDF, origin kiri-bawah
    MaxValue: Single;                     // puncak skala nilai
  end;

// Petakan nilai data ke koordinat Y-nya di dalam plot, Y bertambah ke atas.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
  Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;

Satu keputusan penilaian bersembunyi dalam MaxValue. Jika Anda menetapkannya ke titik data terbesar yang persis, batang tertinggi menyentuh tepi atas plot dan terlihat terpotong. Bulatkan ke atas ke angka bersih di atas maksimum, misalnya kelipatan 10 atau 100 berikutnya, agar bagan memiliki ruang kepala dan label garis kisi terbaca sebagai angka bulat daripada berapa pun ke puncaknya data kebetulan

Bagan batang adalah loop atas persegi panjang

Dengan pemetaan diselesaikan, bagan batang menulis sendiri. Bagi lebar plot menjadi satu slot per kategori, tinggalkan celah antar batang agar tidak bersentuhan, dan gambar setiap batang sebagai persegi panjang terisi yang tingginya berasal dari ValueToY. Rectangle HotPDF mengambil sudut kiri-bawah ditambah lebar dan tinggi, yang berbaris persis dengan batang yang tumbuh ke atas dari garis dasar. Setel warna isian dulu, bentangkan jalurnya, lalu panggil Fill untuk mengecatnya. Label kategori berada di bawah garis dasar, nilai di atas batang:

HotPDF: Anatomi bagan batang yang menunjukkan slot yang dibagi dari lebar plot dengan celah antar batang, setiap batang berupa Rectangle terisi yang tumbuh ke atas dari Plot.Bottom, label kategori di bawah garis dasar, label nilai menempel pada tinggi batang, dan tick sumbu memakai ulang ValueToY
Setiap bar adalah satu Rectangle terisi yang tumbuh ke atas dari Plot.Bottom di dalam slot-nya, celah seperempat slot berskala dengan lebar, dan tick sumbu memakai ulang ValueToY sehingga bar dan gridline sepakat
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;          // celah seperempat-slot di setiap sisi
  BarW := SlotW - Gap;

  // Garis dasar (sumbu X) di sepanjang bawah 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;

    // Label kategori di bawah garis dasar, nilai di atas batang.
    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;

Dua detail layak dipertahankan. Gap adalah pecahan dari slot alih-alih hitungan titik tetap, sehingga batang tetap berjarak proporsional apakah Anda memplot empat kategori atau empat puluh. Dan label nilai diposisikan dengan tinggi turunan ValueToY yang sama yang digunakan batang, sehingga selalu duduk tepat di atas batangnya sendiri alih-alih mengambang pada offset yang ditebak. Jika Anda ingin garis kisi horizontal di belakang batang, gambar sebelum loop: pilih tiga atau empat nilai bulat, jalankan ValueToY pada masing-masing, dan gores garis samar melintasi plot pada Y itu. Menggambarnya lebih dulu menempatkannya di belakang batang dalam tumpukan model-pelukis yang digunakan PDF

Sumbu, tanda centang, dan label hanyalah garis dan teks lagi

Bagan belum selesai sampai pembaca bisa tahu apa arti batangnya, dan itu seluruhnya pekerjaan sumbu. Sumbu vertikal adalah satu garis yang digores ke atas di sepanjang tepi kiri plot dengan segelintir tanda centang dan nilainya. Gunakan kembali ValueToY agar tanda centang mendarat pada skala yang sama yang digunakan batang, jika tidak batang dan garis kisinya tidak setuju dan bagan berbohong diam-diam:

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);   // tanda centang pendek di luar sumbu
    Page.LineTo(Plot.Left, TickY);
    Page.Stroke;
    Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
  end;
end;

Label adalah tempat bagan paling sering pecah dalam produksi, dan kegagalannya selalu sama: teks yang pas di layar melampaui ruangnya dalam PDF. Nama kategori yang panang bertabrakan dengan tetangganya, dan nama bulan yang dilokalkan seperti "septembre" atau "Dezember" lebih lebar daripada "Sep" bahasa Inggris yang Anda uji. Tidak ada autosize untuk menyelamatkan Anda di sini, jadi tinggalkan margin nyata di bawah garis dasar, kecilkan font satu atau dua poin untuk set kategori yang padat, dan jika nama benar-benar panang, rotasikan. TextOut mengambil sudut sebagai argumen ketiganya, jadi meneruskan 90 mendirikan label dan memberi Anda ruang tanpa tumpang tindih. Uji tata letak dengan label terlebar yang Anda harapkan, bukan yang terpendek, sebelum ekspor dikirim

Bagan garis: satu polyline melalui titik yang dipetakan

Bagan garis menggunakan kembali seluruh pemetaan nilai dan hanya mengubah cara titik terhubung. Alih-alih persegi panjang per kategori, Anda berjalan melalui data sekali, mengubah setiap nilai ke (X, Y)-nya dengan ValueToY, dan menjahit titik bersama dengan satu MoveTo diikuti panggilan LineTo, digores di akhir. Titik pertama membuka jalur; setiap titik berikutnya memperluasnya:

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)        // buka jalur pada titik pertama
    else
      Page.LineTo(X, Y);       // perluas melalui setiap titik berikutnya
  end;
  Page.Stroke;                 // satu stroke mengecat seluruh polyline
end;

Perhatikan perbedaan spasi. Bagan batang membagi lebar dengan jumlah batang, karena setiap batang memiliki slot. Bagan garis membagi dengan jumlah interval, Count - 1, karena titik pertama dan terakhir duduk di tepi plot dan garis membentang melintasi celah di antaranya. Mencampur kedua itu adalah alasan biasa bagan garis menyimpang setengah-slot dari bagan batang yang seharusnya dilapisinya. Jika Anda ingin penanda di setiap titik data, jatuhkan Circle kecil dan Fill di setiap (X, Y) setelah polyline digores

Bagan pai: busur, atau baji jika Anda menjaganya sederhana

Irisan pai adalah satu bentuk yang membutuhkan trigonometri, karena baji dibatasi oleh dua jari-jari dan sebuah busur. Versi jujur menyapu busur dengan melangkah segmen garis kecil di sepanjang keliling, yang menghampiri kurva cukup dekat sehingga tidak ada pembaca yang bisa tahu. Sudut sapuan setiap iris adalah bagiannya dari total, (Value / Total) * 2π, dan Anda mengakumulasi sudut berjalan saat Anda berjalan mengelilingi:

HotPDF: Konstruksi irisan pie yang dimulai dengan MoveTo di pusat lingkaran, melangkah dengan segmen LineTo kecil berjarak sekitar dua derajat sepanjang busur sudut sapuan setiap irisan, lalu ditutup ke pusat sehingga Fill melukis baji solid
Wedge dimulai dengan MoveTo di pusat, berjalan dengan langkah LineTo kecil berjarak sekitar dua derajat sepanjang sapuannya di keliling, dan tertutup kembali sehingga Fill melukisnya padat
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 derajat per segmen

    Page.SetRGBFillColor(Colors[I]);
    Page.MoveTo(CX, CY);                     // puncak baji di pusat
    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);                      // tutup kembali ke pusat
    Page.Fill;

    Start := Start + Sweep;                   // maju ke irisan berikutnya
  end;
end;

Jalur dari-pusat-keluar, puncak lebih dulu, lalu busur, lalu kembali ke puncak, memberi baji tertutup yang Fill cat padat. Hitungan segmen memperdagangkan kehalusan dengan ukuran jalur: kira-kira dua derajat per langkah terlihat bulat pada jari-jari yang wajar tanpa menghasilkan aliran konten yang sangat besar. Jika Anda tidak membutuhkan lingkaran sejati, Anda bisa melewati trig sepenuhnya dan merender data yang sama sebagai satu batang bertumpuk horizontal, lebar setiap segmen proporsional terhadap bagiannya. Itu sering lebih terbaca daripada pai, dan itu hanya kode batang dengan persegi panjang disusun ujung-ke-ujung. Jangkau versi busur hanya ketika desain memanggil lingkaran aktual

Tidak satu pun dari ini bergantung pada pustaka bagan apa pun yang terpasang, yang merupakan keuntungan diam dari menggambar primitif secara langsung. Panggilan gambar kanvas yang sama yang menempatkan logo atau kotak tanda tangan membangun bagan ini, dan TextOut yang sama yang memberi label pada kolom formulir memberi label pada sumbu. Letakkan geometri plot dalam sebuah record, petakan nilai ke Y sekali, dan bagan batang, garis, atau pai adalah rutin pendek atas Rectangle, LineTo, dan Circle yang bisa Anda jatuhkan ke laporan apa pun. Panggilan Rectangle, MoveTo, LineTo, Circle, Fill, Stroke, dan TextOut yang digunakan di sini adalah bagian dari HotPDF Delphi Component untuk Delphi dan C++Builder