В 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 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: 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. Длинные названия категорий сталкиваются со своими соседями, а локализованные названия месяцев, такие как "сентябрь" или "декабрь", шире, чем английское "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) // 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 закрашивает сплошным цветом. Количество сегментов позволяет найти компромисс между гладкостью и размером контура: примерно два градуса на шаг выглядят круглыми при любом разумном радиусе, не создавая огромного потока содержимого. Если вам не нужен идеальный круг, вы можете вообще пропустить тригонометрию и отобразить те же данные в виде одной горизонтальной столбчатой диаграммы с накоплением, ширина каждого сегмента которой пропорциональна его доле. Зачастую это в любом случае более удобочитаемо, чем круговая диаграмма, и это просто код столбца с прямоугольниками, уложенными встык. Обращайтесь к версии с дугами только тогда, когда дизайн требует настоящего круга
Ничто из этого не зависит от установки какой-либо библиотеки диаграмм, что является тихим преимуществом непосредственного рисования примитивов. Те же вызовы рисования на холсте, которые размещают логотип или поле для подписи, строят эти диаграммы, а тот же TextOut, который помечает поле формы, помечает ось. Поместите геометрию графика в запись, один раз сопоставьте значения с Y, и столбчатая, линейная или круговая диаграмма станет короткой подпрограммой над Rectangle, LineTo и Circle, которую вы можете вставить в любой отчет. Вызовы Rectangle, MoveTo, LineTo, Circle, Fill, Stroke и TextOut, используемые здесь, являются частью компонента HotPDF для Delphi и C++Builder