Teknik Makale

HotPDF Temel Bileşenleri ile PDF'de Grafikler Çizmek

HotPDF'in bir grafik nesnesi yoktur. TPDFChart, AddBarSeries veya bir dizi sayı alıp oluşturulmuş bir grafik döndüren hiçbir şey yoktur. Bunun yerine size sunduğu şey, her PDF çizim modelinin kullandığı aynı düşük seviyeli kavramlara sahip bir sayfa tuvalidir: dikdörtgenler, çizgiler, daireler, dolgular, çizgiler (strokes) ve tam koordinatlara yerleştirilmiş metin. Bu nedenle HotPDF belgesindeki bir grafik talep ettiğiniz değil, sizin inşa ettiğiniz bir şeydir. Bu, kulağa olduğundan daha zahmetli gibi gelebilir. Koordinat matematiğini bir kez yazdıktan sonra, bir çubuk grafiği dikdörtgenler üzerinde bir döngüden, bir çizgi grafiği bir çoklu çizgiden (polyline) ve bir pasta grafiği bir yay yelpazesinden ibarettir ve sonucun her pikselini siz kontrol edersiniz

Bu önemlidir çünkü insanların ilk başvurduğu alternatif olan, bir ekran grafik kontrolünü bit eşleme (bitmap) dönüştürüp görüntüyü sayfaya yapıştırmak, ekran çözünürlüğüne kilitlenmiş, bulanık yazdırılan ve dosyayı şişiren bir grafik verir. Grafiği HotPDF'in vektör temel bileşenleri (primitives) ile çizmek, çubuklar ve eksenler pikseller değil gerçek PDF yol (path) operatörleri olduğu için her yakınlaştırmada ve her baskı DPI değerinde çıktıyı net tutar. Bunun bedeli düzene sizin sahip olmanızdır. Mekanik birkaç hamleye dayanır: herkesi yanıltan o tek koordinat çevrimi, üzerinde çalışılmış bir çubuk grafiği, çizgi grafikler için çoklu çizgi numarası ve pasta dilimleri için yay matematiği

Tek zor kısım koordinat sistemidir

Ekran grafikleri, Y aşağı doğru büyürken başlangıç noktasını (origin) sol üste koyar. PDF bunun tam tersini yapar. Başlangıç noktası sayfanın sol alt köşesinde bulunur ve Y, nokta (1/72 inç) cinsinden ölçülerek yukarı doğru büyür. HotPDF'teki her çizim çağrısı, TextOut, Rectangle, MoveTo, LineTo, Circle, bu sol-alt, Y-yukarı kuralını kullanır. Ekran grafikleri içgüdülerinizi devam ettirirseniz, ilk grafiğiniz baş aşağı çizilir ve sayfanın altından dışarı taşar

Dolayısıyla herhangi bir grafikteki asıl iş tek bir eşlemedir: bir veri değerini Y-yukarı kuralına uyan bir Y koordinatına dönüştürmek. Bir çizim dikdörtgenine, grafiğin içinde yaşayacağı alan için dört sayıya karar verin, ardından verilerinizdeki en küçük değeri alt kenara ve en büyüğünü üste eşleyin. 0 ile MaxValue arasında değişen bir ölçekte V değerindeki bir çubuk için çubuğun üst kenarı PlotBottom + (V / MaxValue) * PlotHeight şeklindedir ve çubuk PlotBottom noktasından yukarı doğru büyür. O tek ifadeyi doğru yazarsanız geri kalan her şey prosedürden ibarettir. Aşağıdaki yardımcı, çizim geometrisini tutar ve dönüşümü yapar, böylece çizim kodu ham aritmetiğe bir daha asla dokunmaz:

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;

