<?php
/**
* Copyright (C) 2019-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 StackedBarTrait {
use MultiGraphTrait;
private $bar_visibility = [];
// used to determine where the total label should go
protected $last_position_pos = [];
protected $last_position_neg = [];
/**
* Draws the bars
*/
protected function drawBars()
{
$this->barSetup();
$chunk_count = count($this->multi_graph);
$datasets = $this->multi_graph->getEnabledDatasets();
$bars = '';
$legend_entries = [];
foreach($this->multi_graph as $bnum => $itemlist) {
$item = $itemlist[0];
// sort the values from bottom to top, assigning position
$yplus = $yminus = 0;
$chunk_values = [];
for($j = 0; $j < $chunk_count; ++$j) {
if(!in_array($j, $datasets))
continue;
$item = $itemlist[$j];
if($item->value !== null) {
if($item->value < 0) {
array_unshift($chunk_values, [$j, $item, $yminus]);
$yminus += $item->value;
} else {
$chunk_values[] = [$j, $item, $yplus];
$yplus += $item->value;
}
}
}
$bar_count = count($chunk_values);
$b = 0;
foreach($chunk_values as $chunk) {
list($j, $item, $start) = $chunk;
$top = (++$b == $bar_count);
$this->setBarVisibility($j, $item, $top);
$legend_entries[$j][$bnum] = $item;
$bars .= $this->drawBar($item, $bnum, $start, null, $j, ['top' => $top]);
}
$this->barTotals($item, $bnum, $yplus, $yminus, $j);
}
// assign legend entries in order of datasets
foreach($legend_entries as $j => $dataset)
foreach($dataset as $bnum => $item)
$this->setBarLegendEntry($j, $bnum, $item);
return $bars;
}
/**
* Displays the bar totals
*/
public function barTotals(DataItem $item, $bnum, $yplus, $yminus, $dataset)
{
$bar_x = $this->gridPosition($item, $bnum);
if($this->getOption('show_bar_totals') && $bar_x !== null) {
$cb = $this->getOption('bar_total_callback');
if($yplus) {
$bar = $this->barDimensions($item, $bnum, 0, null, $dataset);
$this->barY($yplus, $bar);
if(is_callable($cb)) {
$total = call_user_func($cb, $item->key, $yplus);
} else {
$total = new Number($yplus);
$total = $total->format();
}
$this->addContentLabel('totalpos-' . $dataset, $bnum,
$bar['x'], $bar['y'], $bar['width'], $bar['height'], $total);
}
if($yminus) {
$bar = $this->barDimensions($item, $bnum, 0, null, $dataset);
$this->barY($yminus, $bar);
if(is_callable($cb)) {
$total = call_user_func($cb, $item->key, $yminus);
} else {
$total = new Number($yminus);
$total = $total->format();
}
$this->addContentLabel('totalneg-' . $dataset, $bnum,
$bar['x'], $bar['y'], $bar['width'], $bar['height'], $total);
}
}
}
/**
* Overridden to prevent drawing on other bars
*/
public function dataLabelPosition($dataset, $index, &$item, $x, $y, $w, $h,
$label_w, $label_h)
{
list($pos, $target) = parent::dataLabelPosition($dataset, $index, $item,
$x, $y, $w, $h, $label_w, $label_h);
if(!is_numeric($dataset)) {
// doing this supports stacked grouped bar graph totals too
list($d) = explode('-', $dataset);
if($d === 'totalpos') {
if(isset($this->last_position_pos[$index])) {
list($lpos, $l_h) = $this->last_position_pos[$index];
list($hpos, $vpos) = Graph::translatePosition($lpos);
if($vpos == 'ot') {
$num_offset = new Number(-$l_h);
return ['above 0 ' . $num_offset, $target];
}
}
return ['above', $target];
}
if($d === 'totalneg') {
if(isset($this->last_position_neg[$index])) {
list($lpos, $l_h) = $this->last_position_neg[$index];
list($hpos, $vpos) = Graph::translatePosition($lpos);
if($vpos == 'ob') {
$num_offset = new Number($l_h);
return ['below 0 ' . $num_offset, $target];
}
}
return ['below', $target];
}
}
if($label_h > $h && Graph::isPositionInside($pos))
$pos = str_replace(['top','bottom','above','below'], 'middle', $pos);
if($item->value > 0)
$this->last_position_pos[$index] = [$pos, $label_h];
else
$this->last_position_neg[$index] = [$pos, $label_h];
return [$pos, $target];
}
/**
* Returns the style options for bar labels (and totals)
*/
public function dataLabelStyle($dataset, $index, &$item)
{
$style = parent::dataLabelStyle($dataset, $index, $item);
if(strpos($dataset, 'total') === 0) {
// total settings can override label settings
$simple = [
'font', 'font_size', 'font_weight', 'space', 'type', 'fill',
'font_adjust', 'angle', 'round', 'shadow_opacity',
'tail_width', 'tail_length',
];
foreach($simple as $opt) {
$val = $this->getOption('bar_total_' . $opt);
if(!empty($val))
$style[$opt] = $val;
}
$colour = new Colour($this, $this->getOption('bar_total_colour'));
$back_colour = new Colour($this, $this->getOption('bar_total_back_colour'));
if(!$colour->isNone())
$style['colour'] = $colour;
if(!$back_colour->isNone())
$style['back_colour'] = $back_colour;
$stroke = $this->getOption('bar_total_outline_colour');
$stroke_width = $this->getOption('bar_total_outline_thickness');
$pad_x = $this->getOption('bar_total_padding_x', 'bar_total_padding');
$pad_y = $this->getOption('bar_total_padding_y', 'bar_total_padding');
if(!empty($stroke))
$style['stroke'] = $stroke;
if(!empty($stroke_width))
$style['stroke_width'] = $stroke_width;
if(!empty($pad_x))
$style['pad_x'] = $pad_x;
if(!empty($pad_y))
$style['pad_y'] = $pad_y;
}
return $style;
}
/**
* Returns the maximum (stacked) value
*/
public function getMaxValue()
{
return $this->multi_graph->getMaxSumValue();
}
/**
* Returns the minimum (stacked) value
*/
public function getMinValue()
{
return $this->multi_graph->getMinSumValue();
}
/**
* Sets whether a bar is visible or not
*/
protected function setBarVisibility($dataset, DataItem $item, $top, $override = null)
{
$k = serialize([$dataset, $item->key]);
$this->bar_visibility[$k] = ($override === null ? $item->value != 0 : $override);
}
/**
* Returns TRUE if the item is visible on the graph
*/
public function isVisible($item, $dataset = 0)
{
$k = serialize([$dataset, $item->key]);
return isset($this->bar_visibility[$k]) && $this->bar_visibility[$k];
}
/**
* Returns the ordering for legend entries
*/
public function getLegendOrder()
{
return 'reverse';
}
}
|