HotPDF не має об'єкта діаграми (chart). Немає жодного TPDFChart, жодного AddBarSeries, нічого, що приймало б масив чисел і повертало готовий графік. Замість цього він надає вам полотно (canvas) сторінки з тим самим низькорівневим словником, який використовує кожна модель малювання PDF: прямокутники, лінії, круги, заливки, контури та текст, розміщені за точними координатами. Таким чином, діаграма в документі HotPDF - це те, що ви будуєте, а не те, про що ви просите. Це звучить як складніше завдання, ніж є насправді. Щойно ви один раз напишете математику координат, гістограма (bar chart) перетвориться на цикл по прямокутниках, лінійний графік (line chart) - на ламану лінію (polyline), а кругова діаграма (pie chart) - на віяло дуг, і ви повністю контролюватимете кожен піксель результату
Це має значення, тому що альтернатива, до якої люди тягнуться в першу чергу - растрування екранного елемента керування діаграмою в bitmap-зображення та його вставка на сторінку - дає вам діаграму, прив'язану до роздільної здатності екрана, яка друкується нечітко і роздуває файл. Малювання діаграми за допомогою векторних примітивів 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 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. Якщо ви встановите його точно рівним найбільшій точці даних, найвищий стовпчик торкнеться верхнього краю графіка і виглядатиме обрізаним. Округліть його в більшу сторону до красивого числа вище максимуму, скажімо, до найближчого кратного 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; // 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;
Тут є дві деталі, які варто зазначити. Gap (відступ) - це частка слота, а не фіксована кількість пунктів, тому стовпчики залишаються пропорційно розташованими незалежно від того, будуєте ви графік для чотирьох чи сорока категорій. А мітка значення позиціонується на тій самій висоті, отриманій з ValueToY, що й сам стовпчик, тому вона завжди розміщується прямо над власним стовпчиком, а не висить на вгаданому зміщенні. Якщо вам потрібні горизонтальні лінії сітки позаду стовпчиків, малюйте їх перед циклом: виберіть три-чотири круглі значення, пропустіть кожне через ValueToY і проведіть бліду лінію через увесь графік на цьому Y. Якщо намалювати їх першими, вони опиняться позаду стовпчиків у моделі відмальовування "від заднього плану до переднього", яку використовує PDF
Осі, поділки та мітки - це просто ще більше ліній і тексту
Діаграма не завершена, доки читач не зможе зрозуміти, що означають ці стовпчики, і це повністю робота з осями. Вертикальна вісь - це одна лінія вздовж лівого краю графіка з жменькою позначок і їхніх значень. Використовуйте ValueToY повторно, щоб позначки потрапляли на той самий масштаб, що й стовпчики, інакше стовпчик і лінія сітки розійдуться, і діаграма тихо вам збреше:
procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
TickCount: Integer);
var
I, Count: 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;
Мітки - це те місце, де діаграми найчастіше ламаються у продакшені, і збій завжди однаковий: текст, який помістився на екрані, виходить за межі відведеного йому місця у PDF-файлі. Довгі назви категорій наїжджають на сусідів, а локалізовані назви місяців, як-от "вересень" (укр) або "septembre", ширші за англійське "Sep", з яким ви тестували. Тут немає авторозміру (autosize), який би врятував ситуацію, тож залиште реальний запас знизу під базовою лінією, зменште шрифт на пункт-два для щільних наборів категорій, а якщо імена дійсно довгі - поверніть їх. 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) // 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;
Зверніть увагу на різницю у відступах. Гістограма ділить ширину на кількість стовпчиків, оскільки кожен стовпчик займає свій слот. Лінійний графік ділить на кількість інтервалів, 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 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;
Шлях від центру, спершу вершина, потім дуга, а потім назад до вершини, дає замкнений сектор, який Fill зафарбовує повністю. Кількість сегментів розмінює плавність на розмір шляху: приблизно два градуси на крок виглядають кругло при будь-якому розумному радіусі, не створюючи при цьому величезного потоку вмісту. Якщо вам не потрібне ідеальне коло, ви можете взагалі відмовитися від тригонометрії і відрендерити ті самі дані у вигляді єдиної горизонтальної складеної гістограми (stacked bar), ширина кожного сегмента якої пропорційна її частці. Часто це навіть розбірливіше за кругову діаграму, і це просто код гістограми з прямокутниками, розташованими один за одним. Тягніться до варіанта з дугами лише тоді, коли дизайн вимагає саме кола
Ніщо з цього не залежить від того, чи встановлено якусь бібліотеку діаграм - це тиха перевага прямого малювання примітивів. Ті самі виклики малювання на полотні, що розміщують логотип або поле для підпису, створюють ці діаграми, і той самий TextOut, який маркує поле форми, підписує вісь. Помістіть геометрію графіка в запис (record), один раз відобразіть значення в Y, і гістограма, лінійний графік або кругова діаграма перетворяться на коротку підпрограму з використанням Rectangle, LineTo і Circle, яку можна додати до будь-якого звіту. Виклики Rectangle, MoveTo, LineTo, Circle, Fill, Stroke та TextOut, використані тут, є частиною HotPDF Component для Delphi та C++Builder