Teknik Makale

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

Bir ekran grafik kontrolünden 96 DPI'lık bir bitmap olarak kopyalanan grafik, PDF görüntüleyicide yüzde 100 yakınlaştırmada iyi görünür. Yüzde 400'e çıkın ya da sayfayı 600 DPI'lık bir lazer yazıcıya gönderin; çubuk kenarları yumuşar, eksen etiketleri gri lekelere dönüşür ve dosya, gerçekte bir düzine dikdörtgen olan şey için birkaç yüz kilobayt piksel taşır. Vektör yolları olarak çizilen aynı grafik her yakınlaştırma düzeyinde ve baskı çözünürlüğünde keskin kalır; çünkü çubuklar, eksenler ve etiketler, görüntüleyicinin gerektiği ölçekte yeniden render ettiği PDF yol ve metin operatörleridir

Vektör çıktı, düzen işini kodunuza taşır. Her çubuğun, işaretin ve etiketin nereye düşeceğini hesaplamak zorunda olan bir şeydir; çubuk, çizgi ve pasta grafikleri için o hesap, bir koordinat eşlemesi artı dikdörtgenler, çizgi bölümleri ve yaylar üzerinde birkaç döngüdür. Eşlemeyi bir kez doğru kurun, döngüler kısadır

HotPDF'de DrawChart mı elle çizilen temel bileşenler mi?

HotPDF hazır bir grafik renderer'ıyla gelir ve standart bir rapor grafiği için denenecek ilk şey odur. Bir THPDFChart kurun, serileri AddSeries ile ekleyin (hckLine, hckBar, hckArea, hckPie ve hckScatter türleri), AddPoint ile doldurun ve THPDFPage.DrawChart'ı bir hedef dikdörtgenle çağırın. Renderer eksen aralığını, ızgarayı, işaret etiketlerini, açıklamayı ve tema renklerini kendisi halleder ve yapı ebeveyni alan aşırı yük, grafiği verilerden üretilmiş alternatif metinle etiketli bir figür olarak yazar; belge bir erişilebilirlik kontrolünden geçmek zorundaysa bu önem taşır

var
  Chart: THPDFChart;
  Sales: THPDFChartSeries;
begin
  Chart := THPDFChart.Create;
  try
    Chart.Title := 'Quarterly sales';
    Chart.LegendPosition := hclBottom;
    Sales := Chart.AddSeries(hckBar, 'Sales');
    Sales.AddPoint(1, 420, 'Q1');
    Sales.AddPoint(2, 515, 'Q2');
    Sales.AddPoint(3, 388, 'Q3');
    Sales.AddPoint(4, 610, 'Q4');
    // Sayfa birimlerinde dikdörtgen: sol, üst, genişlik, yükseklik
    HotPDF1.CurrentPage.DrawChart(Chart, 60, 120, 470, 280);
  finally
    Chart.Free;
  end;
end;

Ama renderer'ın sabit bir söz varlığı vardır. Birden çok çubuk serisi her kategoride yan yana durur, pasta grafiği kendi grafiğinin tek serisi olmak zorundadır ve görünüm üç tema artı seri başına renklerden gelir. Yığılmış çubuklar, çubukların arkasında gölgelenmiş bir hedef bandı, tek serideki her çubuk için farklı bir marka rengi, on iki yönünden başlayıp saat yönünde ilerleyen bir pasta: tasarım bunlardan herhangi birini istediği anda, grafiği sayfa canvas'ıyla kendiniz çizmek renderer'ı bükmekten daha az iştir. O temel bileşenler Rectangle, MoveTo, LineTo, Pie, Fill, Stroke ve TextOut'tur; canvas çizim anlatımının kapsadığı çağrıların aynısıdır

Çubuk grafiğim neden baş aşağı çıkar?

