Tekninen artikkeli

Piirrä PDF-kaavioita HotPDF-primitiiveillä

HotPDFissä ei ole chart object -oliota, TPDFChart-luokkaa eikä AddBarSeries-kutsua, joka muuttaisi numerotaulukon valmiiksi kaavioksi. Sen page canvas tarjoaa samat matalan tason PDF-piirtoprimitiivit kuin muutkin PDF-mallit: suorakulmiot, viivat, ympyrät, täytöt, reunaviivat ja tarkkoihin koordinaatteihin sijoitetun tekstin. Siksi kaavio rakennetaan itse. Kun koordinaattimuunnos on tehty, pylväskaavio on suorakulmiosilmukka, viivakaavio polyline ja piirakkakaavio kaariviuhka, ja tuloksen jokainen yksityiskohta pysyy sovelluksen hallinnassa

Tällä on väliä, koska vaihtoehto, johon ihmiset turvautuvat ensimmäisenä, eli ruutukaaviokontrollin (screen chart control) rasterointi bittikartaksi ja kuvan liittäminen sivulle, antaa sinulle näytön resoluutioon lukitun kaavion, joka tulostuu sumeana ja paisuttaa tiedoston. Kaavion piirtäminen HotPDF:n vektoriprimitiiveillä pitää tulosteen terävänä millä tahansa zoomauksella ja millä tahansa tulostimen DPI:llä, koska pylväät ja akselit ovat aitoja PDF:n polkuoperaattoreita, eivät pikseleitä. Hintana on se, että omistat asettelun (layout). Mekaniikka tiivistyy muutamaan siirtoon: yksi koordinaattien kääntö, joka yllättää kaikki, työstetty pylväskaavio, moniviivatemppu viivakaavioille ja kaarimatematiikka piirakan viipaleille

Ainoa vaikea osa on koordinaatisto

Ruutugrafiikka asettaa origon vasempaan yläkulmaan Y:n kasvaessa alaspäin. PDF tekee päinvastoin. Origo istuu sivun vasemmassa alakulmassa ja Y kasvaa ylöspäin, pisteinä (1/72 tuumaa) mitattuna. Jokainen piirtokutsu HotPDF:ssä, TextOut, Rectangle, MoveTo, LineTo, Circle, käyttää tuota vasemman alakulman, Y-ylöspäin -käytäntöä. Jos tuot mukanasi ruutugrafiikan vaistot, ensimmäinen kaaviosi piirtyy ylösalaisin ja karkaa sivun alareunasta yli

Joten todellinen työ missä tahansa kaaviossa on yksi kuvaus (mapping): muuta data-arvo Y-koordinaatiksi, joka kunnioittaa Y-ylöspäin -suuntaa. Päätä juonisuorakulmio (plot rectangle), neljä numeroa alueelle, jossa kaavio elää, ja kuvaa (map) sitten tietojesi pienin arvo alareunaan ja suurin yläreunaan. Arvon V omaavalle pylväälle asteikolla, joka kulkee nollasta arvoon MaxValue, pylvään yläreuna on PlotBottom + (V / MaxValue) * PlotHeight, ja pylväs kasvaa ylöspäin alkaen arvosta PlotBottom. Osu oikeaan tuon yhden lausekkeen kanssa ja kaikki muu on kirjanpitoa. Alla oleva apuohjelma pitää sisällään juonen (plot) geometrian ja tekee muunnoksen, joten piirtokoodi ei koskaan koske raakaan aritmetiikkaan toista kertaa:

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;

Yksi harkintapäätös (judgement call) piileskelee muuttujassa MaxValue. Jos asetat sen täsmälleen suurimpaan arvopisteeseen, pisin pylväs koskettaa kuvaaja-alueen (plot) yläreunaa ja näyttää leikatulta. Pyöristä se ylöspäin puhtaaseen lukuun maksimin yläpuolelle, vaikkapa seuraavaan kymmenen tai sadan kerrannaiseen, jotta kaaviossa on päänvaraa (headroom) ja ruudukkoviivojen (gridlines) nimikkeet luetaan pyöreinä lukuina sen sijaan, mihin data sattuikaan huipentumaan

