Repository URL to install this package:
|
Version:
0.0.1-0 ▾
|
zendcddemo-apps-gotcms-project
/
zendcddemo-files
/
apps
/
gotcms_project
/
vendor
/
SVGGraph
/
SVGGraphGridGraph.php
|
|---|
<?php
/**
* Copyright (C) 2009-2012 Graham Breach
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* For more information, please contact <graham@goat1000.com>
*/
require_once 'SVGGraphAxis.php';
require_once 'SVGGraphAxisFixed.php';
define("SVGG_GUIDELINE_ABOVE", 1);
define("SVGG_GUIDELINE_BELOW", 0);
abstract class GridGraph extends Graph {
protected $bar_unit_width = 0;
protected $x0;
protected $y0;
protected $y_points;
protected $x_points;
/**
* Set to true for horizontal graphs
*/
protected $flip_axes = false;
/**
* Set to true for block-based labelling
*/
protected $label_centre = false;
protected $g_width = null;
protected $g_height = null;
protected $uneven_x = false;
protected $uneven_y = false;
protected $label_adjust_done = false;
protected $axes_calc_done = false;
protected $sub_x;
protected $sub_y;
protected $guidelines = array();
protected $min_guide = array('x' => null, 'y' => null);
protected $max_guide = array('x' => null, 'y' => null);
private $label_left_offset;
private $label_bottom_offset;
/**
* Modifies the graph padding to allow room for labels
*/
protected function LabelAdjustment($longest_v = 1000, $longest_h = 100)
{
// deprecated options need converting
// NOTE: this works because graph settings become properties, whereas
// defaults only exist in the $this->settings array
if(isset($this->show_label_h) && !isset($this->show_axis_text_h))
$this->show_axis_text_h = $this->show_label_h;
if(isset($this->show_label_v) && !isset($this->show_axis_text_v))
$this->show_axis_text_v = $this->show_label_v;
// if the label_x or label_y are set but not _h and _v, assign them
$lh = $this->flip_axes ? $this->label_y : $this->label_x;
$lv = $this->flip_axes ? $this->label_x : $this->label_y;
if(empty($this->label_h) && !empty($lh))
$this->label_h = $lh;
if(empty($this->label_v) && !empty($lv))
$this->label_v = $lv;
if(!empty($this->label_v)) {
// increase padding
$lines = $this->CountLines($this->label_v);
$this->label_left_offset = $this->pad_left + $this->label_space +
$this->label_font_size;
$this->pad_left += $lines * $this->label_font_size +
2 * $this->label_space;
}
if(!empty($this->label_h)) {
$lines = $this->CountLines($this->label_h);
$this->label_bottom_offset = $this->pad_bottom + $this->label_space +
$this->label_font_size * ($lines - 1);
$this->pad_bottom += $lines * $this->label_font_size +
2 * $this->label_space;
}
if($this->show_axes) {
// make space for divisions
$div_size = $this->DivisionOverlap();
$this->pad_bottom += $div_size['x'];
$this->pad_left += $div_size['y'];
if($this->show_axis_text_v || $this->show_axis_text_h) {
$pos_h = $this->GetFirst($this->axis_text_position_h,
$this->axis_text_position);
$pos_v = $this->GetFirst($this->axis_text_position_v,
$this->axis_text_position);
if($pos_h != 'inside' || $pos_v != 'inside') {
$tw = $th = $theight = $twidth = 0;
$len_h = $len_v = 1;
$space_x = $this->width - $this->pad_left - $this->pad_right;
$space_y = $this->height - $this->pad_top - $this->pad_bottom;
for($i = 0; $i < 3; ++$i) {
// find the longest axis labels for the grid space, reduced by
// the current longest label
list($len_h, $len_v, $tx, $ty) = $this->FindLongestAxisLabel(
$space_x - $tw, $space_y - $th);
if($this->show_axis_text_v && $pos_v != 'inside') {
// modify padding for axis markings
list($twidth) = $this->TextSize($len_v, $this->axis_font_size,
$this->axis_font_adjust, $this->axis_text_angle_v);
}
if($this->show_axis_text_h && $pos_h != 'inside') {
// similar to vertical version
list(, $theight) = $this->TextSize($len_h, $this->axis_font_size,
$this->axis_font_adjust, $this->axis_text_angle_h);
}
if($twidth == $tw && $theight == $th)
break;
$tw = $twidth;
$th = $theight;
}
// apply the found spacings
$this->pad_left += $tw;
$this->pad_bottom += $th;
}
}
}
$this->label_adjust_done = true;
}
/**
* Determines the longest axis labels for the given axis lengths
*/
protected function FindLongestAxisLabel($length_x, $length_y)
{
$ends = $this->GetAxisEnds();
list($x_axis, $y_axis) = $this->GetAxes($ends, $length_x, $length_y);
$min_space_h = $this->GetFirst($this->minimum_grid_spacing_h,
$this->minimum_grid_spacing);
$min_space_v = $this->GetFirst($this->minimum_grid_spacing_v,
$this->minimum_grid_spacing);
$bar_h = $bar_v = null;
if($this->flip_axes)
$bar_v = $this->label_centre;
else
$bar_h = $this->label_centre;
$h_grid = $x_axis->Grid($min_space_h, $bar_h);
$v_grid = $y_axis->Grid($min_space_v, $bar_v);
$y_points = $this->GetGridPoints($length_y, $v_grid, 0, 1, $y_axis->Zero(),
$y_axis->Unit(), $y_axis->Uneven());
$x_points = $this->GetGridPoints($length_x, $h_grid, 0, 1, $x_axis->Zero(),
$x_axis->Unit(), $x_axis->Uneven());
$longest_x = $longest_y = 0;
$t_x = $t_y = '';
foreach($x_points as $key => $val) {
$text = $this->flip_axes ? $key : $this->GetKey($key);
$len = strlen($text);
if($len > $longest_x) {
$longest_x = $len;
$t_x = $text;
}
}
foreach($y_points as $key => $val) {
$text = $this->flip_axes ? $this->GetKey($key) : $key;
$len = strlen($text);
if($len > $longest_y) {
$longest_y = $len;
$t_y = $text;
}
}
return array($longest_x, $longest_y, $t_x, $t_y);
}
/**
* Returns the amount of overlap the divisions and subdivisions use
*/
protected function DivisionOverlap()
{
if(!$this->show_divisions && !$this->show_subdivisions)
return array('x' => 0, 'y' => 0);
$dx = $this->DOverlap(
$this->GetFirst($this->division_style_h, $this->division_style),
$this->GetFirst($this->division_size_h, $this->division_size));
$dy = $this->DOverlap(
$this->GetFirst($this->division_style_v, $this->division_style),
$this->GetFirst($this->division_size_v, $this->division_size));
$sx = $this->DOverlap(
$this->GetFirst($this->subdivision_style_h, $this->subdivision_style),
$this->GetFirst($this->subdivision_size_h, $this->subdivision_size));
$sy = $this->DOverlap(
$this->GetFirst($this->subdivision_style_v, $this->subdivision_style),
$this->GetFirst($this->subdivision_size_v, $this->subdivision_size));
$x = max($dx, $sx);
$y = max($dy, $sy);
return array('x' => $x, 'y' => $y);
}
/**
* Calculates the overlap of a division or subdivision
*/
protected function DOverlap($style, $size)
{
$overlap = 0;
switch($style) {
case 'in' :
case 'infull' :
case 'none' :
return 0;
case 'out' :
case 'over' :
case 'overfull' :
default :
return $size;
}
}
/**
* Sets up grid width and height to fill padded area
*/
protected function SetGridDimensions()
{
$this->g_height = $this->height - $this->pad_top - $this->pad_bottom;
$this->g_width = $this->width - $this->pad_left - $this->pad_right;
}
/**
* Returns an array containing the value and key axis min and max
*/
protected function GetAxisEnds()
{
$v_max = $this->GetMaxValue();
$v_min = $this->GetMinValue();
$k_max = $this->GetMaxKey();
$k_min = $this->GetMinKey();
// check guides
if(empty($this->guidelines))
$this->CalcGuidelines();
if(!is_null($this->max_guide['y']))
$v_max = max($v_max, $this->max_guide['y']);
if(!is_null($this->min_guide['y']))
$v_min = min($v_min, $this->min_guide['y']);
if(!is_null($this->max_guide['x']))
$k_max = max($k_max, $this->max_guide['x']);
if(!is_null($this->min_guide['x']))
$k_min = min($k_min, $this->min_guide['x']);
// validate axes
if((is_numeric($this->axis_max_h) && is_numeric($this->axis_min_h) &&
$this->axis_max_h <= $this->axis_min_h) ||
(is_numeric($this->axis_max_v) && is_numeric($this->axis_min_v) &&
$this->axis_max_v <= $this->axis_min_v))
throw new Exception('Invalid axes specified');
if((is_numeric($this->axis_max_h) &&
($this->axis_max_h < ($this->flip_axes ? $v_min : $k_min))) ||
(is_numeric($this->axis_min_h) &&
($this->axis_min_h >= ($this->flip_axes ? $v_max : $k_max+1))) ||
(is_numeric($this->axis_max_v) &&
($this->axis_max_v < ($this->flip_axes ? $k_min : $v_min))) ||
(is_numeric($this->axis_min_v) &&
($this->axis_min_v >= ($this->flip_axes ? $k_max+1 : $v_max))))
throw new Exception('No values in grid range');
return compact('v_max', 'v_min', 'k_max', 'k_min');
}
/**
* Returns the X and Y axis class instances as a list
*/
protected function GetAxes($ends, &$x_len, &$y_len)
{
$h_by_count = $this->AssociativeKeys();
$x_max = $h_by_count ? $this->GetHorizontalCount() - 1 :
max(0, $ends['k_max']);
$x_min = $h_by_count ? 0 : min(0, $ends['k_min']);
$y_max = max(0, $ends['v_max']);
$y_min = min(0, $ends['v_min']);
if($this->flip_axes) {
$max_h = $this->GetFirst($this->axis_max_h, $y_max);
$min_h = $this->GetFirst($this->axis_min_h, $y_min);
$max_v = $this->GetFirst($this->axis_max_v, $x_max);
$min_v = $this->GetFirst($this->axis_min_v, $x_min);
$x_min_unit = 0;
$x_fit = false;
$y_min_unit = 1;
$y_fit = true;
} else {
$max_h = $this->GetFirst($this->axis_max_h, $x_max);
$min_h = $this->GetFirst($this->axis_min_h, $x_min);
$max_v = $this->GetFirst($this->axis_max_v, $y_max);
$min_v = $this->GetFirst($this->axis_min_v, $y_min);
$x_min_unit = 1;
$x_fit = true;
$y_min_unit = 0;
$y_fit = false;
}
// sanitise grid divisions
if(is_numeric($this->grid_division_v) && $this->grid_division_v <= 0)
$this->grid_division_v = null;
if(is_numeric($this->grid_division_h) && $this->grid_division_h <= 0)
$this->grid_division_h = null;
// if fixed grid spacing is specified, make the min spacing 1 pixel
if(is_numeric($this->grid_division_v))
$this->minimum_grid_spacing_v = 1;
if(is_numeric($this->grid_division_h))
$this->minimum_grid_spacing_h = 1;
if(!is_numeric($this->grid_division_h))
$x_axis = new Axis($x_len, $max_h, $min_h, $x_min_unit, $x_fit);
else
$x_axis = new AxisFixed($x_len, $max_h, $min_h, $this->grid_division_h);
if(!is_numeric($this->grid_division_v))
$y_axis = new Axis($y_len, $max_v, $min_v, $y_min_unit, $y_fit);
else
$y_axis = new AxisFixed($y_len, $max_v, $min_v, $this->grid_division_v);
return array($x_axis, $y_axis);
}
/**
* Calculates the effect of axes, applying to padding
*/
protected function CalcAxes()
{
if($this->axes_calc_done)
return;
$ends = $this->GetAxisEnds();
if(!$this->label_adjust_done)
$this->LabelAdjustment($ends['v_max'], $this->GetLongestKey());
if(is_null($this->g_height) || is_null($this->g_width))
$this->SetGridDimensions();
list($x_axis, $y_axis) = $this->GetAxes($ends, $this->g_width,
$this->g_height);
if($this->flip_axes) {
$bar_h = null;
$bar_v = $this->label_centre;
$x_min_unit = 0;
$y_min_unit = 1;
} else {
$bar_h = $this->label_centre;
$bar_v = null;
$x_min_unit = 1;
$y_min_unit = 0;
}
$min_space_h = $this->GetFirst($this->minimum_grid_spacing_h,
$this->minimum_grid_spacing);
$min_space_v = $this->GetFirst($this->minimum_grid_spacing_v,
$this->minimum_grid_spacing);
$this->h_grid = $x_axis->Grid($min_space_h, $bar_h);
$this->v_grid = $y_axis->Grid($min_space_v, $bar_v);
$this->x0 = $x_axis->Zero();
$this->y0 = $y_axis->Zero();
$this->uneven_x = $x_axis->Uneven();
$this->uneven_y = $y_axis->Uneven();
$this->bar_unit_width = $x_axis->Unit();
$this->bar_unit_height = $y_axis->Unit();
if($this->show_subdivisions) {
$this->sub_y = $this->FindSubdiv($this->v_grid, $this->bar_unit_height,
$this->minimum_subdivision, $y_min_unit, $this->subdivision_v);
$this->sub_x = $this->FindSubdiv($this->h_grid, $this->bar_unit_width,
$this->minimum_subdivision, $x_min_unit, $this->subdivision_h);
}
$this->axes_calc_done = true;
}
/**
* Find the subdivision size
*/
protected function FindSubdiv($grid_div, $u, $min, $min_unit, $fixed)
{
if(is_numeric($fixed))
return $u * $fixed;
$D = $grid_div / $u; // D = actual division size
$min = max($min, $min_unit * $u); // use the larger minimum value
$max_divisions = (int)floor($grid_div / $min);
// can we subdivide at all?
if($max_divisions <= 1)
return null;
// convert $D to an integer in the 100's range
$D1 = (int)round(100 * (pow(10,-floor(log10($D)))) * $D);
for($divisions = $max_divisions; $divisions > 1; --$divisions) {
// if $D1 / $divisions is not an integer, $divisions is no good
$dq = $D1 / $divisions;
if($dq - floor($dq) == 0)
return $grid_div / $divisions;
}
return null;
}
/**
* Returns the grid points as an associative array:
* array($value => $position)
*/
protected function GetGridPoints($length, $spacing, $start, $direction,
$zero, $unit_size, $uneven)
{
$c = $pos = 0;
$d = $spacing * 0.5;
$points = array();
while($pos < $length + $d) {
// converted to string to work as array key
$point = $this->NumString(($pos - $zero) / $unit_size);
$points[$point] = $start + ($direction * $pos);
$pos = ++$c * $spacing;
}
// $uneven means the divisions don't fit exactly, so add the last one in
if($uneven) {
$pos = $length - $zero;
$point = $this->NumString($pos / $unit_size);
$points[$point] = $start + $length;
}
return $points;
}
/**
* Returns the grid subdivision points as an array
*/
protected function GetGridSubdivisions(&$points, $spacing, $direction)
{
reset($points);
list(, $pos1) = each($points);
$d = $spacing * 0.5;
$subdivs = array();
while((list(, $pos2) = each($points)) !== false) {
$count = (int)round(abs($pos2 - $pos1) / $spacing);
for($c = 1; $c < $count; ++$c) {
$subdivs[] = $pos1 + ($direction * $c * $spacing);
}
$pos1 = $pos2;
}
return $subdivs;
}
/**
* Calculates the position of grid lines
*/
protected function CalcGrid()
{
if(isset($this->y_points))
return;
$grid_bottom = $this->height - $this->pad_bottom;
$grid_left = $this->pad_left;
$this->y_subdivs = array();
$this->x_subdivs = array();
$this->y_points = $this->GetGridPoints($this->g_height, $this->v_grid,
$grid_bottom, -1, $this->y0, $this->bar_unit_height, $this->uneven_y);
$this->x_points = $this->GetGridPoints($this->g_width, $this->h_grid,
$grid_left, 1, $this->x0, $this->bar_unit_width, $this->uneven_x);
if($this->sub_y)
$this->y_subdivs = $this->GetGridSubdivisions($this->y_points,
$this->sub_y, -1);
if($this->sub_x)
$this->x_subdivs = $this->GetGridSubdivisions($this->x_points,
$this->sub_x, 1);
}
/**
* Converts number to string
*/
protected function NumString($n)
{
// subtract number of digits before decimal point from precision
$d = is_int($n) ? 0 : ($this->precision - floor(log(abs($n))));
$s = number_format($n, $d);
if($d && strpos($s, '.') !== false) {
list($a, $b) = explode('.', $s);
$b1 = rtrim($b, '0');
if($b1 != '')
return "$a.$b1";
return $a;
}
return $s;
}
/**
* Subclasses can override this for non-linear graphs
*/
protected function GetHorizontalCount()
{
$values = $this->GetValues();
return count($values);
}
/**
* Returns the key that takes up the most space
*/
protected function GetLongestKey()
{
$longest_key = '';
if($this->show_axis_text_v) {
$max_len = 0;
foreach($this->values[0] as $k => $v) {
if(is_numeric($k))
$k = $this->NumString($k);
$len = strlen($k);
if($len > $max_len) {
$max_len = $len;
$longest_key = $k;
}
}
}
return $longest_key;
}
/**
* Returns the X axis SVG fragment
*/
protected function XAxis($yoff)
{
$x = $this->pad_left - $this->axis_overlap;
$y = $this->height - $this->pad_bottom - $yoff;
$len = $this->g_width + 2 * $this->axis_overlap;
$path = "M$x {$y}h$len";
return $this->Element('path', array('d' => $path));
}
/**
* Returns the Y axis SVG fragment
*/
protected function YAxis($xoff)
{
$x = $this->pad_left + $xoff;
$len = $this->g_height + 2 * $this->axis_overlap;
$y = $this->height - $this->pad_bottom + $this->axis_overlap - $len;
$path = "M$x {$y}v$len";
return $this->Element('path', array('d' => $path));
}
/**
* Returns the position and size of divisions
* @retval array('pos' => $position, 'sz' => $size)
*/
protected function DivisionsPositions($style, $style_default,
$size, $size_default, $fullsize, $start, $axis_offset)
{
if(empty($style))
$style = $style_default;
if(empty($size))
$size = $size_default;
$sz = $size;
$pos = $start + $axis_offset;
switch($style) {
case 'none' :
return null; // no pos or sz
case 'infull' :
$pos = $start;
$sz = $fullsize;
break;
case 'over' :
$pos -= $size;
$sz = $size * 2;
break;
case 'overfull' :
$pos = $start - $size;
$sz = $fullsize + $size;
break;
case 'in' :
break; // no change
case 'out' :
default :
$pos -= $size;
$sz = $size;
}
return array('sz' => $sz, 'pos' => $pos);
}
/**
* Returns X-axis divisions as a path
*/
protected function XAxisDivisions(&$points, $style, $style_default,
$size, $size_default, $yoff)
{
$path = '';
$pos = $this->DivisionsPositions($style, $style_default, $size,
$size_default, $this->g_height, $this->pad_bottom, $yoff);
if(is_null($pos))
return '';
$y = $this->height - $pos['pos'];
$height = -$pos['sz'];
foreach($points as $x)
$path .= "M$x {$y}v{$height}";
return $path;
}
/**
* Returns Y-axis divisions as a path
*/
protected function YAxisDivisions(&$points, $style, $style_default,
$size, $size_default, $xoff)
{
$path = '';
$pos = $this->DivisionsPositions($style, $style_default, $size,
$size_default, $this->g_width, $this->pad_left, $xoff);
if(is_null($pos))
return '';
$x = $pos['pos'];
$size = $pos['sz'];
foreach($points as $y)
$path .= "M$x {$y}h{$size}";
return $path;
}
/**
* Returns the X-axis text fragment
*/
protected function XAxisText(&$points, $xoff, $yoff, $angle)
{
$labels = '';
$x_prev = -$this->width;
$min_space = $this->GetFirst($this->minimum_grid_spacing_h,
$this->minimum_grid_spacing);
$count = count($points);
$label_centre_x = $this->label_centre && !$this->flip_axes;
$text_centre = $this->axis_font_size * 0.3;
$inside = ('inside' == $this->GetFirst($this->axis_text_position_h,
$this->axis_text_position));
if($inside)
{
$y = $this->height - $this->pad_bottom - $yoff - $this->axis_text_space;
$angle = -$angle;
$x_rotate_offset = -$text_centre;
}
else
{
$y = $this->height - $this->pad_bottom + $yoff + $this->axis_font_size +
$this->axis_text_space - $text_centre;
$x_rotate_offset = $text_centre;
}
if($angle < 0)
$x_rotate_offset = -$x_rotate_offset;
$y_rotate_offset = -$text_centre;
$text = array('y' => $y);
$p = 0;
foreach($points as $label => $x) {
$key = $this->flip_axes ? $label : $this->GetKey($label);
// don't draw 0 over the axis line
if($inside && !$label_centre_x && $key == '0')
$key = '';
if(strlen($key) > 0 && $x - $x_prev >= $min_space
&& (++$p < $count || !$label_centre_x)) {
$text['x'] = $x + $xoff;
if($angle != 0) {
$text['x'] -= $x_rotate_offset;
$rcx = $text['x'] + $x_rotate_offset;
$rcy = $text['y'] + $y_rotate_offset;
$text['transform'] = "rotate($angle,$rcx,$rcy)";
}
$labels .= $this->Element('text', $text, NULL, $key);
}
$x_prev = $x;
}
if($angle == 0) {
$tgroup = array('text-anchor' => 'middle');
} else {
$tgroup = array('text-anchor' => $this->axis_text_angle_h < 0 ?
'end' : 'start');
}
return $this->Element('g', $tgroup, NULL, $labels);
}
/**
* Returns the Y-axis text fragment
*/
protected function YAxisText(&$points, $xoff, $yoff, $angle)
{
$labels = '';
$y_prev = $this->height;
$min_space = $this->minimum_grid_spacing_v;
$text_centre = $this->axis_font_size * 0.3;
$label_centre_y = $this->label_centre && $this->flip_axes;
$inside = ('inside' == $this->GetFirst($this->axis_text_position_v,
$this->axis_text_position));
$anchor = $inside ? 'start' : 'end';
$x_rotate_offset = $inside ? $text_centre : -$text_centre;
$y_rotate_offset = -$text_centre;
$x = $this->pad_left + ($inside ? $xoff + $this->axis_text_space :
-$xoff - $this->axis_text_space);
$text = array('x' => $x);
$count = count($points);
$p = 0;
foreach($points as $label => $y) {
$key = $this->flip_axes ? $this->GetKey($label) : $label;
// don't draw 0 over the axis line
if($inside && !$label_centre_y && $key == '0')
$key = '';
if(strlen($key) && $y_prev - $y >= $min_space &&
(++$p < $count || !$label_centre_y)) {
$text['y'] = $y + $text_centre + $yoff;
if($angle != 0) {
$rcx = $text['x'] + $x_rotate_offset;
$rcy = $text['y'] + $y_rotate_offset;
$text['transform'] = "rotate($angle,$rcx,$rcy)";
}
$labels .= $this->Element('text', $text, NULL, $key);
}
$y_prev = $y;
}
return $this->Element('g', array('text-anchor' => $anchor), NULL, $labels);
}
/**
* Returns the horizontal axis label
*/
protected function HLabel(&$attribs)
{
if(empty($this->label_h))
return '';
$x = ($this->width - $this->pad_left - $this->pad_right) / 2 +
$this->pad_left;
$y = $this->height - $this->label_bottom_offset;
$pos = array('x' => $x, 'y' => $y);
return $this->Text($this->label_h, $this->label_font_size,
array_merge($attribs, $pos));
}
/**
* Returns the vertical axis label
*/
protected function VLabel(&$attribs)
{
if(empty($this->label_v))
return '';
$x = $this->label_left_offset;
$y = ($this->height - $this->pad_bottom - $this->pad_top) / 2 +
$this->pad_top;
$pos = array(
'x' => $x,
'y' => $y,
'transform' => "rotate(270,$x,$y)",
);
return $this->Text($this->label_v, $this->label_font_size,
array_merge($attribs, $pos));
}
/**
* Returns the labels grouped with the provided axis division labels
*/
protected function Labels($axis_text = '')
{
$labels = $axis_text;
if(!empty($this->label_h) || !empty($this->label_v)) {
$label_text = array('text-anchor' => 'middle');
if($this->label_font != $this->axis_font)
$label_text['font-family'] = $this->label_font;
if($this->label_font_size != $this->axis_font_size)
$label_text['font-size'] = $this->label_font_size;
if($this->label_font_weight != 'normal')
$label_text['font-weight'] = $this->label_font_weight;
if(!empty($this->label_colour) &&
$this->label_colour != $this->axis_text_colour)
$label_text['fill'] = $this->label_colour;
if(!empty($this->label_h)) {
$label_text['y'] = $this->height - $this->label_bottom_offset;
$label_text['x'] = $this->pad_left +
($this->width - $this->pad_left - $this->pad_right) / 2;
$labels .= $this->Text($this->label_h, $this->label_font_size,
$label_text);
}
$labels .= $this->VLabel($label_text);
}
if(!empty($labels)) {
$font = array(
'font-size' => $this->axis_font_size,
'font-family' => $this->axis_font,
'fill' => empty($this->axis_text_colour) ?
$this->axis_colour : $this->axis_text_colour,
);
$labels = $this->Element('g', $font, NULL, $labels);
}
return $labels;
}
/**
* Draws bar or line graph axes
*/
protected function Axes()
{
if(!$this->show_axes)
return $this->Labels();
$this->CalcGrid();
$x_axis_visible = $this->y0 >= 0 && $this->y0 < $this->g_height;
$y_axis_visible = $this->x0 >= 0 && $this->x0 < $this->g_width;
$yoff = $x_axis_visible ? $this->y0 : 0;
$xoff = $y_axis_visible ? $this->x0 : 0;
$axis_group = $axes = $label_group = $divisions = $axis_text = '';
if($x_axis_visible)
$axes .= $this->XAxis($yoff);
if($y_axis_visible)
$axes .= $this->YAxis($xoff);
if($axes != '') {
$line = array(
'stroke-width' => $this->axis_stroke_width,
'stroke' => $this->axis_colour
);
$axis_group = $this->Element('g', $line, NULL, $axes);
}
$x_offset = $y_offset = 0;
if($this->label_centre) {
if($this->flip_axes)
$y_offset = -0.5 * $this->bar_unit_height;
else
$x_offset = 0.5 * $this->bar_unit_width;
}
arsort($this->y_points);
asort($this->x_points);
$text_offset = $this->DivisionOverlap();
if($this->show_axis_text_v)
$axis_text .= $this->YAxisText($this->y_points, $text_offset['y'],
$y_offset, $this->axis_text_angle_v);
if($this->show_axis_text_h)
$axis_text .= $this->XAxisText($this->x_points, $x_offset,
$text_offset['x'], $this->axis_text_angle_h);
$label_group = $this->Labels($axis_text);
if($this->show_divisions) {
// use an array to join paths with same colour
$div_paths = array();
$dx_path = $this->XAxisDivisions($this->x_points,
$this->division_style_h, $this->division_style,
$this->division_size_h, $this->division_size, $yoff);
if(!empty($dx_path)) {
$dx_colour = $this->GetFirst($this->division_colour_h,
$this->division_colour, $this->axis_colour);
@$div_paths[$dx_colour] .= $dx_path;
}
$dy_path = $this->YAxisDivisions($this->y_points,
$this->division_style_v, $this->division_style,
$this->division_size_v, $this->division_size, $xoff);
if(!empty($dy_path)) {
$dy_colour = $this->GetFirst($this->division_colour_v,
$this->division_colour, $this->axis_colour);
@$div_paths[$dy_colour] .= $dy_path;
}
if($this->show_subdivisions) {
$sdy_path = $this->YAxisDivisions($this->y_subdivs,
$this->subdivision_style_v, $this->subdivision_style,
$this->subdivision_size_v, $this->subdivision_size, $xoff);
$sdx_path = $this->XAxisDivisions($this->x_subdivs,
$this->subdivision_style_h, $this->subdivision_style,
$this->subdivision_size_h, $this->subdivision_size, $yoff);
if(!empty($sdx_path)) {
$sdx_colour = $this->GetFirst($this->subdivision_colour_h,
$this->subdivision_colour, $this->division_colour_h,
$this->division_colour, $this->axis_colour);
@$div_paths[$sdx_colour] .= $sdx_path;
}
if(!empty($sdy_path)) {
$sdy_colour = $this->GetFirst($this->subdivision_colour_v,
$this->subdivision_colour, $this->division_colour_v,
$this->division_colour, $this->axis_colour);
@$div_paths[$sdy_colour] .= $sdy_path;
}
}
foreach($div_paths as $colour => $path) {
$div = array(
'd' => $path,
'stroke-width' => 1,
'stroke' => $colour
);
$divisions .= $this->Element('path', $div);
}
}
return $divisions . $axis_group . $label_group;
}
/**
* Returns a set of gridlines
*/
protected function GridLines($path, $colour, $dash, $fill = null)
{
if($path == '' || $colour == 'none')
return '';
$opts = array('d' => $path, 'stroke' => $colour);
if(!empty($dash))
$opts['stroke-dasharray'] = $dash;
if(!empty($fill))
$opts['fill'] = $fill;
return $this->Element('path', $opts);
}
/**
* Draws the grid behind the bar / line graph
*/
protected function Grid()
{
$this->CalcAxes();
if(!$this->show_grid)
return '';
$this->CalcGrid();
$back = $subpath = $path_h = $path_v = '';
$back_colour = $this->grid_back_colour;
if(!empty($back_colour) && $back_colour != 'none') {
$rect = array(
'x' => $this->pad_left, 'y' => $this->pad_top,
'width' => $this->g_width, 'height' => $this->g_height,
'fill' => $back_colour
);
$back = $this->Element('rect', $rect);
}
if($this->show_grid_subdivisions) {
$subpath_h = $subpath_v = '';
foreach($this->y_subdivs as $y)
$subpath_v .= "M{$this->pad_left} {$y}h{$this->g_width}";
foreach($this->x_subdivs as $x)
$subpath_h .= "M$x {$this->pad_top}v{$this->g_height}";
if($subpath_h != '' || $subpath_v != '') {
$colour_h = $this->GetFirst($this->grid_subdivision_colour_h,
$this->grid_subdivision_colour, $this->grid_colour_h,
$this->grid_colour);
$colour_v = $this->GetFirst($this->grid_subdivision_colour_v,
$this->grid_subdivision_colour, $this->grid_colour_v,
$this->grid_colour);
$dash_h = $this->GetFirst($this->grid_subdivision_dash_h,
$this->grid_subdivision_dash, $this->grid_dash_h, $this->grid_dash);
$dash_v = $this->GetFirst($this->grid_subdivision_dash_v,
$this->grid_subdivision_dash, $this->grid_dash_v, $this->grid_dash);
if($dash_h == $dash_v && $colour_h == $colour_v) {
$subpath = $this->GridLines($subpath_h . $subpath_v, $colour_h,
$dash_h);
} else {
$subpath = $this->GridLines($subpath_h, $colour_h, $dash_h) .
$this->GridLines($subpath_v, $colour_v, $dash_v);
}
}
}
foreach($this->y_points as $y)
$path_v .= "M{$this->pad_left} {$y}h{$this->g_width}";
foreach($this->x_points as $x)
$path_h .= "M$x {$this->pad_top}v{$this->g_height}";
$colour_h = $this->GetFirst($this->grid_colour_h, $this->grid_colour);
$colour_v = $this->GetFirst($this->grid_colour_v, $this->grid_colour);
$dash_h = $this->GetFirst($this->grid_dash_h, $this->grid_dash);
$dash_v = $this->GetFirst($this->grid_dash_v, $this->grid_dash);
if($dash_h == $dash_v && $colour_h == $colour_v) {
$path = $this->GridLines($path_v . $path_h, $colour_h, $dash_h);
} else {
$path = $this->GridLines($path_h, $colour_h, $dash_h) .
$this->GridLines($path_v, $colour_v, $dash_v);
}
return $back . $subpath . $path;
}
/**
* clamps a value to the grid boundaries
*/
protected function ClampVertical($val)
{
return max($this->pad_top, min($this->height - $this->pad_bottom, $val));
}
protected function ClampHorizontal($val)
{
return max($this->pad_left, min($this->width - $this->pad_right, $val));
}
/**
* Returns a clipping path for the grid
*/
protected function ClipGrid(&$attr)
{
$rect = array(
'x' => $this->pad_left, 'y' => $this->pad_top,
'width' => $this->width - $this->pad_left - $this->pad_right,
'height' => $this->height - $this->pad_top - $this->pad_bottom
);
$clip_id = $this->NewID();
$this->defs[] = $this->Element('clipPath', array('id' => $clip_id),
NULL, $this->Element('rect', $rect));
$attr['clip-path'] = "url(#{$clip_id})";
}
/**
* Returns the grid position for a bar or point, or NULL if not on grid
* $key = actual value array index
* $ikey = integer position in array
*/
protected function GridPosition($key, $ikey)
{
$position = null;
$gkey = $this->AssociativeKeys() ? $ikey : $key;
if($this->flip_axes) {
$top = $this->label_centre ?
$this->g_height - ($this->bar_unit_height / 2) : $this->g_height;
$offset = $this->y0 + ($this->bar_unit_height * $gkey);
if($offset >= 0 && floor($offset) <= $top)
$position = $this->height - $this->pad_bottom - $offset;
} else {
$right_end = $this->label_centre ?
$this->g_width - ($this->bar_unit_width / 2) : $this->g_width;
$offset = $this->x0 + ($this->bar_unit_width * $gkey);
if($offset >= 0 && floor($offset) <= $right_end)
$position = $this->pad_left + $offset;
}
return $position;
}
/**
* Converts guideline options to more useful member variables
*/
protected function CalcGuidelines($g = null)
{
if(is_null($g)) {
// no guidelines?
if(empty($this->guideline) && $this->guideline !== 0)
return;
if(is_array($this->guideline) && count($this->guideline) > 1 &&
!is_string($this->guideline[1])) {
// array of guidelines
foreach($this->guideline as $gl)
$this->CalcGuidelines($gl);
return;
}
// single guideline
$g = $this->guideline;
}
if(!is_array($g))
$g = array($g);
$value = $g[0];
$axis = (isset($g[2]) && ($g[2] == 'x' || $g[2] == 'y')) ? $g[2] : 'y';
$above = isset($g['above']) ? $g['above'] : $this->guideline_above;
$position = $above ? SVGG_GUIDELINE_ABOVE : SVGG_GUIDELINE_BELOW;
$guideline = array(
'value' => $value,
'depth' => $position,
'title' => isset($g[1]) ? $g[1] : '',
'axis' => $axis
);
$lopts = $topts = array();
$line_opts = array(
'colour' => 'stroke',
'dash' => 'stroke-dasharray',
'stroke_width' => 'stroke-width',
'opacity' => 'opacity',
// not SVG attributes
'length' => 'length',
'length_units' => 'length_units',
);
$text_opts = array(
'colour' => 'fill',
'opacity' => 'opacity',
'font' => 'font-family',
'font_size' => 'font-size',
'font_weight' => 'font-weight',
'text_colour' => 'fill', // overrides 'colour' option from line
'text_opacity' => 'opacity', // overrides line opacity
// these options do not map to SVG attributes
'font_adjust' => 'font_adjust',
'text_position' => 'text_position',
'text_padding' => 'text_padding',
'text_angle' => 'text_angle',
'text_align' => 'text_align',
);
foreach($line_opts as $okey => $opt)
if(isset($g[$okey]))
$lopts[$opt] = $g[$okey];
foreach($text_opts as $okey => $opt)
if(isset($g[$okey]))
$topts[$opt] = $g[$okey];
if(count($lopts))
$guideline['line'] = $lopts;
if(count($topts))
$guideline['text'] = $topts;
// update maxima and minima
if(is_null($this->max_guide[$axis]) || $value > $this->max_guide[$axis])
$this->max_guide[$axis] = $value;
if(is_null($this->min_guide[$axis]) || $value < $this->min_guide[$axis])
$this->min_guide[$axis] = $value;
// can flip the axes now the min/max are stored
if($this->flip_axes)
$guideline['axis'] = ($guideline['axis'] == 'x' ? 'y' : 'x');
$this->guidelines[] = $guideline;
}
/**
* Returns the elements to draw the guidelines
*/
protected function Guidelines($depth)
{
if(empty($this->guidelines))
return '';
// build all the lines at this depth (above/below) that use
// global options as one path
$d = $lines = $text = '';
$path = array(
'stroke' => $this->guideline_colour,
'stroke-width' => $this->guideline_stroke_width,
'stroke-dasharray' => $this->guideline_dash,
'fill' => 'none'
);
if($this->guideline_opacity != 1)
$path['opacity'] = $this->guideline_opacity;
$textopts = array(
'font-family' => $this->guideline_font,
'font-size' => $this->guideline_font_size,
'font-weight' => $this->guideline_font_weight,
'fill' => $this->GetFirst($this->guideline_text_colour,
$this->guideline_colour),
);
$text_opacity = $this->GetFirst($this->guideline_text_opacity,
$this->guideline_opacity);
foreach($this->guidelines as $line) {
if($line['depth'] == $depth) {
// opacity cannot go in the group because child opacity is multiplied
// by group opacity
if($text_opacity != 1 && !isset($line['text']['opacity']))
$line['text']['opacity'] = $text_opacity;
$this->BuildGuideline($line, $lines, $text, $path, $d);
}
}
if(!empty($d)) {
$path['d'] = $d;
$lines .= $this->Element('path', $path);
}
if(!empty($text))
$text = $this->Element('g', $textopts, null, $text);
return $lines . $text;
}
/**
* Adds a single guideline and its title to content
*/
protected function BuildGuideline(&$line, &$lines, &$text, &$path, &$d)
{
$length = $this->guideline_length;
$length_units = $this->guideline_length_units;
if(isset($line['line'])) {
$this->UpdateAndUnset($length, $line['line'], 'length');
$this->UpdateAndUnset($length_units, $line['line'], 'length_units');
}
if($length != 0) {
if($line['axis'] == 'x')
$h = $length;
else
$w = $length;
} elseif($length_units != 0) {
if($line['axis'] == 'x')
$h = $length_units * $this->bar_unit_height;
else
$w = $length_units * $this->bar_unit_width;
}
$path_data = $this->GuidelinePath($line['axis'], $line['value'],
$line['depth'], $x, $y, $w, $h);
if(!isset($line['line'])) {
// no special options, add to main path
$d .= $path_data;
} else {
$line_path = array_merge($path, $line['line'], array('d' => $path_data));
$lines .= $this->Element('path', $line_path);
}
if(!empty($line['title'])) {
$text_pos = $this->guideline_text_position;
$text_pad = $this->guideline_text_padding;
$text_angle = $this->guideline_text_angle;
$text_align = $this->guideline_text_align;
$font_size = $this->guideline_font_size;
$font_adjust = $this->guideline_font_adjust;
if(isset($line['text'])) {
$this->UpdateAndUnset($text_pos, $line['text'], 'text_position');
$this->UpdateAndUnset($text_pad, $line['text'], 'text_padding');
$this->UpdateAndUnset($text_angle, $line['text'], 'text_angle');
$this->UpdateAndUnset($text_align, $line['text'], 'text_align');
$this->UpdateAndUnset($font_adjust, $line['text'], 'font_adjust');
if(isset($line['text']['font-size']))
$font_size = $line['text']['font-size'];
}
list($text_w, $text_h) = $this->TextSize($line['title'],
$font_size, $font_adjust, $text_angle, $font_size);
list($x, $y, $text_right) = Graph::RelativePosition(
$text_pos, $y, $x, $y + $h, $x + $w,
$text_w, $text_h, $text_pad, true);
$t = array('x' => $x, 'y' => $y + $font_size);
if($text_right && empty($text_align))
$text_align = 'right';
$align_map = array('right' => 'end', 'centre' => 'middle');
if(!empty($text_align) && isset($align_map[$text_align]))
$t['text-anchor'] = $align_map[$text_align];
if($text_angle != 0) {
$rx = $x + $text_h/2;
$ry = $y + $text_h/2;
$t['transform'] = "rotate($text_angle,$rx,$ry)";
}
if(isset($line['text']))
$t = array_merge($t, $line['text']);
$text .= $this->Text($line['title'], $font_size, $t);
}
}
/**
* Creates the path data for a guideline and sets the dimensions
*/
protected function GuidelinePath($axis, $value, $depth, &$x, &$y, &$w, &$h)
{
$y_axis_pos = $this->height - $this->pad_bottom - $this->y0;
$x_axis_pos = $this->pad_left + $this->x0;
if($axis == 'x') {
$x = $x_axis_pos + ($value * $this->bar_unit_width);
$y = $this->height - $this->pad_bottom - $this->g_height;
$w = 0;
if($h == 0) {
$h = $this->g_height;
} elseif($h < 0) {
$h = -$h;
} else {
$y = $this->height - $this->pad_bottom - $h;
}
return "M$x {$y}v$h";
} else {
$x = $this->pad_left;
$y = $y_axis_pos - ($value * $this->bar_unit_height);
if($w == 0) {
$w = $this->g_width;
} elseif($w < 0) {
$w = -$w;
$x = $this->pad_left + $this->g_width - $w;
}
$h = 0;
return "M$x {$y}h$w";
}
}
/**
* Updates $var with $array[$key] and removes it from array
*/
protected function UpdateAndUnset(&$var, &$array, $key)
{
if(isset($array[$key])) {
$var = $array[$key];
unset($array[$key]);
}
}
}