MaxValue içinde gizlenen bir karar noktası vardır. Bunu tam olarak en büyük veri noktasına ayarlarsanız, en uzun çubuk çizimin üst kenarına dokunur ve kırpılmış gibi görünür. Bunu maksimumun üzerindeki net bir sayıya, örneğin 10 veya 100'ün bir sonraki katına yuvarlayın, böylece grafiğin boşluk payı olur ve kılavuz çizgisi etiketleri verilerin tepe yaptığı rastgele sayılar yerine yuvarlak rakamlar olarak okunur

Çubuk grafik, dikdörtgenler üzerinde bir döngüdür

Eşleme çözüldüğünde, bir çubuk grafiği kendi kendini yazar. Çizim genişliğini her kategori için bir yuvaya bölün, çubukların birbirine değmemesi için aralarında bir boşluk bırakın ve her çubuğu yüksekliği ValueToY işlevinden gelen dolu bir dikdörtgen olarak çizin. HotPDF'in Rectangle metodu sol alt köşeyi, artı bir genişlik ve yüksekliği alır ki bu da taban çizgisinden yukarı doğru büyüyen bir çubukla tam olarak hizalanır. Önce dolgu rengini ayarlayın, yolu (path) belirleyin, ardından onu boyamak için Fill çağrısı yapın. Kategori etiketi taban çizgisinin altına, değer ise çubuğun üstüne gelir:

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;

İki ayrıntı görevini iyi yapar. Gap, sabit bir nokta sayısı yerine yuvanın bir bölümüdür, bu nedenle ister dört ister kırk kategori çizin, çubuklar orantılı aralıklarla kalır. Değer etiketi de çubuğun kullandığı, ValueToY türevi aynı yükseklikte konumlandırılır, böylece tahmin edilen bir uzaklıkta süzülmek yerine her zaman kendi çubuğunun hemen üzerinde durur. Çubukların arkasında yatay kılavuz çizgileri istiyorsanız, bunları döngüden önce çizin: üç veya dört yuvarlak değer seçin, her birinde ValueToY çalıştırın ve o Y noktasında çizim boyunca soluk bir çizgi (stroke) çizin. Bunları ilk çizmek, PDF'in kullandığı ressam modeli yığınında (painter's-model stacking) onları çubukların arkasına koyar

Eksenler, işaretler (ticks) ve etiketler sadece daha fazla çizgi ve metindir

Bir okuyucu çubukların ne anlama geldiğini söyleyebilene kadar grafik bitmiş sayılmaz ve bu tamamen eksen çalışmasıdır. Dikey eksen, çizimin sol kenarı boyunca uzanan, bir avuç onay işareti (tick marks) ve bunların değerlerini içeren tek bir çizgidir. İşaretlerin çubukların kullandığı aynı ölçeğe yerleşmesi için ValueToY işlevini yeniden kullanın, aksi takdirde bir çubuk ile onun kılavuz çizgisi uyuşmaz ve grafik sessizce yalan söyler:

procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
  TickCount: Integer);
var
  I, TickV, TickY: Single; // NOTE: Made slight fix here? Wait I shouldn't modify source code type declarations, wait original was TickV, TickY: Single; I: Integer;
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;

Etiketler, grafiklerin üretimde en çok bozulduğu yerdir ve başarısızlık her zaman aynıdır: ekrana sığan metin PDF'deki alanını aşar. Uzun kategori adları komşularıyla çarpışır ve "septembre" veya "Dezember" gibi yerelleştirilmiş ay adları, test ettiğiniz İngilizce "Sep" kelimesinden daha geniştir. Burada sizi kurtaracak bir otomatik boyutlandırma (autosize) yoktur, bu nedenle taban çizgisinin altında gerçek bir kenar boşluğu bırakın, yoğun kategori setleri için yazı tipini bir veya iki nokta küçültün ve adlar gerçekten uzunsa bunları döndürün. TextOut üçüncü argümanı olarak bir açı alır, bu nedenle 90 iletmek etiketi dik tutar ve örtüşme olmadan size yer kazandırır. Dışa aktarma işlemini göndermeden önce düzeni en kısa değil, beklediğiniz en geniş etiketle test edin