Pylväskaavio on silmukka suorakulmioiden yli

Kun kuvaus on ratkaistu, pylväskaavio kirjoittaa itse itsensä. Jaa kuvaaja-alueen (plot) leveys yhteen paikkaan (slot) kategoriaa kohden, jätä rako pylväiden väliin, jotta ne eivät kosketa toisiaan, ja piirrä kukin pylväs täytettynä suorakulmiona, jonka korkeus tulee luvusta ValueToY. HotPDF:n Rectangle ottaa vasemman alakulman sekä leveyden ja korkeuden, mikä linjautuu täsmälleen perusviivasta ylöspäin kasvavan pylvään kanssa. Aseta täyttöväri ensin, laske polku (path), ja kutsu sitten menetelmää Fill maalataksesi sen. Kategorian nimike menee perusviivan alapuolelle, arvo pylvään yläpuolelle:

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;

Kaksi yksityiskohtaa ansaitsevat paikkansa. Gap on murto-osa paikasta kiinteän pistemäärän sijaan, joten pylväät pysyvät suhteellisilla etäisyyksillä toisistaan, piirsitpä sitten neljä kategoriaa tai neljäkymmentä. Ja arvonimike sijoitetaan samalla menetelmästä ValueToY johdetulla korkeudella, jota pylväs käyttää, joten se istuu aina juuri oman pylväänsä yläpuolella arvatulla poikkeamalla kellumisen sijaan. Jos haluat vaakasuorat ruudukkoviivat pylväiden taakse, piirrä ne ennen silmukkaa: valitse kolme tai neljä pyöreää arvoa, suorita ValueToY jokaiselle ja piirrä heikko viiva kuvaaja-alueen poikki kyseisessä Y-kohdassa. Niiden piirtäminen ensin asettaa ne pylväiden taakse maalarin mallin (painter's-model) pinoamisessa, jota PDF käyttää

Akselit, ruksit ja nimikkeet ovat vain lisää viivoja ja tekstiä

Kaavio ei ole valmis ennen kuin lukija pystyy kertomaan, mitä pylväät tarkoittavat, ja tuo on täysin akselityötä. Pystyakseli on yksi piirretty (stroked) viiva kuvaaja-alueen (plot) vasenta reunaa ylöspäin varustettuna kourallisella asetusmerkkejä (tick marks) ja niiden arvoja. Käytä menetelmää ValueToY uudelleen, jotta ruksit laskeutuvat samalle asteikolle, jota pylväät käyttävät, muuten pylväs ja sen ruudukkoviiva ovat eri mieltä ja kaavio valehtelee hiljaa:

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;

Nimikkeissä (labels) kaaviot menevät rikki tuotannossa useimmiten, ja vika on aina sama: näytölle mahtunut teksti ylittää tilansa PDF:ssä. Pitkät kategorianimet törmäävät naapureihinsa, ja lokalisoidut kuukausien nimet kuten "septembre" tai "Dezember" ovat leveämpiä kuin englanninkielinen "Sep", jolla testasit. Täällä ei ole automaattista koonmuutosta (autosize) pelastamassa sinua, joten jätä aitoa marginaalia perusviivan alle, kutista fonttia pisteen tai kahden verran tiheille kategoriajoukoille, ja jos nimet ovat todella pitkiä, kierrä niitä. TextOut ottaa kulman kolmantena argumenttinaan, joten luvun 90 välittäminen nostaa nimikkeen pystyyn ja ostaa sinulle tilaa ilman päällekkäisyyksiä. Testaa asettelu leveimmällä odottamallasi nimikkeellä, ei lyhimmällä, ennen kuin vienti (export) toimitetaan

Viivakaaviot: yksi moniviiva kuvattujen pisteiden läpi

Viivakaavio käyttää uudelleen koko arvojen kuvauksen ja muuttaa vain sitä, miten pisteet yhdistyvät. Yhden suorakulmion kategoriaa kohden sijaan kävelet datan läpi kerran, muunnat jokaisen arvon sen (X, Y) -koordinaateiksi ValueToY-menetelmällä ja sidot pisteet yhteen yhdellä MoveTo-kutsulla, jota seuraavat LineTo-kutsut, ja piirrät (stroke) kaiken lopussa. Ensimmäinen piste avaa polun; jokainen myöhempi piste laajentaa sitä:

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;

Huomioi ero väleissä. Pylväskaavio jakaa leveyden pylväiden määrällä, koska kukin pylväs omistaa paikkansa (slot). Viivakaavio jakaa aikavälien (intervals) määrällä, Count - 1, koska ensimmäinen ja viimeinen piste istuvat kuvaaja-alueen (plot) reunoilla ja viiva ulottuu niiden välisten rakojen yli. Näiden kahden sekoittaminen on tavallinen syy siihen, miksi viivakaavio ajelehtii puoli paikkaa sivuun pylväskaaviosta, jonka sen on tarkoitus asettua päälle. Jos haluat merkin jokaiseen arvopisteeseen, pudota pieni Circle ja Fill jokaiseen (X, Y) -kohtaan sen jälkeen, kun moniviiva (polyline) on piirretty

Piirakkakaaviot: kaaret tai viipaleet, jos pidät sen yksinkertaisena

Piirakan viipaleet ovat se yksi muoto, joka vaatii trigonometriaa, koska viipaletta rajaavat kaksi sädettä ja kaari. Rehellinen versio pyyhkäisee kaaren (arc) astumalla pieniä viivasegmenttejä pitkin kehää (circumference), mikä approksimoi käyrää tarpeeksi lähelle, jottei kukaan lukija erota eroa. Kunkin viipaleen pyyhkäisykulma (sweep angle) on sen osuus kokonaisuudesta, (Value / Total) * 2π, ja kerrytät juoksevaa kulmaa sitä mukaa, kun menet ympäri:

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;

Keskustasta ulos -polku (center-out path), huippu (apex) ensin, sitten kaari, sitten takaisin huippuun, antaa suljetun viipaleen, jonka Fill maalaa kiinteästi. Segmenttien määrä vaihtaa sileyttä polun kokoon: karkeasti kaksi astetta askeleessa näyttää pyöreältä millä tahansa järkevällä säteellä tuottamatta valtavaa sisältövirtaa (content stream). Jos et tarvitse todellista ympyrää, voit ohittaa trigonometrian kokonaan ja renderöidä samat tiedot yhtenä vaakasuorana pinottuna pylväänä (stacked bar), kunkin segmentin leveys suhteessa sen osuuteen. Se on usein joka tapauksessa luettavampi kuin piirakka, ja se on vain pylväskoodi (bar code), jossa suorakulmiot on asetettu päittäin. Tartu kaariversioon (arc version) vain, kun suunnittelu vaatii varsinaista ympyrää

Mikään tästä ei riipu minkään kaaviokirjaston asennuksesta, mikä on primitiivien suoraan piirtämisen hiljainen etu. Samat kankaan piirtokutsut (canvas drawing calls), jotka sijoittavat logon tai allekirjoituslaatikon, rakentavat nämä kaaviot, ja sama TextOut, joka nimeää lomakekentän, nimeää akselin. Laita kuvaajan (plot) geometria tietueeseen (record), kuvaa (map) arvot Y:hyn kerran, ja pylväs-, viiva- tai piirakkakaavio on lyhyt rutiini Rectangle-, LineTo- ja Circle-komentojen yli, jonka voit pudottaa mihin tahansa raporttiin. Tässä käytetyt kutsut Rectangle, MoveTo, LineTo, Circle, Fill, Stroke ja TextOut ovat osa HotPDF-komponenttia Delphille ja C++Builderille