Techninis straipsnis

Diagramų piešimas PDF faile su „HotPDF“ primityvais

„HotPDF“ neturi diagramos objekto. Nėra TPDFChart, nėra AddBarSeries, nėra nieko, kas paimtų skaičių masyvą ir grąžintų atvaizduotą grafiką. Vietoj to jis suteikia puslapio drobę su tokiu pačiu žemo lygio žodynu, kurį naudoja kiekvienas PDF piešimo modelis: stačiakampiais, linijomis, apskritimais, užpildais, potėpiais ir tekstu, įterptu tiksliose koordinatėse. Todėl diagrama „HotPDF“ dokumente yra tai, ką sukuriate, o ne tai, ko paprašote. Tai skamba kaip didesnis darbas, nei yra iš tikrųjų. Kai vieną kartą parašysite koordinačių matematiką, stulpelinė diagrama bus ciklas per stačiakampius, linijinė diagrama – laužtė, o skritulinė diagrama – lankų vėduoklė, ir jūs galėsite valdyti kiekvieną rezultato pikselį

Tai svarbu, nes alternatyva, kurios žmonės griebiasi pirmiausia – ekrano diagramos valdiklio rastravimas į bitų schemą ir vaizdo įklijavimas į puslapį, – sukuria diagramą, susietą su ekrano raiška, kuri spausdinant atrodo neryškiai ir išpučia failo dydį. Piešiant diagramą su „HotPDF“ vektoriniais primityvais, rezultatas išlieka ryškus esant bet kokiam priartinimui ir bet kokiam spausdinimo DPI, nes stulpeliai ir ašys yra tikri PDF kelio operatoriai, o ne pikseliai. Kaina ta, kad jūs patys valdote išdėstymą. Mechanika susiveda į kelis žingsnius: vienas koordinačių apvertimas, kuris visus pagauna, veikianti stulpelinė diagrama, laužtės triukas linijinėms diagramoms ir lanko matematika skritulinės diagramos išpjovoms

Vienintelė sunkioji dalis yra koordinačių sistema

Ekrano grafikoje pradžios taškas yra viršutiniame kairiajame kampe, o Y ašis didėja žemyn. PDF daro priešingai. Pradžios taškas yra puslapio apatiniame kairiajame kampe, o Y ašis didėja aukštyn, matuojama taškais (1/72 colio). Kiekvienas piešimo iškvietimas „HotPDF“ aplinkoje, TextOut, Rectangle, MoveTo, LineTo, Circle, naudoja tą apatinės kairės, Y aukštyn konvenciją. Jei perkelsite ekrano grafikos instinktus, jūsų pirmoji diagrama bus nupiešta aukštyn kojomis ir išlįs pro puslapio apačią

Taigi tikrasis darbas bet kurioje diagramoje yra vienas atvaizdavimas: paversti duomenų reikšmę į Y koordinatę, atsižvelgiant į Y ašies kryptį aukštyn. Nuspręskite dėl braižymo srities stačiakampio, keturių skaičių sričiai, kurioje gyvens diagrama, tada priskirkite mažiausią duomenų reikšmę apatiniam kraštui, o didžiausią – viršutiniam. Stulpeliui, kurio vertė V skalėje nuo 0 iki MaxValue, stulpelio viršutinis kraštas yra PlotBottom + (V / MaxValue) * PlotHeight, ir stulpelis auga į viršų nuo PlotBottom. Teisingai pritaikykite šią išraišką ir visa kita bus tik buhalterija. Žemiau pateiktas pagalbininkas saugo braižymo srities geometriją ir atlieka konversiją, todėl piešimo kodas niekada neliečia grynos aritmetikos du kartus:

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;

Vienas sprendimas slepiasi ties MaxValue. Jei nustatysite ją tiksliai kaip didžiausią duomenų tašką, aukščiausias stulpelis liesis su braižymo srities viršutiniu kraštu ir atrodys nukirptas. Suapvalinkite ją iki švaraus skaičiaus, esančio virš maksimumo, tarkime, kito 10 ar 100 kartotinio, kad diagramoje būtų erdvės viršuje, o tinklelio linijų etiketės būtų skaitomos kaip apvalūs skaičiai, o ne tai, ką atsitiktinai pasiekė duomenys

Stulpelinė diagrama yra stačiakampių ciklas

Išsprendus atvaizdavimą, stulpelinė diagrama pasirašo pati. Padalinkite braižymo srities plotį į vieną lizdą kiekvienai kategorijai, palikite tarpą tarp stulpelių, kad jie nesiliestų, ir nupieškite kiekvieną stulpelį kaip užpildytą stačiakampį, kurio aukštis gaunamas iš ValueToY. „HotPDF“ funkcija Rectangle priima apatinį kairįjį kampą bei plotį ir aukštį, kas tiksliai sutampa su stulpeliu, augančiu aukštyn nuo bazinės linijos. Pirmiausia nustatykite užpildymo spalvą, nutieskite kelią, tada iškvieskite Fill, kad jį nupieštumėte. Kategorijos etiketė eina po bazine linija, o vertė – virš stulpelio:

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;

