Použití GnuPlot pro tvorbu grafů v PHP

Úvod

Určitě se mnoho z vás dostalo do situace, kdy potřebovali prezentovat svá data na internetu v grafické podobě. Má-li být graf statický, je jednou z možností nastartovat nějaký kancelářský balík a uložit ručně vytvořený graf jako obrázek. Pro tvorbu dynamických grafů běžná distribuce linuxu zdánlivě nic rozumného nenabízí. Pro jednodušší grafy lze ale celkem snadno použít program GnuPlot, který je běžnou součástí různých distribucí už dlouhá léta.

GnuPlot je určený spíše na vizualizaci vědeckých dat. Jenže co si budeme vykládat - veškerá matematická věda nakonec stejně skončí u počítání peněz. Použitelnost programu GnuPlot je pro grafy ekonomického zaměření ale poněkud omezená. Nos budou nad výstupy programu GnuPlot ohrnovat především lidé s otiskem telefonního sluchátka na uchu - obchodníci, marketingoví specialisté a jim podobná sorta podnikové fauny. Grafy z GnuPlot jim budou připadat málo barevné, málo koláčové, málo interaktivní, málo jásavé a celkově hnusné. Ovšem pomineme-li nároky této specifické kategorie lidí, dá se GnuPlot použít i na velikou spoustu ekonomických a statistických grafů.

Typické použití programu GnuPlot můžete vidět na příkladu grafu charakteristiky solárního článku.

Charakteristika solárního článku - typické využítí GnuPlot

Interaktivní režim - seznámení s GnuPlot

Gnuplot nastartujte nejlépe v X-terminálu příkazem gnuplot. Dostanete se na povelovou řádku - i když ta není zdaleka tak komfortní jako třeba povelová řádka bash, můžete v ní procházet příkazy v historii a editovat je (marně ale budete toužit po příkazové řádce ve stylu svého oblíbeného editoru vi). Můžete hned vyzkoušet první graf:

plot sin(x);

GnuPlot si volí meze grafu podle vlastního uvážení. Pokud vám nevyhovují meze vybrané programem, můžete zadat hranice grafu vlastní:

plot [-3.14:3.14] sin(x);

Hranice lze zadat jak pro vodorovnou, tak pro svislou osu. Hranice se zadávají v hranatých závorkách hned za příkazem plot, ještě před zadáním vykreslovaných funkcí. První zadaná hranice platí pro vodorovnou osu - v našem příkladu se zobrazuje sinusovka od -3.14 do +3.14. Další v hranatých závorkách zadaná hranice platí pro osu svislou.

GnuPlot umí pracovat i s proměnnými a funkcemi. Složitější funkce lze naprogramovat předem a použít později například na výpočty, iterace a jiné různé psí kusy:

pi=3.1415926535;
f(x)=sin(x)/cos(x)
plot [-pi:pi] f(x);

Spíše než funkce nás bude zajímat zobrazování dat z externích zdrojů. Vytvořte si textový soubor se jménem x.data s následujícím (či podobným obsahem):

1 40 5
2 50 6
3 50 8
6 70 3
8 12 2
9 10 1
10 6 4

V GnuPlot si pak vyzkoušejte několik jednoduchých grafů:

plot "x.data";
plot "x.data" using 1 with lines;
plot "x.data" using 2 with lines;
plot "x.data" using 1:2 with lines;

První příkaz vykreslí do grafu několik puntíků - i když je v tomto případě jednoduché vysledovat podle dat, co GnuPlot kreslí, ve složitějších grafech to není tak snadné. Parametrem using můžete lépe specifikovat, jak má GnuPlot datový soubor interpretovat.

Příkaz plot s parametrem using 1 nakreslí prostý výčet hodnot z prvního sloupce souboru, plot s parametrem using 2 nakreslí výčet hodnot z druhého sloupce souboru. Parametr using 1:2 použije první sloupec jako vodorovnou souřadnici a druhý sloupec jako svislou souřadnici.

Nakonec jsem si nechal trochu složitější příklad grafu, který lze využít například pro zobrazení průměrů, související standardní odchylky a podobných statistických záležitostí:

plot "x.data" using 1:2 with lines, "x.data" using 1:2:3 with errorbars;

Příkazem plot nemusíte vykreslovat do grafu pouze jednu hodnotu - v tomto případě se nakreslí normální čárový graf a do každého zadaného bodu se nakreslí ještě svislá čára, jejíž délka je určená třetím zadaným parametrem (using 1:2:3).

Dávkový režim

