<?php
/**
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Copy of GNU Lesser General Public License at: http://www.gnu.org/copyleft/lesser.txt
* Contact author at: hide@address.com
*
*/
/**
* Takes an array or multiple arrays of data and outputs a graph in SVG format.
* The SVG language allows for a high degree of control of the output,
* thus this class is intended to be extended.
*
* @author Herman Veluwenkamp
* @version 1.2alpha
*/
class svgGraph {
/**
* Total width of svg graphic.
* @type integer
* @public
*/
var $graphicWidth = 0;
/**
* Total height of svg graphic.
* @type integer
* @public
*/
var $graphicHeight = 0;
/**
* Width of plot area.
* @type integer
* @public
*/
var $plotWidth = 0;
/**
* Height of plot area.
* @type integer
* @public
*/
var $plotHeight = 0;
/**
* Offset of plot area from left of graphic area.
* @type integer
* @public
*/
var $plotOffsetX = 0;
/**
* Offset of plot area from top of graphic area.
* @type integer
* @public
*/
var $plotOffsetY = 0;
/**
* Padding between outer border of graphic area and text (title and labels).
* @type integer
* @public
*/
var $outerPadding = 0;
/**
* Padding between bottom border of plot area and text (tags).
* @type integer
* @public
*/
var $innerPaddingX = 0;
/**
* Padding between left border of plot area and text (tags).
* @type integer
* @public
*/
var $innerPaddingY = 0;
/**
* Array of data holding values for X axis
* @type string
* @public
*/
var $dataX = array();
/**
* Two dimensional array holding values for Y axis. The key for each array must be unique.
* @type string
* @public
*/
var $dataY = array();
/**
* Title for Graph.
* @type string
* @public
*/
var $title = '';
/**
* Presentation attributes for title.
* @type string
* @public
*/
var $styleTitle = '';
/**
* Default presentation attributes for title.
* @type string
* @public
*/
var $styleTitleDefault = '';
/**
* Label for X axis.
* @type string
* @public
*/
var $labelX = '';
/**
* Presentation attributes for label.
* @type string
* @public
*/
var $styleLabelX = '';
/**
* Default presentation attributes for label.
* @type string
* @public
*/
var $styleLabelXDefault = '';
/**
* Label for Y axis.
* @type string
* @public
*/
var $labelY = '';
/**
* Presentation attributes for label.
* @type string
* @public
*/
var $styleLabelY = '';
/**
* Default presentation attributes for label.
* @type string
* @public
*/
var $styleLabelYDefault = '';
/**
* Offset of first X axis gridline from lower-left of plot area as a
* fraction of normal gridline spacing.
* @type integer
* @public
*/
var $offsetGridlinesX = 0;
/**
* Offset of first Y axis gridline from lower-left of plot area as a
* fraction of normal gridline spacing.
* @type integer
* @public
*/
var $offsetGridlinesY = 0;
/**
* Minimum value for Y axis values. If a lower value is found in data then
* this value is not used.
* @type integer
* @public
*/
var $minY = 0;
/**
* Maximum value for Y axis values. If a higher value is found in data then
* this value is not used.
* @type integer
* @public
*/
var $maxY = 0;
/**
* Resolution for Y axis tags. See notes for method <code>_findRange</code>.
* @type integer
* @public
*/
var $resolutionY = 0;
/**
* Number of decimal places to show for Y axis tags.
* @type integer
* @public
*/
var $decimalPlacesY = 0;
/**
* Number of grid lines corresponding to X axis.
* @type integer
* @public
*/
var $numGridlinesX = 0;
/**
* Number of grid lines corresponding to Y axis.
* @type integer
* @public
*/
var $numGridlinesY = 0;
/**
* Presentation attributes for grid corresponding to X axis.
* @type string
* @public
*/
var $styleGridX = '';
/**
* Default presentation attributes for grid corresponding to X axis.
* @type string
* @public
*/
var $styleGridXDefault = '';
/**
* Presentation attributes for grid corresponding to Y axis.
* @type string
* @public
*/
var $styleGridY = '';
/**
* Default presentation attributes for grid corresponding to X axis.
* @type string
* @public
*/
var $styleGridYDefault = '';
/**
* Presentation attributes for box around plot area.
* @type string
* @public
*/
var $styleBox = '';
/**
* Default presentation attributes for box around plot area.
* @type string
* @public
*/
var $styleBoxDefault = '';
/**
* Presentation attributes for X axis tags.
* @type string
* @public
*/
var $styleTagsX = '';
/**
* Default presentation attributes for X axis tags.
* @type string
* @public
*/
var $styleTagsXDefault = '';
/**
* X axis tags rotation. Negative/Anticlockwise.
* Increase innerPaddingX to prevent overlap with plot area.
* @type integer
* @public
*/
var $rotTagsX = 0;
/**
* Presentation attributes for Y axis tags.
* @type string
* @public
*/
var $styleTagsY = '';
/**
* Default presentation attributes for Y axis tags.
* @type string
* @public
*/
var $styleTagsYDefault = '';
/**
* Y axis tags rotation. Negative/Anticlockwise.
* Increase innerPaddingY to prevent overlap with plot area.
* @type integer
* @public
*/
var $rotTagsY = 0;
/**
* Default presentation attributes for line plots.
* @type string
* @public
*/
var $styleLineDefault = '';
/**
* Default presentation attributes for bar plots.
* @type string
* @public
*/
var $styleBarDefault = '';
/**
* Default presentation attributes for polyline plots (inside group tag).
* @type string
* @public
*/
var $stylePolylineDefault = '';
/**
* Extra SVG to add to graph. e.g. Filters, Defs, Title.
* Note: Title is useful to add if image is viewed out of context.
* <dt><code>$svg</code><dd>
* String holding SVG text.
* </dl>
* @type string
* @public
*/
var $extraSVG = '';
/**
* SVG XML result.
* @type string
* @public
*/
var $svg = '';
/**
* Contains error messages.
* @type string
* @public
*/
var $error = '';
/**
* Define static variables used in the class.
* @returns void
*/
function svgGraph() {
define("DECIMAL_POINT", ".");
define("THOUSANDS_SEPERATOR", ",");
}
/**
* Initialises the variables used for drawing points, lines, grid, and ticks in the plotting area.
* @return Boolean - FALSE if an error was encountered while processing data.
* @returns boolean
*/
function init() {
$this->svg = ''; // complete SVG of graph
$this->svgText = ''; // SVG for outer text labels and title
$this->svgPlot = ''; // SVG for plot including tag text
$this->numGridlinesX = sizeof($this->dataX);
if ($this->numGridlinesX == 0) {
$this->error = 'No data to plot. Check data.';
return FALSE;
}
if ($this->numGridlinesY == 0) {
$this->error = 'No range for Y values. Check number of Y ticks.';
return FALSE;
}
$width = $this->plotWidth - 1;
$height = $this->plotHeight - 1;
// find range of all y values
$allDataY = array();
foreach ($this->dataY as $i => $dataY) $allDataY = array_merge($allDataY, $dataY);
$this->allDataY = $allDataY;
$data = $this->_findRange($allDataY, $this->minY, $this->maxY, $this->resolutionY);
$this->dataMinY = $data['min'];
$this->dataMaxY = $data['max'];
if (($this->dataMaxY - $this->dataMinY) == 0) {
$this->error = 'No range to plot. Check data.';
return FALSE; // data error
}
// find spacing for ticks and grid lines
$this->deltaTicksX = $width / ($this->numGridlinesX - 1 + (2 * $this->offsetGridlinesX));
$this->deltaTicksY = $height / ($this->numGridlinesY - 1 + (2 * $this->offsetGridlinesY));
$this->factorY = $height / ($this->dataMaxY - $this->dataMinY);
// format text for tags on Y axis
//$deltaTagsY = $this->dataMaxY / ($this->numGridlinesY - 1 + (2 * $this->offsetGridlinesY));
$deltaTagsY = ($this->dataMaxY - $this->dataMinY) / ($this->numGridlinesY - 1 + (2 * $this->offsetGridlinesY));
//$factorTagsY = $this->dataMaxY / ($this->dataMaxY - $this->dataMinY);
for ($i=0; $i<$this->numGridlinesY; $i++) {
$text = $this->dataMinY + $deltaTagsY * ($i + $this->offsetGridlinesY);
$this->tagsY[$i] = number_format($text, $this->decimalPlacesY, DECIMAL_POINT, THOUSANDS_SEPERATOR);
}
$this->tagsY = array_reverse($this->tagsY);
if (empty($this->tagsY)) {
$this->error = 'No Y axis data.';
return FALSE;
}
return TRUE;
}
/**
* Calls functions to draw title, labels, tags, grid lines, and box of graph.
* @returns void
*/
function drawGraph() {
$this->drawOuterText();
$this->drawGridX();
$this->drawGridY();
$this->drawBox();
$this->drawTagsX();
$this->drawTagsY();
}
/**
* Draw the title and axis labels around the outside of the graphic area.
* @returns void
*/
function drawOuterText() {
$outerPadding = $this->outerPadding;
// draw title
if (!empty($this->title)) { // check if there is something to draw
$offset = $this->graphicWidth / 2;
$transform = "transform='translate(0, $outerPadding)'";
$this->svgText .= "<text text-anchor='middle' $transform x='$offset' y='1em' ";
$this->svgText .= "style='{$this->styleTitleDefault}{$this->styleTitle}'>";
$this->svgText .= "{$this->title}</text>\n";
}
//draw label Y
if (!empty($this->labelY)) { // check if there is something to draw
$offset = $this->plotOffsetY + ($this->plotHeight / 2);
$transform = "transform='translate($outerPadding, 0) rotate(-90 0 $offset)'";
$this->svgText .= "<text text-anchor='middle' $transform x='0' y='$offset' dy='1em' ";
$this->svgText .= "style='{$this->styleLabelYDefault}{$this->styleLabelY}'>";
$this->svgText .= "{$this->labelY}</text>\n";
}
//draw label X
if (!empty($this->labelX)) { // check if there is something to draw
$offset = $this->plotOffsetX + ($this->plotWidth / 2);
$transform = "transform='translate(0, -$outerPadding)'";
$this->svgText .= "<text text-anchor='middle' $transform x='$offset' y='{$this->graphicHeight}' ";
$this->svgText .= "style='{$this->styleLabelXDefault}{$this->styleLabelX}'>";
$this->svgText .= "{$this->labelX}</text>\n";
}
}
/**
* Draws the grid lines from top to bottom in the plotting area.
* @returns void
*/
function drawGridX() {
$this->svgPlot .= "<g style='{$this->styleGridXDefault}{$this->styleGridX}'>\n";
$top = 0;
$bottom = $this->plotHeight - 1;
foreach ($this->dataX as $i => $x) {
$u = $this->deltaTicksX * ($i + $this->offsetGridlinesX);
$this->svgPlot .= "<line x1='$u' y1='$top' x2='$u' y2='$bottom'/>\n";
}
$this->svgPlot .= "</g>\n";
}
/**
* Draws the grid lines from right to left in the plotting area.
* @returns void
*/
function drawGridY() {
$this->svgPlot .= "<g style='{$this->styleGridYDefault}{$this->styleGridX}'>\n";
$left = 0;
$right = $this->plotWidth - 1;
for ($i = 0; $i < $this->numGridlinesY; $i++) {
$v = $this->deltaTicksY * ($i + $this->offsetGridlinesY);
$this->svgPlot .= "<line x1='$left' y1='$v' x2='$right' y2='$v'/>\n";
}
$this->svgPlot .= "</g>\n";
}
/**
* Draws the box around the plotting area.
* @returns void
*/
function drawBox() {
$width = $this->plotWidth - 1;
$height = $this->plotHeight - 1;
$this->svgPlot .= "<g style='{$this->styleBoxDefault}{$this->styleBox}'>\n";
$this->svgPlot .= "<line x1='0' y1='0' x2='$width' y2='0'/>\n";
$this->svgPlot .= "<line x1='0' y1='$height' x2='$width' y2='$height'/>\n";
$this->svgPlot .= "<line x1='$width' y1='0' x2='$width' y2='$height'/>\n";
$this->svgPlot .= "<line x1='0' y1='0' x2='0' y2='$height'/>\n";
$this->svgPlot .= "</g>\n";
}
/**
* Draws the axis tag text outside the plotting area on the x axis.
* @returns void
*/
function drawTagsX() {
$this->svgPlot .= "<g style='{$this->styleTagsXDefault}{$this->styleTagsX}'>\n";
$bottom = $this->plotHeight - 1;
$innerPadding = $this->innerPaddingX;
foreach ($this->dataX as $i => $text) {
$u = $this->deltaTicksX * ($i + $this->offsetGridlinesX);
if ($this->rotTagsX == 0) {
$transform = "transform='translate(0, $innerPadding)'";
$this->svgPlot .= "<text text-anchor='middle' $transform dy='1em' x='$u' y='$bottom'>$text</text>\n";
} else if($this->rotTagsX > 0) {
$transform = "transform='translate(0, $innerPadding) rotate({$this->rotTagsX} $u $bottom)'";
$this->svgPlot .= "<text text-anchor='start' $transform x='$u' y='$bottom'>$text</text>\n";
} else {
$transform = "transform='translate(0, $innerPadding) rotate({$this->rotTagsX} $u $bottom)'";
$this->svgPlot .= "<text text-anchor='end' $transform x='$u' y='$bottom'>$text</text>\n";
}
}
$this->svgPlot .= "</g>\n";
}
/**
* Draws the axis tag text outside the plotting area on the y axis.
* @returns void
*/
function drawTagsY() {
if (empty($this->tagsY)) return; // no data to plot. error should be picked up by init method.
$this->svgPlot .= "<g style='{$this->styleTagsYDefault}{$this->styleTagsY}'>\n";
$innerPadding = $this->innerPaddingY;
foreach ($this->tagsY as $i => $text) {
$v = $this->deltaTicksY * ($i + $this->offsetGridlinesY);
if ($this->rotTagsY == 0) {
$transform = "transform='translate(-$innerPadding, 0)'";
$this->svgPlot .= "<text text-anchor='end' $transform x='0' dy='0.5em' y='$v'>$text</text>\n";
} else {
$transform = "transform='translate(-$innerPadding, 0) rotate({$this->rotTagsY} 0 $v)'";
$this->svgPlot .= "<text text-anchor='end' $transform x='0' dy='0.5em' y='$v'>$text</text>\n";
}
}
$this->svgPlot .= "</g>\n";
}
/**
* Draw a line from one point to the next continuously without stopping to draw markers.
* This method is used for drawing lines with markers on the end, for example, an arrow indicating trend.
* <br><br>
* The format parameter array for the selected dataset can have two members:<br>
* 'style' - Style for line,<br>
* 'attributes' - Attributes to place inside polyline tag.
* @param $whichDataSet Which set of data to draw. This is the index of the data array to be used.
* @return BOOLEAN FALSE if no style is defined for the data set selected.
* @returns boolean
*/
function polyLine($whichDataSet) {
if (empty($this->format[$whichDataSet]['style'])) {
$this->error = 'No style defined for data plot.';
return FALSE; // data error
}
$attributes = empty($this->format[$whichDataSet]['attributes']) ?
'' : $this->format[$whichDataSet]['attributes'];
$this->svgPlot .= "<g style='{$this->stylePolylineDefault}{$this->format[$whichDataSet]['style']}'>\n";
$this->svgPlot .= "<polyline $attributes ";
$u = 0;
$v = 0;
foreach ($this->dataX as $i => $x) {
$y = $this->dataY[$whichDataSet][$i];
$u = $this->deltaTicksX * ($i + $this->offsetGridlinesX);
$v = $this->factorY * ($y - $this->dataMinY);
$v = $this->plotHeight - $v;
if ($i==0) $this->svgPlot .= "points='$u,$v";
else $this->svgPlot .= " $u,$v ";
$oldU = $u;
$oldV = $v;
}
$this->svgPlot .= "'/>\n</g>\n";
return TRUE;
}
/**
* Draw line from one point to the next stopping at each.
* This method is used for drawing lines with markers at each plot point.
* <br><br>
* The format parameter array for the selected dataset can have two members:<br>
* 'style' - Style for line,<br>
* 'attributes' - Attributes to place inside line tag.
* @param $whichDataSet Which set of data to draw. This is the index of the data array to be used.
* @returns void
*/
function line($whichDataSet) {
if (empty($this->format[$whichDataSet]['style'])) {
$this->error = 'No style defined for data plot. Check parameters.';
return FALSE; // data error
}
$attributes = empty($this->format[$whichDataSet]['attributes']) ?
'' : $this->format[$whichDataSet]['attributes'];
$this->svgPlot .= "<g style='{$this->styleLineDefault}{$this->format[$whichDataSet]['style']}'>\n";
$u = 0;
$v = 0;
foreach ($this->dataX as $i => $x) {
$y = $this->dataY[$whichDataSet][$i];
$u = $this->deltaTicksX * ($i + $this->offsetGridlinesX);
$v = $this->factorY * ($y - $this->dataMinY);
$v = $this->plotHeight - $v;
if ($i==0) {
$oldU = $u;
$oldV = $v;
}
$this->svgPlot .= "<line $attributes x1='$oldU' y1='$oldV' x2='$u' y2='$v'/>\n";
$oldU = $u;
$oldV = $v;
}
$this->svgPlot .= "</g>\n";
return TRUE;
}
/**
* Draw a bar for each data point from the data set selected.
* @param $whichDataSet Which set of data to draw. This is the index of the data array to be used.
* <br><br>
* The format parameter array for the selected dataset can have three members:<br>
* 'style' - Style for bar,<br>
* 'barWidth' - Width of bar as fraction of distance between gridlines.
* Values greater than 1 will result in bars overlapping.<br>
* 'barOffset' - Offset of the bar as fraction of bar width. By default it is centered on the gridline.
* @return BOOLEAN FALSE if style, barWidth, or barOffset parameters are missing.
* @returns boolean
*/
function bar($whichDataSet) {
if (!isset($this->format[$whichDataSet]['style']) ||
!isset($this->format[$whichDataSet]['barWidth']) ||
!isset($this->format[$whichDataSet]['barOffset'] )) {
$this->error = 'Style parameters missing for bar plot.';
return FALSE; // data error
}
$this->svgPlot .= "<g style='{$this->styleBarDefault}{$this->format[$whichDataSet]['style']}'>\n";
$barWidth = $this->format[$whichDataSet]['barWidth'] * $this->deltaTicksX;
$barOffset = $this->format[$whichDataSet]['barOffset'] * $barWidth;
$u = 0;
$v = 0;
foreach ($this->dataX as $i => $x) {
$y = $this->dataY[$whichDataSet][$i];
$u = $this->deltaTicksX * ($i + $this->offsetGridlinesX) - ($barWidth / 2) + $barOffset;
$v = $this->factorY * ($y - $this->dataMinY);
$v = $this->plotHeight - $v;
$height = $this->factorY * ($y - $this->dataMinY) - 1;
$this->svgPlot .= "<rect x='$u' y='$v' width='$barWidth' height='$height'/>\n";
}
$this->svgPlot .= "</g>\n";
return TRUE;
}
/**
* Find the maximum and minimum values for a set of data.<br>
* The $resolution variable is used for rounding maximum and minimum values.<br>
* If maximum value is 8645 then<br>
* If $resolution is 0, then maximum value becomes 9000.<br>
* If $resolution is 1, then maximum value becomes 8700.<br>
* If $resolution is 2, then maximum value becomes 8650.<br>
* If $resolution is 3, then maximum value becomes 8645.<br>
* @param $data Data to find the range for
* @param $min Minimum value to start at. If a lower number is found then this value is not used.
* @param $max Maximum value to start at. If a larger number is found then this value is not used.
* @param $resolution Resolution for range.
* @returns array
* @private
*/
function _findRange($data, $min, $max, $resolution) {
if (sizeof($data) == 0 ) return array('min' => 0, 'max' => 0);
foreach ($data as $key => $value) {
if ($value=='none') continue;
if ($value > $max) $max = $value;
if ($value < $min) $min = $value;
}
if ($max == 0) {
$factor = 1;
} else {
if ($max < 0) $factor = - pow(10, (floor(log10(abs($max))) + $resolution) );
else $factor = pow(10, (floor(log10(abs($max))) - $resolution) );
}
$max = $factor * @ceil($max / $factor);
$min = $factor * @floor($min / $factor);
return array('min' => $min, 'max' => $max);
}
/**
* Generate SVG for entire graph.
* @returns void
*/
function generateSVG() { //enableZoomAndPanControls='false'
$this->svg = "<?xml version='1.0' encoding='iso-8859-1'?>\n";
$this->svg .= "<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 20000303 Stylable//EN' ";
$this->svg .= "'http://www.w3.org/TR/2000/03/WD-SVG-20000303/DTD/svg-20000303-stylable.dtd'>\n";
$this->svg .= "<svg width='{$this->graphicWidth}' height='{$this->graphicHeight}'>\n";
if (!empty($this->extraSVG)) {
$this->svg .= "<!-- Extra SVG -->\n";
$this->svg .= $this->extraSVG."\n";
$this->svg .= "<!-- End Extra SVG -->\n";
}
if (!empty($this->svgText)) {
$this->svg .= "<!-- Outer Text -->\n";
$this->svg .= "<g>\n";
$this->svg .= $this->svgText."\n";
$this->svg .= "</g>\n" ;
$this->svg .= "<!-- End Outer Text -->\n";
}
$this->svg .= "<!-- Plot Area -->\n";
$this->svg .= "<g transform='translate({$this->plotOffsetX},{$this->plotOffsetY})'>\n";
$this->svg .= $this->svgPlot."\n";
$this->svg .= "</g>\n" ;
$this->svg .= "<!-- End Plot Area -->\n";
$this->svg .= "</svg>\n" ;
}
/**
* Output SVG as XML text including appropriate HTTP header information.
* @returns void
*/
function outputSVG() {
if (empty($this->svg)) $this->generateSVG();
header("Content-type: image/svg+xml");
print $this->svg;
}
}
?>