HotPDF sayfa koordinatları başlangıcı sol üst köşeye koyar, Y aşağı doğru büyür; VCL canvas'ıyla aynı çerçeve. PDF kullanıcı uzayının kendisi öbür yöne akar (başlangıç sol alt, Y yukarı, ISO 32000-1 §8.3.2.3) ve HotPDF içerik akışını yazarken her koordinatı o uzaya çevirir. Grafik değerleri ise yukarı doğru büyür. Ölçeklenmiş bir değeri doğrudan Y olarak besleyin ve en uzun çubuk buz sarkacı gibi plot'un tepesinden sarkar

Çözüm tek bir ifadedir. Bir plot dikdörtgeni seçin, sıfırı alt kenarına, ölçeğin tepesini üst kenarına eşleyin ve her değeri sayfaya dokunmadan o eşlemeden geçirin. 0'dan MaxValue'a uzanan bir ölçekte V değeri için sayfa Y'si Plot.Bottom - (V / Plot.MaxValue) * Plot.Height'tır; Plot.Bottom taban çizgisidir. Aşağıdaki çubuk grafiği kodu bunu küçük bir ValueToY yardımcısına sarar ve öbür rutinlerin hepsi onu yeniden kullanır

HotPDF: Ekran koordinatları ile HotPDF sayfa koordinatları, Y aşağı doğru büyüyerek sol üst başlangıcı paylaşır; grafik değerleri yukarı büyür ve ValueToY bir veri değerini o Y-aşağı sayfa uzayına çevirir
HotPDF alışıldık ekran çerçevesini korur: başlangıç sol üstte, Y aşağı doğru büyür; böylece tek bir ValueToY ifadesi yukarı büyüyen her veri değerini sayfa uzayına çevirir

O ifadede gizli iki seçim vardır. MaxValue, en büyük veri noktasının üzerinde yuvarlak bir sayı olmalı; 10'un ya da 100'un bir sonraki katı; böylece en uzun çubuk üst kenara değmez ve ızgara etiketleri 0, 203,5, 407 yerine 0, 200, 400 okunur. Ve ölçek bilerek sıfırdan başlar. Çubuk uzunluğu değerin kendisidir; 350'den başlayan bir değer ekseni, 388'lik çubuğun 420'lik çubuğun yarısından az kadar uzun görünmesini sağlar, oysa gerçek fark yüzde 8'in altındadır. Yerleşik renderer, tam bu nedenle bar ve area serileri için sıfırı aralığa çeker. Elle çizilen kodda da aynı kuralı koruyun

Birimler eşlemenin öteki yarısıdır. Varsayılan Resolution 72 iken bir sayfa birimi bir PDF point'tir (1/72 inç). Çözünürlüğü değiştirin; koordinatlarınız, çizgi genişlikleriniz ve metin ölçümleriniz hep yeni birime geçer; Resolution ve çizim birimleri makalesi bunu ayrıntısıyla işler. DrawChart hedef dikdörtgenini sayfa birimlerinde alır ama kendi kenar boşluklarını, font boyutlarını ve çizgi genişliklerini her çözünürlükte point cinsinden dizer

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

Eşleme yerindeyken çubuk grafiği bir döngüdür. Plot genişliğini kategori başına bir yuvaya bölün, komşu çubuklar değmesin diye bir boşluk bırakın ve her çubuğu ValueToY tepesinden taban çizgisine dolgulu bir dikdörtgen olarak çizin. Rectangle sol üst köşeyi, genişlik ve yükseklik alır; bu eşlemeye doğrudan oturur:

HotPDF: Çubuk grafiği anatomisi; plot genişliğinden bölünen ve çubuklar arasında boşluk bırakan yuvalar, her çubuğun Plot.Bottom'dan yukarı büyüyen dolgulu bir Rectangle olması, taban çizgisinin altında kategori etiketleri, çubuk yüksekliğine binen değer etiketleri ve ValueToY'yi yeniden kullanan eksen işaretleri
Her çubuk, yuvası içinde Plot.Bottom'dan yukarı büyüyen bir dolgulu Rectangle'dır; çeyrek yuvalık boşluk genişlikle ölçeklenir ve eksen işaretleri ValueToY'yi yeniden kullanır; böylece çubuklar ile ızgara çizgileri uyuşur
type
  TPlotArea = record
    Left, Bottom, Width, Height: Single;  // sayfa birimleri; Bottom taban çizgisi Y'dir
    MaxValue: Single;                     // değer ölçeğinin tepesi
  end;

