HotPDF nie posiada obiektu wykresu. Nie ma TPDFChart, nie ma AddBarSeries, nie ma niczego, co przyjmuje tablicę liczb i zwraca wyrenderowany wykres. Zamiast tego otrzymujesz płótno strony z tym samym niskopoziomowym słownictwem, którego używa każdy model rysowania PDF: prostokąty, linie, koła, wypełnienia, obrysy i tekst umieszczony na dokładnych współrzędnych. Wykres w dokumencie HotPDF jest zatem czymś, co budujesz, a nie czymś, o co prosisz. Brzmi to jak więcej pracy niż jest w rzeczywistości. Kiedy raz napiszesz matematykę współrzędnych, wykres słupkowy to pętla po prostokątach, wykres liniowy to polilinia, a wykres kołowy to wachlarz łuków, a ty kontrolujesz każdy piksel wyniku
Ma to znaczenie, ponieważ alternatywa, po którą ludzie sięgają w pierwszej kolejności, rasteryzacja kontrolki wykresu ekranowego do bitmapy i wklejenie obrazu na stronę, daje wykres zablokowany do rozdzielczości ekranu, który drukuje się nieostro i rozdyma plik. Rysowanie wykresu za pomocą prymitywów wektorowych HotPDF sprawia, że wynik jest ostry przy dowolnym powiększeniu i dowolnym DPI druku, ponieważ słupki i osie są prawdziwymi operatorami ścieżek PDF, a nie pikselami. Kosztem jest to, że ty jesteś właścicielem układu. Mechanika sprowadza się do kilku ruchów: jednego odwrócenia współrzędnych, które łapie wszystkich, działającego wykresu słupkowego, sztuczki z polilinią dla wykresów liniowych i matematyki łuków dla kawałków tortu
Jedyną trudną częścią jest układ współrzędnych
Grafika ekranowa umieszcza początek układu współrzędnych w lewym górnym rogu z Y rosnącym w dół. PDF robi odwrotnie. Początek znajduje się w lewym dolnym rogu strony, a Y rośnie w górę, mierzony w punktach (1/72 cala). Każde wywołanie rysowania w HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, używa tej konwencji "lewy dolny, Y w górę". Jeśli przeniesiesz instynkty z grafiki ekranowej, twój pierwszy wykres narysuje się do góry nogami i ucieknie poza dół strony
Tak więc prawdziwą pracą na każdym wykresie jest jedno mapowanie: zamień wartość danych na współrzędną Y, która respektuje Y-up. Zdecyduj się na prostokąt wykresu, cztery liczby dla obszaru, na którym żyje wykres, a następnie zmapuj najmniejszą wartość w twoich danych na dolną krawędź, a największą na górną. Dla słupka o wartości V w skali, która biegnie od 0 do MaxValue, górna krawędź słupka to PlotBottom + (V / MaxValue) * PlotHeight, a słupek rośnie w górę od PlotBottom. Zrozum to jedno wyrażenie prawidłowo, a cała reszta to księgowość. Poniższy pomocnik przechowuje geometrię wykresu i wykonuje konwersję, więc kod rysowania nigdy nie dotyka surowej arytmetyki dwukrotnie:
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;
Jeden osąd ukrywa się w MaxValue. Jeśli ustawisz go na dokładny największy punkt danych, najwyższy słupek dotyka górnej krawędzi wykresu i wygląda na przycięty. Zaokrąglij ją do czystej liczby powyżej maksimum, powiedzmy do następnej wielokrotności 10 lub 100, aby wykres miał zapas, a etykiety linii siatki odczytywały się jako okrągłe liczby, a nie cokolwiek, na co złożyły się dane
Wykres słupkowy to pętla po prostokątach
Po ustaleniu mapowania wykres słupkowy pisze się sam. Podziel szerokość wykresu na jedno miejsce na kategorię, zostaw szczelinę między słupkami, aby się nie stykały, i narysuj każdy słupek jako wypełniony prostokąt, którego wysokość pochodzi z ValueToY. Rectangle z HotPDF pobiera lewy dolny róg plus szerokość i wysokość, co dokładnie pasuje do słupka rosnącego w górę od linii bazowej. Najpierw ustaw kolor wypełnienia, ułóż ścieżkę, a następnie zadzwoń do Fill, aby go pomalować. Etykieta kategorii trafia pod linię bazową, wartość nad słupek:
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;
Dwa szczegóły zarabiają na siebie. Gap to ułamek miejsca zamiast stałej liczby punktów, więc słupki pozostają proporcjonalnie rozstawione, niezależnie od tego, czy wykreślasz cztery kategorie, czy czterdzieści. A etykieta wartości jest pozycjonowana za pomocą tej samej wysokości opartej na ValueToY, co słupek, więc zawsze siedzi tuż nad własnym słupkiem zamiast unosić się przy odgadniętym przesunięciu. Jeśli chcesz mieć poziome linie siatki za słupkami, narysuj je przed pętlą: wybierz trzy lub cztery okrągłe wartości, uruchom ValueToY na każdej i narysuj słabą linię na całym wykresie przy tej Y. Narysowanie ich w pierwszej kolejności umieszcza je za słupkami w modelu układania stosowanym w PDF
Osie, znaczniki i etykiety to po prostu kolejne linie i tekst
Wykres nie jest ukończony, dopóki czytelnik nie może powiedzieć, co oznaczają słupki, a to całkowicie praca z osiami. Oś pionowa to jedna pociągnięta linia w górę lewej krawędzi wykresu z garstką znaczników wyboru i ich wartości. Ponownie użyj ValueToY, aby znaczniki wylądowały na tej samej skali co słupki, w przeciwnym razie słupek i jego linia siatki się nie zgadzają, a wykres po cichu kłamie:
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); // 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;
Etykiety to miejsce, w którym wykresy najczęściej psują się w produkcji, a awaria zawsze jest taka sama: tekst, który mieścił się na ekranie, przekracza swoje miejsce w pliku PDF. Długie nazwy kategorii zderzają się ze swoimi sąsiadami, a zlokalizowane nazwy miesięcy, takie jak "septembre" lub "Dezember", są szersze niż angielski "Sep", z którym testowałeś. Nie ma tutaj funkcji autouzupełniania, która cię uratuje, więc zostaw prawdziwy margines pod linią bazową, zmniejsz czcionkę o punkt lub dwa w przypadku gęstych zestawów kategorii, a jeśli nazwy są naprawdę długie, obróć je. TextOut przyjmuje kąt jako swój trzeci argument, więc podanie 90 ustawia etykietę na końcu i daje ci miejsce bez nakładania się. Przetestuj układ z najszerszą oczekiwaną etykietą, a nie najkrótszą, zanim eksport zostanie wysłany
Wykresy liniowe: jedna polilinia przez zmapowane punkty
Wykres liniowy ponownie wykorzystuje całe mapowanie wartości i zmienia tylko sposób, w jaki punkty się łączą. Zamiast prostokąta na kategorię, przechodzisz przez dane raz, konwertujesz każdą wartość na jej (X, Y) z ValueToY, i łączysz punkty razem za pomocą pojedynczego MoveTo, a następnie wywołań LineTo, narysowanych na końcu. Pierwszy punkt otwiera ścieżkę; każdy późniejszy punkt ją rozszerza:
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;
Zwróć uwagę na różnicę odstępów. Wykres słupkowy dzieli szerokość przez liczbę słupków, ponieważ każdy słupek ma własne gniazdo. Wykres liniowy dzieli przez liczbę przedziałów, Count - 1, ponieważ pierwszy i ostatni punkt leżą na krawędziach wykresu, a linia obejmuje luki między nimi. Mieszanie tych dwóch elementów jest zwykle powodem, dla którego wykres liniowy dryfuje o pół miejsca w stosunku do wykresu słupkowego, na który ma nakładać się. Jeśli chcesz znacznik na każdym punkcie danych, upuść małe Circle i Fill przy każdym (X, Y) po pociągnięciu polilinii
Wykresy kołowe: łuki lub kliny, jeśli zachowasz prostotę
Wycinki kołowe to jedyny kształt, który wymaga trygonometrii, ponieważ klin jest ograniczony dwoma promieniami i łukiem. Szczera wersja omiata łuk, przenosząc małe segmenty linii wzdłuż obwodu, co przybliża krzywą na tyle blisko, że żaden czytelnik nie może powiedzieć. Kąt omiatania każdego plasterka to jego udział w całkowitej sumie, (Value / Total) * 2π, i akumulujesz kąt biegnący wokół:
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;
Ścieżka od środka na zewnątrz, najpierw wierzchołek, potem łuk, a następnie z powrotem do wierzchołka, daje zamknięty klin, który Fill maluje solidnie. Liczba segmentów zamienia płynność na wielkość ścieżki: z grubsza dwa stopnie na krok wygląda okrągło przy dowolnym sensownym promieniu bez tworzenia ogromnego strumienia treści. Jeśli nie potrzebujesz prawdziwego koła, możesz całkowicie pominąć trygonometrię i wyrenderować te same dane jako jeden poziomy, ułożony wykres słupkowy, a szerokość każdego segmentu proporcjonalnie do jego udziału. To jest często bardziej czytelne niż ciasto i jest to tylko kod słupkowy z prostokątami ułożonymi od końca do końca. Sięgaj po wersję z łukiem tylko wtedy, gdy projekt wymaga rzeczywistego koła
Żadne z nich nie zależy od zainstalowania dowolnej biblioteki wykresów, co stanowi cichą przewagę polegającą na bezpośrednim rysowaniu prymitywów. Te same wywołania rysowania na płótnie, które umieszczają logo lub pole podpisu, budują te wykresy, a ten sam TextOut, który oznacza pole formularza, jest etykietą osi. Umieść geometrię wykresu na płycie, mapuj wartości do Y raz, a wykres słupkowy, liniowy lub kołowy jest krótką rutyną w Rectangle, LineTo i Circle, którą można wrzucić do dowolnego raportu. Połączenia z Rectangle, MoveTo, LineTo, Circle, Fill, Stroke i TextOut przedstawione tutaj stanowią część pakietu z HotPDF Component z programami dla Delphi wraz z oprogramowaniem od C++Builder