Stejně dobře, jako funguje GnuPlot v interaktivní režimu, může pracovat v režimu dávkovém, a graf vykreslovat místo na obrazovku přímo ve formátu například gif na standardní výstup. Toho se dá využít a přesměrovat výstup přes http server přímo na http klienta.

Starší verze programu GnuPlot umějí pracovat ještě s formátem gif, novější verze (určitě verze 4) už formát gif nepodporují a alespoň podle manuálu při požadavku o formát gif generují místo toho formát png.

Bohužel starší verze programu GnuPlot neumožňují při generování png formátu zadat ani tak základní věc, jako je požadovaná velikost grafu. Vzhledem k tomu a vzhledem k velkému množství historických grafů používám vesele formát gif ve verzi 3.7 a možnosti, které mi nabízejí novější verze, jsem s lehkým srdcem oželel. Ve verzi 4 se podpora png formátu něco málo změnila a použitelným se tak stal i formát png. Protože ale nemám nikde nainstalovanou verzi 4, použiji v příkladech raději formát gif a nebudu riskovat, že něco z příkladů nebude fungovat, protože to nebylo kde vyzkoušet.

Pro volání programu GnuPlot si vytvořte jednoduchý skript v PHP. Skriptu se v proměnné TEXT posílá jediný parametr - jméno souboru, ve kterém je uložený příkazový soubor pro GnuPlot.

<?
Header("Content-type: image/gif");
Header("Pragma: no-cache");
Header("Cache-Control: no-cache");
Header("Expires: ".GMDate("D, d M Y H:i:s")." GMT");
$TEXT = basename ($TEXT);
$TEXT = "/tmp/".$TEXT;
# Nelze použít funkci PassThru() - nebafruje a je pomalá.
$fd = popen("gnuplot $TEXT", "r");
while ( ($X = fread($fd, 8192)) ) {
    print $X;
    }
fclose ($fd);
# Datové a příkazové soubory NEMAZAT, jinak nelze
# graf vytisknout v MSIE!
?>

Jako ukázkový příklad jsem zvolil Z-graf pro sledování měsíčního plánu výroby. Data i s příkazy pro tvorbu tabulek jsou přibalena v souboru gnuplot.sql. Předpokládám databázi PostgreSQL.

Z-graf: plnění měsíčního plánu výroby

Celý graf byl vytvořený následujícím skriptem. Kvůli jednoduchosti není ve skriptu prakticky žádné ošetření chyb. Snažil jsem se do skriptu napsat něco málo komentářů, takže by vám jeho pochopení nemuselo dělat větší potíže.

<!DOCTYPE HTML
    PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<?
# Připojení k databázi
$DB = pg_connect ("host=dbserver dbname=pokusy");

