Техническа статия

Чертане на графики в PDF с HotPDF примитиви

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 файла. Дългите имена на категории се сблъскват със своите съседи, а локализираните имена на месеци като "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)        // 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 Component за Delphi и C++Builder