// Kendi yardımcınız: büyük değer daha yüksekte durur; Y-aşağı bir sayfada
// bu daha küçük Y demektir
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
  Result := Plot.Bottom - (V / Plot.MaxValue) * Plot.Height;
end;

procedure DrawBarChart(Page: THPDFPage; const Plot: TPlotArea;
  const Values: array of Single; const Labels: array of string;
  const BarColors: array of TColor);
var
  I, Count: Integer;
  SlotW, BarW, BarX, BarTop, Gap, GridV, GridY: Single;
begin
  Count := Length(Values);
  SlotW := Plot.Width / Count;
  Gap := SlotW * 0.25;              // yuvanın çeyreği, sola ve sağa bölünür
  BarW := SlotW - Gap;

  // Önce kesikli ızgara çizgileri; çubuklar onların üstüne boyasın
  Page.SetLineWidth(0.5);
  Page.SetRGBStrokeColor(RGB(200, 200, 200));
  Page.SetDash([2, 2], 0);
  GridV := Plot.MaxValue / 4;
  while GridV <= Plot.MaxValue do
  begin
    GridY := ValueToY(Plot, GridV);
    Page.MoveTo(Plot.Left, GridY);
    Page.LineTo(Plot.Left + Plot.Width, GridY);
    Page.Stroke;
    GridV := GridV + Plot.MaxValue / 4;
  end;
  Page.NoDash;

  Page.SetFont('Arial', [], 9);
  for I := 0 to Count - 1 do
  begin
    BarX := Plot.Left + I * SlotW + Gap / 2;
    BarTop := ValueToY(Plot, Values[I]);

    Page.SetRGBFillColor(BarColors[I mod Length(BarColors)]);
    Page.Rectangle(BarX, BarTop, BarW, Plot.Bottom - BarTop);
    Page.Fill;

    // TextOut metnin üstünü Y'ye koyar
    Page.SetRGBFillColor(clBlack);
    Page.TextOut(BarX, Plot.Bottom + 4, 0, Labels[I]);
    Page.TextOut(BarX, BarTop - 12, 0, FormatFloat('0', Values[I]));
  end;

  // Taban çizgisi en son; çubuk diplerinin üstünde dursun
  Page.SetLineWidth(1.0);
  Page.SetRGBStrokeColor(clBlack);
  Page.MoveTo(Plot.Left, Plot.Bottom);
  Page.LineTo(Plot.Left + Plot.Width, Plot.Bottom);
  Page.Stroke;
end;

Sıra, buradaki katmanlama modelinin tamamıdır. PDF bir ressam modeli kullanır; içerik akışındaki sonraki operatörler öncekilerin üstüne boyar. Bu yüzden önce ızgara çizgileri, üstlerine çubuklar, en son taban çizgisi gelir. Izgara bloğunu döngünün altına alın ve kesik çizgiler her çubuğun üzerinden dümdüz geçer

Boşluk, sabit bir point sayısı değil yuvanın kesridir. 470 pointlik bir plot'ta dört kategoriyle her yuva 117,5 point genişliğindedir ve çubuklar arasında yaklaşık 29 point hava kalır; kırk kategoriyle aynı kod 3 pointin altında bırakır ve bu hâlâ ayrı çubuklar olarak okunur. Sabit 12 pointlik bir boşluk, kırk kategoride 470 pointin 480'ini yutardı ve çubuk genişliği negatife dönerdi

Çarpışmayan eksen etiketleri

