<?php
/**
* Copyright (C) 2022 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 <[email protected]>
*/
namespace Goat1000\SVGGraph;
trait ThreeDTrait {
// Number of data ranges
protected $depth = 1;
protected $depth_unit = 1;
protected $project_angle;
/**
* Converts x,y,z coordinates into flat x,y
*/
protected function project($x, $y, $z)
{
$a = deg2rad($this->project_angle);
$xp = $z * cos($a);
$yp = $z * sin($a);
return [$x + $xp, $y - $yp];
}
/**
* Adjust axes for block spacing, setting the depth unit
*/
protected function adjustAxes(&$x_len, &$y_len)
{
// make sure project_angle is in range
$this->project_angle = min(89, max(1, $this->getOption('project_angle', 30)));
$ends = $this->getAxisEnds();
$bars = $ends['k_max'][0] - $ends['k_min'][0] + 1;
$a = deg2rad($this->project_angle);
$depth = $this->depth;
$u = $x_len / ($bars + $depth * cos($a));
$d = $u * $depth * sin($a);
if($d > $y_len) {
// doesn't fit - use 1/2 y length
$d = $y_len / 2;
$u = $d / $depth * sin($a);
}
$c = $u * $depth * cos($a);
$x_len -= $c;
$y_len -= $d;
$this->depth_unit = $u;
return [$c, $d];
}
/**
* Draws the grid behind the bar / line graph
*/
protected function grid()
{
$this->calcAxes();
$this->calcGrid();
if(!$this->getOption('show_grid') ||
(!$this->getOption('show_grid_h') && !$this->getOption('show_grid_v')))
return '';
// move to depth
$z = $this->depth * $this->depth_unit;
list($xd,$yd) = $this->project(0, 0, $z);
$x_w = new Number($this->g_width);
// convert to Number now - more efficient
$minus_x_w = new Number(-$this->g_width);
$y_h = new Number($this->g_height);
$minus_y_h = new Number(-$this->g_height);
$xleft = new Number($this->pad_left);
$ybottom = new Number($this->height - $this->pad_bottom);
$minus_xd = new Number(-$xd);
$minus_yd = new Number(-$yd);
$xd = new Number($xd);
$yd = new Number($yd);
$back = $subpath = $path = '';
$back_colour = new Colour($this, $this->getOption('grid_back_colour'));
if(!$back_colour->isNone()) {
$dpath = new PathData('M', $xleft, $ybottom, 'v', $minus_y_h, 'l', $xd, $yd);
$dpath->add('h', $x_w, 'v', $y_h, 'l', $minus_xd, $minus_yd, 'z');
$bpath = [
'd' => $dpath,
'fill' => $back_colour
];
if($this->getOption('grid_back_opacity') != 1)
$bpath['fill-opacity'] = $this->getOption('grid_back_opacity');
$back = $this->element('path', $bpath);
}
$back .= $this->getGridStripes();
if($this->getOption('show_grid_subdivisions')) {
$subpath_h = new PathData;
$subpath_v = new PathData;
if($this->getOption('show_grid_h')) {
$subdivs = $this->getSubDivsY($this->main_y_axis);
foreach($subdivs as $y)
$subpath_v->add('M', $xleft, $y->position, 'l', $xd, $yd,
'l', $x_w, 0);
}
if($this->getOption('show_grid_v')) {
$subdivs = $this->getSubDivsX(0);
foreach($subdivs as $x)
$subpath_h->add('M', $x->position, $ybottom, 'l', $xd, $yd,
'l', 0, $minus_y_h);
}
if(!($subpath_h->isEmpty() && $subpath_v->isEmpty())) {
$colour_h = $this->getOption('grid_subdivision_colour_h',
'grid_subdivision_colour', 'grid_colour_h', 'grid_colour');
$colour_v = $this->getOption('grid_subdivision_colour_v',
'grid_subdivision_colour', 'grid_colour_v', 'grid_colour');
$dash_h = $this->getOption('grid_subdivision_dash_h',
'grid_subdivision_dash', 'grid_dash_h', 'grid_dash');
$dash_v = $this->getOption('grid_subdivision_dash_v',
'grid_subdivision_dash', 'grid_dash_v', 'grid_dash');
$width_h = $this->getOption('grid_subdivision_stroke_width_h',
'grid_subdivision_stroke_width', 'grid_stroke_width_h',
'grid_stroke_width');
$width_v = $this->getOption('grid_subdivision_stroke_width_v',
'grid_subdivision_stroke_width', 'grid_stroke_width_v',
'grid_stroke_width');
if($dash_h == $dash_v && $colour_h == $colour_v && $width_h == $width_v) {
$subpath_h->add($subpath_v);
$subpath = $this->gridLines($subpath_h, $colour_h, $dash_h, $width_h,
['fill' => 'none']);
} else {
$subpath = $this->gridLines($subpath_h, $colour_h, $dash_h,$width_h,
['fill' => 'none']) .
$this->gridLines($subpath_v, $colour_v, $dash_v, $width_v,
['fill' => 'none']);
}
}
}
// start with axis lines
$path = new PathData('M', $xleft, $ybottom, 'l', $x_w, 0);
$path->add('M', $xleft, $ybottom, 'l', 0, $minus_y_h);
$path_v = new PathData;
$path_h = new PathData;
if($this->getOption('show_grid_h')) {
$points = $this->getGridPointsY($this->main_y_axis);
foreach($points as $y)
$path_v->add('M', $xleft, $y->position, 'l', $xd, $yd, 'l', $x_w, 0);
}
if($this->getOption('show_grid_v')) {
$points = $this->getGridPointsX(0);
foreach($points as $x)
$path_h->add('M', $x->position, $ybottom, 'l', $xd, $yd, 'l', 0, $minus_y_h);
}
$colour_h = $this->getOption('grid_colour_h', 'grid_colour');
$colour_v = $this->getOption('grid_colour_v', 'grid_colour');
$dash_h = $this->getOption('grid_dash_h', 'grid_dash');
$dash_v = $this->getOption('grid_dash_v', 'grid_dash');
$width_h = $this->getOption('grid_stroke_width_h', 'grid_stroke_width');
$width_v = $this->getOption('grid_stroke_width_v', 'grid_stroke_width');
if($dash_h == $dash_v && $colour_h == $colour_v && $width_h == $width_v) {
$path_h->add($path_v);
$path = $this->gridLines($path_h, $colour_h, $dash_h, $width_h,
['fill' => 'none']);
} else {
$path = $this->gridLines($path_h, $colour_h, $dash_h, $width_h,
['fill' => 'none']) .
$this->gridLines($path_v, $colour_v, $dash_v, $width_v,
['fill' => 'none']);
}
return $back . $subpath . $path;
}
/**
* Returns the grid stripes as a string
*/
protected function getGridStripes()
{
if(!$this->getOption('grid_back_stripe'))
return '';
// move to depth
$z = $this->depth * $this->depth_unit;
list($xd,$yd) = $this->project(0, 0, $z);
$x_w = new Number($this->g_width);
$minus_x_w = new Number(-$this->g_width);
$xleft = new Number($this->pad_left);
$minus_xd = new Number(-$xd);
$minus_yd = new Number(-$yd);
$xd = new Number($xd);
$yd = new Number($yd);
// use array of colours if available, otherwise stripe a single colour
$colours = is_array($this->getOption('grid_back_stripe_colour')) ?
$this->getOption('grid_back_stripe_colour') :
[null, $this->getOption('grid_back_stripe_colour')];
$pathdata = '';
$c = 0;
$p1 = null;
$num_colours = count($colours);
$points = $this->getGridPointsY($this->main_y_axis);
$first = array_shift($points);
$last_pos = $first->position;
$stripes = '';
foreach($points as $y) {
$y = $y->position;
$cc = $colours[$c % $num_colours];
if($cc !== null) {
$y1 = $last_pos - $y;
$dpath = new PathData('M', $xleft, $y, 'l', $xd, $yd);
$dpath->add('h', $x_w, 'v', $y1, 'h', $minus_x_w);
$dpath->add('l', $minus_xd, $minus_yd, 'z');
$bpath = [
'fill' => new Colour($this, $cc),
'd' => $dpath,
];
if($this->getOption('grid_back_stripe_opacity') != 1)
$bpath['fill-opacity'] = $this->getOption('grid_back_stripe_opacity');
$stripes .= $this->element('path', $bpath);
} else {
$p1 = $y;
}
$last_pos = $y;
++$c;
}
return $stripes;
}
/**
* clamps a value to the grid boundaries
*/
protected function clampVertical($val)
{
return max($this->height - $this->pad_bottom - $this->g_height,
min($this->height - $this->pad_bottom, $val));
}
protected function clampHorizontal($val)
{
return max($this->width - $this->pad_right - $this->g_width,
min($this->width - $this->pad_right, $val));
}
/**
* Returns the path for a guideline, and sets dimensions of the straight bit
*/
public function guidelinePathBelow($axis, $value, &$x, &$y, &$w, &$h,
$reverse_length)
{
$coords = new Coords($this);
$grid_value = 'g' . (is_numeric($value) ? new Number($value) : $value);
if($axis == 'x') {
$y = $coords->transform('gt', 'y');
$x = $coords->transform($grid_value, 'x', null);
if($x === null)
return '';
if(is_string($h) || $h > 0) {
$h = $coords->transform($h, 'y');
} else {
$h = $coords->transform('gh', 'y');
}
if(!$reverse_length)
$y = $coords->transform('gb', 'y') - $h;
} else {
$x = $coords->transform('gl', 'x');
$y = $coords->transform($grid_value, 'y', null);
if($y === null)
return '';
if(is_string($w) || $w > 0) {
$w = $coords->transform($w, 'x');
} else {
$w = $coords->transform('gw', 'x');
}
if($reverse_length)
$x = $coords->transform('gr', 'x') - $w;
$h = 0;
}
// get the depth section of the path
$z = $this->depth * $this->depth_unit;
list($xd, $yd) = $this->project(0, 0, $z);
$x1 = $x;
$y1 = $y + $h;
$x = $x + $xd;
$y = $y + $yd;
// for reverse lengths the line doesn't meet the axis
$path = new PathData('M', $x, $y, 'l', $w, $h);
if(!$reverse_length)
$path->add('M', $x1, $y1, 'l', $xd, $yd);
return $path;
}
/**
* Returns TRUE if the item is visible on the graph
*/
public function isVisible($item, $dataset = 0)
{
// 0 values are visible, NULLs are not
return $item->value !== null;
}
/**
* Returns the SVG code for a bar
*/
protected function drawBar3D(DataItem $item, $index, $start = 0, $axis = null,
$dataset = 0, $options = [])
{
if($item->value === null)
return '';
$bar = $this->barDimensions($item, $index, $start, $axis, $dataset);
if(empty($bar))
return '';
// is the bar top drawn?
$top = !isset($options['top']) || $options['top'];
// if the bar is empty and no legend or labels to show give up now
if(!$top && (string)$bar['height'] == '0' && !$this->getOption('legend_show_empty') &&
!$this->getOption('show_data_labels'))
return '';
// fill and stroke the group
$group = ['fill' => $this->getColour($item, $index, $dataset)];
$this->setStroke($group, $item, $index, $dataset);
if($this->getOption('semantic_classes'))
$group['class'] = 'series' . $dataset;
$label_shown = $this->addDataLabel($dataset, $index, $group, $item,
$bar['x'], $bar['y'], $bar['width'], $bar['height']);
if($this->getOption('show_tooltips'))
$this->setTooltip($group, $item, $dataset, $item->key, $item->value,
$label_shown);
if($this->getOption('show_context_menu'))
$this->setContextMenu($group, $dataset, $item, $label_shown);
$bar_part = $this->element('g', $group, null,
$this->bar3D($item, $bar, $top, $index, $dataset, $start, $axis));
return $this->getLink($item, $item->key, $bar_part);
}
}
|