# Dotaz vyhledávající potřebná data v tabulkách.
$ri = pg_exec ($DB, "
set datestyle to 'german';
select 
   to_char(datum, 'DD.MM.') as datum,
   vyroba as denni_vyroba,
   extract('dow' from datum) as dow,
   int4(datum - '1.4.2004'::date + 1) as den,
   (select sum(vyroba)
      from pk_vyroba v2
     where v2.datum>=v1.datum-'30 days'::interval
       and v2.datum<=v1.datum
       ) as klouzavy_soucet,
   (select sum(vyroba)
      from pk_vyroba v3
     where v3.datum<=v1.datum
       and v3.datum>='1.4.2004'::date) as vyrobeno
    from pk_vyroba v1
   where v1.datum>='1.4.2004'
    order by datum;
");

# Data máme v paměti - uložíme si je do dočasného souboru,    
# ze kterého je pak bude lovit gnuplot.
$Fd1 = tempnam ("/tmp", "graf.dt.");
$fd = fopen ($Fd1, "w");
for ($i=0; $i<pg_numrows($ri); $i++) {
    $data = pg_fetch_object ($ri, $i);
    # Do datového souboru zapíšeme všechny hodnoty.
    fwrite ($fd, $data->den." ".
                 $data->denni_vyroba." ".
                 $data->vyrobeno." ".
                 $data->klouzavy_soucet."\n");
    }
fclose ($fd);

# Nyní vytvoříme soubor s příkazy pro gnuplot
$Fprg = tempnam ("/tmp", "graf.pr.");
$fd = fopen ($Fprg, "w");
fwrite ($fd, "
set terminal gif transparent size 480, 340 \\
    xffffff x000000 xa0a0a0 x909000 x000000 xaa9090
set output;
set grid;
set key left reverse;
set bmargin 4; set xtics rotate (\\
");

# Popisky na vodorovné ose - kreslí se pouze někde.
# V příkazovém souboru vypadají popisky asi takto:
# set xtics rotate (\
#      "1.4.2004" 1,\
#      "2.4.2004" 2,\
#      "3.4.2004" 3);
for ($ODDELOVAC="", $i=0; $i<pg_numrows($ri); $i++) {
    $data = pg_fetch_object($ri, $i);
    if ($data->dow==0 || $data->dow==6) continue;
    fwrite ($fd, "$ODDELOVAC\"$data->datum\" $data->den");
    $ODDELOVAC=",\\\n";
    }
fwrite ($fd, ");");

# Nalezení plánovaného objemu výroby v databázi
$ri = pg_exec ($DB, "select 
     int4(datum-'1.4.2004'::date+1) as den, plan 
     from pk_plan;");
$data = pg_fetch_object ($ri, 0);

fwrite ($fd, "plot [1:30] \
  \"$Fd1\" using 1:2 title \"Denní výroba\" with linespoints,\
  \"$Fd1\" using 1:3 title \"Vyrobeno\" with linespoints,\
  \"$Fd1\" using 1:4 title \"Klouzavý úhrn\" with linespoints,\
  \"-\"    using 1:2 title \"Plán\" with lines;
0 0
$data->den $data->plan
e
");
fclose ($fd);

print "<img src=\"graf.php3?TEXT=".URLencode($Fprg)."\">\n";

pg_close($DB)
?>
</body>
</html>

Užitečné fígle

Podle výše uvedeného příkladu lze vyrobit velkou část požadovaných grafů. Další grafy lze vyrobit většinou stejně jednoduše - největší práci dá pročítání dokumentace, případně prohledávání dříve vyrobených grafů a zjišťování "jak jsem to dělal posledně?". Shrnuté zkušenosti stačí na tvorbu drtivé většiny grafů, které kdy od GnuPlot můžete chtít.

Nápověda

GnuPlot má vestavěnou velmi podrobnou dokumentaci. K ní se dostanete přímo z povelové řádky, například:

help plot

vypíše nápovědu k příkazu plot. Nicméně takto zpracovaná dokumentace může být pro někoho mírně nepřehledná, a bude mu lépe vyhovovat dokumentace na internetu: http://www.gnuplot.info/docs/gnuplot.html.

Program GnuPlot poměrně inteligentně reaguje na proměnnou PAGER. Nevyhovuje-li vám pro prohlížení použitý more, zkuste před startem nastavit proměnnou PAGER:

export PAGER=less gnuplot

Typy grafů

Parametrem with lze měnit typ grafů. Z nejpoužívanějších typů se celkem normálně chovají lines, points, linespoints, dots a impulses. Trochu komplikovanější chování je u typů errorbars, boxes a podobně - tyto datové typy používají tři rozměry místo obvyklých dvou. U takových grafů se třetí rozměr zadává v parametru using jako další číslo sloupce:

plot "data" using 1:2:3 with errorbars;

Sloupcový graf

Sloupcový graf se vytváří příkazem plot s parametrem with boxes. Většinou má ale GnuPlot o sloupcovém grafu jinou představu než uživatelé.

Není-li zadaný údaj jiným způsobem, volí GnuPlot šířku sloupce automaticky tak, aby byly jednotlivé sloupce v grafu přilepené jeden na druhém. Jednoduše lze šířku sloupce nastavit příkazem set boxwidth 0.5. Hodnota 1 odpovídá maximální šířka sloupce jako při nezadané hodnotě (tedy sloupce přilepené jeden na druhém). Kromě toho lze šířku sloupce zadat přímo v datovém souboru a použít v příkazu plot parametr using 1:2:3 - z prvního sloupce datového souboru se pak bere vodorovná souřadnice, z druhého sloupce výška boxu a ze třetího sloupce šířka boxu. Každý sloupec tedy může mít jinou šířku.

Ve starších verzích nejsou sloupce vyplněné. Od verze 4 je možné vyplnit sloupce barvou nebo vzorem pomocí příkazu set style fill.

Popis datových hodnot

GnuPlot normálně popisuje datové hodnoty způsobem, který většinou asi nebude vyhovovat. Každou hodnotu lze popsat zvlášť pomocí parametru title příkazu plot. Popis jedné vybrané hodnoty lze potlačit parametrem notitle.

plot "x" using 1:2 title "Prodej v lednu", "x" using 1:3 notitle;

Přesun popisků jinam

Často se stane, že popisky zasahují do vykreslovaných hodnot a zavazejí. Přesunout jinam či úplně potlačit se dají příkazem set key. Například příkaz

set key left reverse;

přesune popisky do levého horního rohu a obrátí pořadí popisků - první se bude zobrazovat styl hodnoty a pak textový popis. Popisky se dají úplně potlačit příkazem set nokey - ve verzi 4 příkazem set key off.

Popis vodorovné a svislé osy

Vodorovnou a svislou osu lze popsat příkazem set:

set xlabel "Vodorovná osa"; set ylabel "Svislá osa";

Možná ale stejně jako já zjistíte, že popisovat osy tímto způsobem není to pravé ořechové a na popisování os rezignujete.

Značky na osách

GnuPlot volí meze a hustotu stupnice os automaticky. Často se ale do grafu vykreslují například hodnoty po jednotlivých dnech a pak je vhodné mít v popiscích uvedené konkrétní datumy, nikoliv nic neříkající čísla.

set bmargin 8;
set xtics rotate ("1.týden" 1, "2.týden" 7, "3.týden" 14);

V uvedeném příkladu se bude zobrazovat popis pouze každé sedmé hodnoty a místo čísla se vypíše uvedený text. Aby se texty v grafu nepřekrývaly, jsou popisky parametrem rotate otočené o devadesát stupňů. Protože GnuPlot má pro popisy vyhražené pouze omezené místo, je zvětšený příkazem set bmargin spodní okraj grafu.

Se značkami souvisí i kreslení mřížky v samotném grafu. Mřížka se zapíná nebo vypíná příkazy set grid případně set nogrid.

Barvy

V příkazu set terminal gif je pamatováno i na barvy. Ty se zadávají v hexadecimálním vyjádření s pískenkem x na začátku a v obvyklém pořadí RRGGBB (červená, zelená a modrá složka) - například x00ff00 znamená zelenou barvu. Zadané barvy interpretuje GnuPlot v tomto pořadí: pozadí, rámeček, mřížka a následují barvy jednotlivých zobrazovaných hodnot.

Vstup dat z příkazové řádky

Stejně jako ze souboru lze zobrazovat i data z příkazové řádky. Místo jména souboru se zadá pomlčka a hodnoty se zadávají za příkaz plot. Celý datový blok končí písmenem e na prázdném řádku. Tímto způsobem se dá zobrazovat i několik datových setů za sebou - z příkazové řádky se čtou hodnoty v tom pořadí, v jakém byly zadané v příkazu plot:

plot '-' using 1:2 title 'Pmax' with steps, '-' using 1:2 title 'I450';
0 10
4 0
e
0.450 3.5
e

Výpočty a podmíněné hodnoty

Občas je potřeba provést nějaké výpočty přímo v programu GnuPlot. Dá se to udělat v parametru using:

plot "soubor" using 1:($2==0 ? 1/0 : $3) with impulses;

Uvedený příklad kreslí na vodorovné souřadnici (zadaná ve sloupci 1) hodnotu ze sloupce 3, pouze v případě, že sloupec 2 obsahuje nulu, nevykresluje nic (jednička dělená nulou má v GnuPlot svůj význam). Podobným způsobem lze provádět s daty i různé výpočty.

Vyhlazené hodnoty

V některých typech grafů jsou užitečnější vyhlazené hodnoty, než nezpracovaná data. Vyhlazené hodnoty by vás určitě zajímaly v případě, že byste chtěli sledovat například oblíbenou výši "průměrného" platu a počty lidí pobírající určitou částku. Ve výrobních podnicích se podobným způsobem bude sledovat zase statistické rozdělení výrobků podle kvality a podobně:

plot "data" using 1:2 with impulses, "data" using 1:2 smooth bezier with lines;

Vyhlazení zobrazovaných hodnot bezierovou křivkou

Kromě parametru bezier lze použít i parametr csplines a další.

Ladění

GnuPlot volaný z PHP skriptu se dost špatně ladí. Chybové hlášky z programu najdete obvykle v chybovém logu web serveru (nejspíše někde ve /var/log/http/error_log). Nepomůže-li vám chybové hlášení z logu, můžete zkusit najít příkazový soubor v adresáři /tmp. Nezapomeňte ale na to, že v souboru je příkazy set terminal gif... ; set output; přesměrovaný výstup místo na obrazovku na standardní výstup.

Závěrem

Určitě jste poznali, že tento článek není žádným uceleným návodem na výrobu grafů v PHP a GnuPlot. Snažil jsem se jen upozornit na jednu z možností, jak obohatit www stránky o jednoduché grafické výstupy a ulehčit vám hledání v manuálech při řešení triviálních problémů (popisy os, popisy hodnot a podobně).