Etiketler, elle çizilen grafiklerin ilk kırıldığı yerdir ve neredeyse hep aynı biçimde kırılır. Ekrana sığan metin sayfadaki alanını aşar; çünkü geliştirici "Eyl" ile test etmiştir ve Almanca rapor 9 punto fontla "September" ya da "Dezember" basar. Değer ekseninin aynı sorunun daha sessiz bir çeşidi vardır: sabit X ofsetinde sola yaslı işaret etiketleri, sayılar boyut değiştirdiği anda dağınık görünür ve "1.200" etiketi "800"ün rahatça kaçtığı eksen çizgisine girer

Tahmin etmek yerine ölçün. TextWidth geçerli fontta bir dizenin genişliğini sayfa birimlerinde döndürür, TextHeight geçerli boyuttaki fontun çıkıntısını (ascent). Her işaret etiketini eksene sağa yaslayıp kendi işaretinin ortasına getirin:

procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
  TickCount: Integer);
var
  I: Integer;
  TickV, TickY: Single;
  S: string;
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);        // eksenin dışında kısa işaret
    Page.LineTo(Plot.Left, TickY);
    Page.Stroke;

    S := FormatFloat('#,##0', TickV);
    Page.TextOut(Plot.Left - 7 - Page.TextWidth(AnsiString(S)),
      TickY - Page.TextHeight(AnsiString(S)) / 2, 0, S);
  end;
end;

Kategori etiketleri için aynı ölçüm, yatay metinden ne zaman vazgeçileceğini söyler. En geniş etiketi, hiçbir şey çizmeden yuva genişliğiyle karşılaştırın. Sığmıyorsa fontu bir-iki point düşürün; hâlâ sığmıyorsa TextOut'a açı argümanı olarak 90 geçirip etiketleri dikin ve taban çizgisinin altına en uzunu için yeterli kenar boşluğu bırakın. Raporun gerçekten üretebileceği en geniş etiketle test edin; yerelleştirilmiş bir yapıda ay adları için bu nadiren İngilizce olandır

Çizgi grafikleri aynı eşlemeyi yeniden kullanır

Çizgi grafiği yalnızca noktaların nasıl bağlandığını değiştirir. Veriye bir kez yürüyün, her değeri ValueToY ile çevirin ve noktaları tek yola dikin: ilk nokta için MoveTo, sonraki her biri için LineTo, sonda tek bir Stroke. Noktalarınız hâlihazırda bir dizideyse Polyline bir array of THPDFCurrPoint alır, taze bir yol açar ve konturu size bırakır; iki noktadan azı için özel durum fırlatır, dolayısıyla tek okumalı bir seri çizgi yerine işaretçi ister

Aralıklar, çizgi grafiğinin aynı plot üzerinde çizilen bir çubuk grafiğinden saptığı yerdir. Çubuklar genişliği kategori sayısına böler; her çubuğun bir yuvası vardır. Çizgi, aralık sayısına böler: Count - 1; çünkü ilk ve son noktalar plot kenarlarında durur. İkisini üst üste koyun ve çizgi noktaları çubuk merkezleri yerine yuva sınırlarına düşer. Bir çizgi çubukların üstüne binecekse noktalarını her yuva merkezine koyun, Plot.Left + I * SlotW + SlotW / 2, ve Count - 1 aralığını büsbütün bırakın. Her noktaya bir işaretçi için, çizgi kontur edildikten sonra küçük bir Circle artı Fill işi görür

Pie ile pasta dilimleri, on ikiden saat yönünde

Bir pasta dilimi iki yarıçap ve bir yaydır. Bunu elle kurma yolu merkezden başlar, çember boyunca birkaç derece arayla kısa LineTo bölümleri atar ve merkeze geri kapanır; aşağıdaki şekildeki kurulum budur. İş görür ama 270 derecelik bir dilim yüzden fazla yol bölümüne dönüşür ve çember, yüksek yakınlaştırmada hâlâ bir poligondur