Dvi detalės atsiperka. Gap (tarpas) yra lizdo dalis, o ne fiksuotas taškų skaičius, todėl stulpeliai išlieka proporcingai atskirti, nesvarbu, ar braižote keturias kategorijas, ar keturiasdešimt. O vertės etiketė išdėstoma to paties iš ValueToY gauto aukščio, kurį naudoja stulpelis, pagrindu, todėl ji visada sėdi tiesiai virš savo stulpelio, o ne plūduriuoja su spėjamu poslinkiu. Jei už stulpelių norite horizontalių tinklelio linijų, nupieškite jas prieš ciklą: pasirinkite tris ar keturias apvalias vertes, kiekvienai iš jų paleiskite ValueToY ir nubrėžkite neryškią liniją per visą braižymo sritį tame Y taške. Pirmiausia jas nupiešus, jos atsiduria už stulpelių PDF naudojamame tapytojo modelio (painter's-model) sluoksniavime

Ašys, padalos ir etiketės tėra dar daugiau linijų ir teksto

Diagrama nebus baigta, kol skaitytojas negalės pasakyti, ką reiškia stulpeliai, ir tai yra visiškai ašių darbas. Vertikali ašis yra viena nubrėžta linija, einanti į viršų palei kairįjį braižymo srities kraštą su saujele padalų žymų ir jų verčių. Pakartotinai naudokite ValueToY, kad padalos atsidurtų toje pačioje skalėje, kurią naudoja stulpeliai, kitaip stulpelis ir jo tinklelio linija nesutaps, o diagrama tyliai meluos:

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;

Etiketės yra ta vieta, kur diagramos dažniausiai lūžta gamyboje, ir gedimas visada tas pats: tekstas, kuris tilpo ekrane, peržengia savo erdvę PDF faile. Ilgi kategorijų pavadinimai susiduria su kaimyniniais, o lokalizuoti mėnesių pavadinimai, pvz., „septembre“ ar „Dezember“, yra platesni nei angliškas „Sep“, su kuriuo testavote. Čia nėra automatinio dydžio keitimo, kuris jus išgelbėtų, todėl palikite tikrą paraštę po bazine linija, sumažinkite šriftą vienu ar dviem taškais tankiems kategorijų rinkiniams, o jei pavadinimai iš tiesų ilgi, pasukite juos. TextOut priima kampą kaip savo trečiąjį argumentą, todėl perdavus 90, etiketė pastatoma ant galo ir gaunate vietos be persidengimo. Ištestuokite išdėstymą su plačiausia numatoma etikete, o ne su trumpiausia, prieš išleidžiant eksportą

Linijinės diagramos: viena laužtė per atvaizduotus taškus

Linijinė diagrama pakartotinai naudoja visą verčių atvaizdavimą ir pakeičia tik tai, kaip sujungiami taškai. Vietoje stačiakampio kiekvienai kategorijai, jūs vieną kartą praeinate per duomenis, konvertuojate kiekvieną vertę į jos (X, Y) naudodami ValueToY ir sujungiate taškus kartu naudodami vieną MoveTo, po kurio seka LineTo iškvietimai, o pabaigoje brėžiamas potėpis (stroked). Pirmasis taškas atidaro kelią; kiekvienas vėlesnis jį pratęsia:

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;

Atkreipkite dėmesį į atstumo skirtumą. Stulpelinė diagrama padalija plotį iš stulpelių skaičiaus, nes kiekvienam stulpeliui priklauso po lizdą. Linijinė diagrama dalija iš intervalų skaičiaus, Count - 1, nes pirmas ir paskutinis taškai sėdi braižymo srities pakraščiuose, o linija apima tarpus tarp jų. Jų sumaišymas yra įprasta priežastis, dėl kurios linijinė diagrama pasislenka puse lizdo nuo stulpelinės diagramos, kurią ji turi perdengti. Jei norite žymeklio ties kiekvienu duomenų tašku, numeskite nedidelį Circle ir Fill ties kiekvienu (X, Y) po to, kai laužtė nubrėžta

Skritulinės diagramos: lankai, arba išpjovos, jei laikotės paprastumo

Skritulinės išpjovos yra viena iš tų formų, kuriai reikia trigonometrijos, nes išpjovą riboja du spinduliai ir lankas. Sąžiningoje versijoje lankas nužymimas brėžiant mažus linijų segmentus išilgai apskritimo, kas pakankamai tiksliai priartina kreivę, jog joks skaitytojas neatskirtų. Kiekvienos išpjovos nužymėjimo kampas yra jos bendros sumos dalis, (Value / Total) * 2π, ir einant ratu jūs kaupiate bėgantįjį kampą:

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;

Kelias iš centro, pirmiausia viršūnė, tada lankas, o tada atgal į viršūnę, suteikia uždarą išpjovą, kurią Fill nudažo tolygiai. Segmentų skaičius keičia lygumą į kelio dydį: maždaug du laipsniai vienam žingsniui atrodo apvaliai esant bet kokiam protingam spinduliui nesukuriant milžiniško turinio srauto. Jei jums nereikia tikro apskritimo, galite visiškai praleisti trigonometriją ir atvaizduoti tuos pačius duomenis kaip vieną horizontalų sukrautą stulpelį, kiekvieno segmento plotis proporcingas jo daliai. Tai bet kokiu atveju dažnai yra lengviau įskaitoma nei skritulys, ir tai yra tiesiog stulpelinės diagramos kodas su stačiakampiais, sudėtais vienas prie kito galais. Naudokite lanko versiją tik tada, kai dizainas reikalauja tikro apskritimo

Niekas iš to nepriklauso nuo įdiegtos diagramų bibliotekos, o tai yra tylus privalumas piešiant primityvus tiesiogiai. Tie patys drobės piešimo iškvietimai, kurie įdeda logotipą ar parašo laukelį, sukuria šias diagramas, o tas pats TextOut, kuris paženklina formos lauką, paženklina ir ašį. Įdėkite braižymo srities geometriją į įrašą (record), vieną kartą priskirkite vertes Y ašiai, ir stulpelinė, linijinė ar skritulinė diagrama bus trumpa rutina virš Rectangle, LineTo ir Circle, kurią galėsite įdėti į bet kokią ataskaitą. Čia naudojami Rectangle, MoveTo, LineTo, Circle, Fill, Stroke ir TextOut iškvietimai yra HotPDF Component, skirto „Delphi“ ir „C++Builder“, dalis