HotPDF не має об'єкта графіка. Немає жодного TPDFChart, жодного AddBarSeries, нічого, що приймало б масив чисел і повертало відрендерений графік. Натомість він дає вам канву сторінки з тим самим низькорівневим словником, яким користується будь-яка модель малювання в PDF: прямокутники, лінії, кола, заливки, обведення й текст, розміщений за точними координатами. Тож графік у документі HotPDF є чимось, що ви будуєте, а не чимось, що ви замовляєте. Це звучить як більше роботи, ніж є насправді. Щойно ви одного разу написали математику координат, стовпчикова діаграма стає циклом по прямокутниках, лінійна - ламаною, а кругова - віялом дуг, і ви керуєте кожним пікселем результату
Це важить тому, що альтернатива, до якої люди тягнуться першою, - растеризувати екранний контрол графіка в растр і вклеїти зображення на сторінку, - дає вам графік, прикутий до екранної роздільної здатності, який друкується розмито й роздуває файл. Малювання графіка векторними примітивами HotPDF тримає вивід чітким за будь-якого масштабу й будь-якого DPI друку, бо стовпці та осі є справжніми операторами контурів PDF, а не пікселями. Ціна в тому, що компонування - на вас. Механіка зводиться до кількох ходів: одне перевертання координат, яке ловить усіх, розібрана стовпчикова діаграма, прийом із ламаною для лінійних графіків і математика дуг для секторів кругової
Єдина справді складна частина - це система координат
Екранна графіка кладе початок координат у лівий верхній кут, а Y зростає донизу. PDF робить навпаки. Початок координат сидить у лівому нижньому куті сторінки, а Y зростає вгору й відмірюється в пунктах (1/72 дюйма). Кожен виклик малювання в HotPDF - TextOut, Rectangle, MoveTo, LineTo, Circle - користується цією конвенцією "лівий низ, Y угору". Якщо ви перенесете інстинкти екранної графіки, ваш перший графік намалюється догори дриґом і збіжить за нижній край сторінки
Тож справжня робота в будь-якому графіку зводиться до одного відображення: перетворити значення даних на координату Y, яка шанує зростання Y угору. Визначтеся з прямокутником побудови, чотирма числами для площі, у якій живе графік, а потім відобразіть найменше значення ваших даних на нижній край, а найбільше - на верхній. Для стовпця зі значенням V на шкалі, що йде від 0 до MaxValue, верхній край стовпця дорівнює PlotBottom + (V / MaxValue) * PlotHeight, а сам стовпець росте вгору від PlotBottom. Зробіть цей єдиний вираз правильно - і все інше буде бухгалтерією. Помічник нижче тримає геометрію побудови й виконує перетворення, тож код малювання ніколи не торкається сирої арифметики двічі:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // пункти PDF, початок у лівому низу
MaxValue: Single; // верх шкали значень
end;
// Відображає значення даних на його координату Y всередині побудови, Y зростає вгору.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
В MaxValue ховається одне оцінне рішення. Якщо ви поставите його рівним точно найбільшій точці даних, найвищий стовпець торкнеться верхнього краю побудови й виглядатиме обрізаним. Округліть його вгору до чистого числа над максимумом, скажімо до наступного кратного 10 чи 100, щоб графік мав запас угорі, а підписи ліній сітки читалися круглими числами, а не тим, на чому дані випадково досягли піка
Стовпчикова діаграма є циклом по прямокутниках
Щойно відображення владнане, стовпчикова діаграма пишеться сама. Поділіть ширину побудови на один слот для кожної категорії, лишіть проміжок між стовпцями, щоб вони не торкалися, і намалюйте кожен стовпець як залитий прямокутник, чия висота походить із ValueToY. Rectangle у HotPDF приймає лівий нижній кут плюс ширину й висоту, що точно лягає на стовпець, який росте вгору від базової лінії. Спершу задайте колір заливки, викладіть контур, а потім викличте Fill, щоб його зафарбувати. Підпис категорії йде під базову лінію, значення - над стовпець:
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; // проміжок у чверть слота з кожного боку
BarW := SlotW - Gap;
// Базова лінія (вісь X) уздовж низу побудови.
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, ширина, висота
Page.Fill;
// Підпис категорії під базовою лінією, значення над стовпцем.
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;
Дві деталі відпрацьовують своє місце. Gap є часткою слота, а не фіксованою кількістю пунктів, тож стовпці лишаються пропорційно розставленими, чи ви малюєте чотири категорії, чи сорок. А підпис значення позиціюється тією самою похідною від ValueToY висотою, якою користується стовпець, тож він завжди сидить трохи вище за власний стовпець, а не висить на вгаданому зсуві. Якщо вам потрібні горизонтальні лінії сітки позаду стовпців, малюйте їх до циклу: оберіть три-чотири круглі значення, проженіть ValueToY на кожному й обведіть бліду лінію впоперек побудови на тому Y. Малювання їх першими кладе їх під стовпці в моделі накладання "як художник", якою користується PDF
Осі, поділки та підписи - це просто ще лінії й текст
Графік не завершений, доки читач не може сказати, що означають стовпці, а це цілком робота осей. Вертикальна вісь є однією обведеною лінією вгору по лівому краю побудови з жменькою поділок та їхніми значеннями. Перевикористайте ValueToY, щоб поділки лягли на ту саму шкалу, якою користуються стовпці, інакше стовпець і його лінія сітки розійдуться, і графік тихцем збреше:
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); // коротка поділка ззовні осі
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Підписи є тим місцем, де графіки найчастіше ламаються в продакшені, і збій завжди той самий: текст, який вміщався на екрані, вилазить за свій простір у PDF. Довгі назви категорій зіштовхуються із сусідніми, а локалізовані назви місяців на кшталт "septembre" чи "Dezember" ширші за англійське "Sep", на якому ви тестували. Жодного автопідбору розміру, який вас урятує, тут немає, тож лишайте справжній відступ під базовою лінією, зменшуйте шрифт на пункт-два для щільних наборів категорій, а якщо назви справді довгі, повертайте їх. TextOut приймає кут третім аргументом, тож передача 90 ставить підпис на ребро й купує вам простір без накладання. Тестуйте компонування на найширшому очікуваному підписі, а не на найкоротшому, ще до того, як експорт піде в реліз
Лінійні графіки: одна ламана через відображені точки
Лінійний графік перевикористовує все відображення значень і змінює лише те, як з'єднуються точки. Замість прямокутника на категорію ви один раз проходите дані, перетворюєте кожне значення на його (X, Y) через ValueToY і зшиваєте точки одним MoveTo, за яким ідуть виклики LineTo, а наприкінці обводите. Перша точка відкриває контур; кожна пізніша точка його продовжує:
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) // відкриває контур у першій точці
else
Page.LineTo(X, Y); // продовжує його через кожну пізнішу точку
end;
Page.Stroke; // одне обведення фарбує всю ламану
end;
Зверніть увагу на різницю в кроці. Стовпчикова діаграма ділить ширину на кількість стовпців, бо кожен стовпець володіє слотом. Лінійний графік ділить на кількість інтервалів, Count - 1, бо перша й остання точки сидять на краях побудови, а лінія перетинає проміжки між ними. Плутанина між цими двома і є звичайною причиною того, що лінійний графік зсувається на пів слота від стовпчикової діаграми, яку він мав накласти. Якщо вам потрібен маркер у кожній точці даних, покладіть маленький Circle і Fill у кожній (X, Y) після того, як ламану обведено
Кругові діаграми: дуги або клини, якщо тримати все просто
Сектори кругової діаграми є єдиною фігурою, якій потрібна тригонометрія, бо клин обмежений двома радіусами й дугою. Чесна версія обходить дугу, крокуючи маленькими відрізками вздовж кола, що наближає криву достатньо близько, аби жоден читач не помітив. Кут розгортки кожного сектора є його часткою від загального, (Value / Total) * 2π, і ви накопичуєте поточний кут, обходячи коло:
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 градуси на сегмент
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // вершина клина в центрі
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); // замикання назад до центру
Page.Fill;
Start := Start + Sweep; // перехід до наступного сектора
end;
end;
Контур від центру назовні - спершу вершина, потім дуга, потім назад до вершини - дає замкнений клин, який Fill фарбує суцільно. Кількість сегментів міняє гладкість на розмір контуру: приблизно два градуси на крок виглядають круглими за будь-якого розумного радіуса й не породжують велетенського потоку вмісту. Якщо вам не потрібне справжнє коло, ви можете взагалі обійтися без тригонометрії й відрендерити ті самі дані як одну горизонтальну складену смугу, де ширина кожного сегмента пропорційна його частці. Це часто й так читабельніше за круг, і це просто код стовпчикової діаграми з прямокутниками, викладеними впритул один за одним. Звертайтеся до версії з дугами лише тоді, коли дизайн вимагає справжнього кола
Ніщо з цього не залежить від встановленої бібліотеки графіків, і це тиха перевага прямого малювання примітивами. Ті самі виклики малювання на канві, що розміщують логотип чи рамку підпису, будують ці графіки, а той самий TextOut, що підписує поле форми, підписує вісь. Покладіть геометрію побудови в запис, один раз відобразіть значення на Y - і стовпчикова, лінійна чи кругова діаграма стане короткою підпрограмою над Rectangle, LineTo та Circle, яку ви зможете вкинути в будь-який звіт. Використані тут виклики Rectangle, MoveTo, LineTo, Circle, Fill, Stroke та TextOut є частиною HotPDF Delphi Component для Delphi та C++Builder