HotPDF: Pasta dilimi kurulumu; çember merkezinde MoveTo ile başlar, her dilimin süpürme açısı boyunca yay üzerinde yaklaşık iki derece arayla küçük LineTo bölümleri atar, sonra merkeze kapanır; böylece Fill dolu bir dilim boyar
Bir dilim merkezde MoveTo ile başlar, çember üzerindeki süpürmesi boyunca yaklaşık iki derece arayla küçük LineTo adımları atar ve geriye kapanır; böylece Fill onu dolgunca boyar

Bunun yerine Pie kullanın. THPDFPage.Pie GDI geleneğini izler: bir sınırlayıcı dikdörtgen, derece cinsinden başlangıç ve bitiş açısı; 0 doğuya, 90 kuzeye bakar, pozitif açılar saat yönünün tersinedir. Yayı her biri en çok 90 derecelik Bézier bölümlerinden kurar, merkeze çizgiyi çizer ve yolu kapatır; böylece çember gerçek bir eğri kalır ve tam bir dilim bir avuç operatöre mal olur. İş pastaları çoğunlukla on ikiden başlayıp saat yönünde akar; GDI terimleriyle bu, 90'dan başlayıp açıyı aşağı yürümektir:

procedure DrawPieChart(Page: THPDFPage; CX, CY, Radius: Single;
  const Values: array of Double; const Colors: array of TColor);
var
  I: Integer;
  Total, Sweep, Angle: Double;
begin
  Total := 0;
  for I := 0 to High(Values) do
    if Values[I] > 0 then
      Total := Total + Values[I];
  if Total <= 0 then
    Exit;

  Angle := 90;                            // on iki yönü
  Page.SetRGBStrokeColor(clWhite);
  Page.SetLineWidth(0.8);
  for I := 0 to High(Values) do
  begin
    if Values[I] <= 0 then
      Continue;                           // aşağıya bakın: hiçbir zaman sıfır ya da negatif süpürme geçmeyin
    Sweep := Values[I] / Total * 360;
    Page.SetRGBFillColor(Colors[I mod Length(Colors)]);
    // Angle - Sweep'ten Angle'a saat yönünün tersi, geçerli konumun
    // saat yönünde kalan dilimidir
    Page.Pie(CX - Radius, CY - Radius, CX + Radius, CY + Radius,
      Angle - Sweep, Angle);
    Page.FillAndStroke;                   // beyaz kontur dilimleri ayırır
    Angle := Angle - Sweep;
  end;
end;

Continue koruması en çok işe yarayan satırdır. Pie, başlangıç açısından küçük bir bitiş açısını 360'ı aşan bir süpürme sayar; GDI okuması budur ve bir dilimin 0 derece çizgisini geçmesini sağlayan da budur. Kaynak verideki bir iade satırı ya da düzeltme kaydı negatif bir değer verir, hesaplanan bitiş açısı başlangıcın altına düşer ve dilim, önünde çizilmiş her şeyin üstüne boyanan neredeyse tam bir diske sarılır. Sıfır değer daha yumuşak ama yine görünürdür: kapanan yol bir yarıçapa büzülür ve beyaz ayırıcı kontur onu çemberden merkeze giden başıboş bir çizgi olarak çizer. İkisini de döngüden önce süzün ve raporun negatif tutarları nasıl göstereğine ayrıca karar verin; çünkü bir pasta onları hiç gösteremez

Tasarım bir çemberde ısrar etmiyorsa, tek bir yatay yığılmış çubuk aynı payları, uç uca dizilmiş Rectangle çağrılarından başka hiçbir şeye gerek olmadan gösterir ve okurlar açılardan çok uzunlukları daha isabetli karşılaştırır. Beş-altı kategoriyi aşan bir bütünün-parçası figürü için daha iyi grafik budur; üstteki çubuk grafiği kodu da işin çoğunu zaten yapar

Bu makaledeki her grafik, HotPDF Delphi Component'in sayfa canvas çağrılarından kurulur; standart çizgi, çubuk, alan, pasta ve scatter düzenleri için DrawChart da hazırdır ve hepsi, build makinesinde kurulu bir grafik kütüphanesi olmadan Delphi'den ya da C++Builder'dan çalışır