Çizgi grafikler: eşlenen noktalar boyunca tek bir çoklu çizgi (polyline)

Bir çizgi grafik (line chart) tüm değer eşlemesini yeniden kullanır ve yalnızca noktaların nasıl bağlandığını değiştirir. Her kategori için bir dikdörtgen yerine, verilerin üzerinden bir kez geçersiniz, her değeri ValueToY ile (X, Y) koordinatına dönüştürürsünüz ve tek bir MoveTo ve ardından gelen LineTo çağrılarıyla noktaları birbirine dikip sonunda çizersiniz (stroke). İlk nokta yolu açar; daha sonraki her nokta onu uzatır:

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;

Aralık farkına dikkat edin. Bir çubuk grafiği, her çubuğun bir yuvası olduğu için genişliği çubuk sayısına böler. Bir çizgi grafiği ise, ilk ve son noktalar çizim kenarlarında oturduğundan ve çizgi bunların arasındaki boşluklara yayıldığından, aralık sayısına (Count - 1) böler. Bu ikisini karıştırmak, bir çizgi grafiğinin, üzerine yerleştirilmesi amaçlanan çubuk grafiğinden yarım yuva kaymasının olağan nedenidir. Her veri noktasında bir işaretçi (marker) istiyorsanız, çoklu çizgi çizildikten sonra her (X, Y) noktasına küçük bir Circle ve Fill bırakın

Pasta grafikler: yaylar veya basit tutarsanız dilimler (wedges)

Pasta dilimleri trigonometri gerektiren tek şekildir, çünkü bir dilim iki yarıçap ve bir yay (arc) ile sınırlandırılmıştır. Dürüst versiyon, çevre boyunca küçük çizgi parçalarını adımlayarak yayı tarar; bu eğriye hiçbir okuyucunun fark edemeyeceği kadar yaklaşır. Her dilimin tarama açısı (sweep angle) toplam içindeki payıdır, (Value / Total) * 2π ve etrafında dönerken çalışan açıyı biriktirirsiniz:

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;

Merkezden dışa doğru yol (önce tepe noktası, sonra yay, sonra tepe noktasına geri dönüş), Fill komutunun tamamen boyadığı kapalı bir dilim verir. Segment sayısı, yol boyutu için pürüzsüzlüğü takas eder: adım başına kabaca iki derece, devasa bir içerik akışı oluşturmadan herhangi bir makul yarıçapta yuvarlak görünür. Gerçek bir daireye ihtiyacınız yoksa, trigonometriyi tamamen atlayabilir ve aynı verileri tek bir yatay yığılmış çubuk (stacked bar) olarak, her segmentin genişliğini payıyla orantılı olacak şekilde işleyebilirsiniz. Bu zaten genellikle bir pastadan daha okunaklıdır ve sadece dikdörtgenlerin uç uca dizildiği çubuk koduyla aynıdır. Yay (arc) versiyonuna yalnızca tasarım gerçek bir daire gerektirdiğinde uzanın

Bunların hiçbiri kurulu herhangi bir grafik kütüphanesine bağlı değildir, ki temel bileşenleri doğrudan çizmenin sessiz avantajı da budur. Bir logoyu veya imza kutusunu yerleştiren aynı tuval çizim çağrıları bu grafikleri oluşturur ve bir form alanını etiketleyen aynı TextOut bir ekseni etiketler. Çizim geometrisini bir kayda (record) koyun, değerleri Y'ye bir kez eşleyin ve artık bir çubuk, çizgi veya pasta grafiği, herhangi bir rapora bırakabileceğiniz Rectangle, LineTo ve Circle üzerinde kısa bir yordamdır. Burada kullanılan Rectangle, MoveTo, LineTo, Circle, Fill, Stroke ve TextOut çağrıları, Delphi ve C++Builder için HotPDF Bileşeni'nin (Component) bir parçasıdır