. */ /** * For more information, please contact */ namespace Goat1000\SVGGraph; class StackedBarAndLineGraph extends StackedBarGraph { protected $linegraph = null; protected $dataset_types = []; protected $min_values = null; protected $max_values = null; /** * We need an instance of the LineGraph class */ public function __construct($w, $h, array $settings, array $fixed_settings = []) { $fixed = ['single_axis' => false]; $fixed = array_merge($fixed, $fixed_settings); parent::__construct($w, $h, $settings, $fixed); // prevent repeated labels unset($settings['label']); $this->linegraph = new MultiLineGraph($w, $h, $settings); // validate secondary axis datasets are only lines if(isset($settings['dataset_axis'])) { if(!is_array($settings['dataset_axis'])) { $this->setOption('dataset_axis', null); return; } $lines = is_array($settings['line_dataset']) ? $settings['line_dataset'] : [$settings['line_dataset']]; $line_map = []; foreach($lines as $line) $line_map[$line] = 1; foreach($settings['dataset_axis'] as $dataset => $axis) { if($axis != 0 && !isset($line_map[$dataset])) { throw new \Exception('Bar datasets must use axis 0'); } } } } /** * Draws the bars and lines */ protected function draw() { if($this->getOption('log_axis_y')) throw new \Exception('log_axis_y not supported by StackedBarAndLineGraph'); $body = $this->grid() . $this->underShapes(); // LineGraph has not been initialised, need to copy in details $copy = ['colours', 'links', 'x_axes', 'y_axes', 'main_x_axis', 'main_y_axis', 'legend', // needed for best-fit line support 'g_width', 'g_height', 'pad_left', 'pad_top']; foreach($copy as $member) $this->linegraph->{$member} = $this->{$member}; // keep gradients and patterns synced $this->linegraph->defs =& $this->defs; // find the lines $lines = $this->getOption('line_dataset'); $line_breaks = []; $line_points = []; $points = []; if(!is_array($lines)) $lines = [$lines]; rsort($lines); foreach($lines as $line) { $line_breaks[$line] = $this->getOption(['line_breaks', $line]); $line_points[$line] = []; $points[$line] = []; } $lines = array_flip($lines); $y_axis_pos = $this->height - $this->pad_bottom - $this->y_axes[$this->main_y_axis]->zero(); $y_bottom = min($y_axis_pos, $this->height - $this->pad_bottom); $this->barSetup(); $marker_offset = $this->x_axes[$this->main_x_axis]->unit() / 2; $datasets = $this->multi_graph->getEnabledDatasets(); // draw bars, store line points $line_dataset = 0; $bars = ''; $legend_entries = []; foreach($this->multi_graph as $bnum => $itemlist) { $item = $itemlist[0]; $bar_pos = $this->gridPosition($item, $bnum); if($bar_pos !== null) { $yplus = $yminus = 0; $chunk_values = []; foreach($datasets as $j) { $y_axis = $this->datasetYAxis($j); $item = $itemlist[$j]; if(array_key_exists($j, $lines)) { $line_dataset = $j; $this->dataset_types[$j] = 'l'; if($line_breaks[$line_dataset] && $item->value === null && count($points[$line_dataset]) > 0) { $line_points[$line_dataset][] = $points[$line_dataset]; $points[$line_dataset] = []; } elseif($item->value !== null) { $x = $bar_pos + $marker_offset; $y = $this->gridY($item->value, $y_axis); $points[$line_dataset][] = [$x, $y, $item, $line_dataset, $bnum]; $this->setBarVisibility($line_dataset, $item, false, true); } continue; } $this->dataset_types[$j] = 'b'; if($item->value !== null) { // sort the values from bottom to top, assigning position 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; // store whether the bar can be seen or not $top = (++$b == $bar_count); $this->setBarVisibility($j, $item, $top, $top || $item->value != 0); $legend_entries[$j][$bnum] = $item; $bars .= $this->drawBar($item, $bnum, $start, null, $j, ['top' => $top]); } $this->barTotals($item, $bnum, $yplus, $yminus, $j); } } foreach($legend_entries as $j => $dataset) foreach($dataset as $bnum => $item) $this->setBarLegendEntry($j, $bnum, $item); foreach($points as $line_dataset => $line) { if(!empty($line)) $line_points[$line_dataset][] = $line; } // draw lines clipped to grid $graph_line = ''; foreach($line_points as $dataset => $points) { foreach($points as $p) { $graph_line .= $this->linegraph->drawLine($dataset, $p, $y_bottom); } } $group = []; $this->clipGrid($group); list($best_fit_above, $best_fit_below) = $this->linegraph->bestFitLines(); $bars .= $best_fit_below; $bars .= $this->element('g', $group, null, $graph_line); $group = []; if($this->getOption('semantic_classes')) $group['class'] = 'series'; $shadow_id = $this->defs->getShadow(); if($shadow_id !== null) $group['filter'] = 'url(#' . $shadow_id . ')'; if(!empty($group)) $bars = $this->element('g', $group, null, $bars); $body .= $bars; $body .= $this->overShapes(); $body .= $this->axes(); // add in the markers created by line graph $body .= $this->linegraph->drawMarkers(); $body .= $best_fit_above; return $body; } /** * Return box or line for legend */ public function drawLegendEntry($x, $y, $w, $h, $entry) { if(isset($entry->style['line_style'])) return $this->linegraph->drawLegendEntry($x, $y, $w, $h, $entry); return parent::drawLegendEntry($x, $y, $w, $h, $entry); } /** * Draws this graph's data labels, and the line graph's data labels */ protected function drawDataLabels() { $labels = parent::drawDataLabels(); $labels .= $this->linegraph->drawDataLabels(); return $labels; } /** * Returns the minimum value for an axis */ protected function getAxisMinValue($axis) { if($this->min_values === null) $this->calcMinMaxValues(); return isset($this->min_values[$axis]) ? $this->min_values[$axis] : null; } /** * Returns the maximum value for an axis */ protected function getAxisMaxValue($axis) { if($this->max_values === null) $this->calcMinMaxValues(); return isset($this->max_values[$axis]) ? $this->max_values[$axis] : null; } /** * Finds the minimum and maximum stack or line */ private function calcMinMaxValues() { $lines = $this->getOption('line_dataset'); if(!is_array($lines)) $lines = [$lines]; sort($lines); $lines = array_flip($lines); $axis_count = $this->yAxisCount(); $axis_max = array_fill(0, $axis_count, null); $axis_min = array_fill(0, $axis_count, null); $stack_max = null; $stack_min = null; $datasets = $this->multi_graph->getEnabledDatasets(); $bar_datasets = 0; foreach($datasets as $j) { if(!array_key_exists($j, $lines)) ++$bar_datasets; } if($bar_datasets === 0) throw new \Exception('No bar datasets enabled'); foreach($this->multi_graph as $itemlist) { $stack_pos = $stack_neg = 0; foreach($datasets as $j) { $item = $itemlist[$j]; if($item->value === null) continue; if(!is_numeric($item->value)) throw new \Exception('Non-numeric value'); if(array_key_exists($j, $lines)) { // for lines find the global min/max for each axis $axis = $this->datasetYAxis($j); if($axis_min[$axis] === null || $axis_min[$axis] > $item->value) $axis_min[$axis] = $item->value; if($axis_max[$axis] === null || $axis_max[$axis] < $item->value) $axis_max[$axis] = $item->value; } else { // for bars need to find min and max stack sizes, using positive // and negative stacks if($item->value < 0) $stack_neg += $item->value; else $stack_pos += $item->value; } } if($stack_min === null || $stack_neg < $stack_min) $stack_min = $stack_neg; if($stack_max === null || $stack_neg > $stack_max) $stack_max = $stack_neg; if($stack_min === null || $stack_pos < $stack_min) $stack_min = $stack_pos; if($stack_max === null || $stack_pos > $stack_max) $stack_max = $stack_pos; } if($axis_min[0] === null || $stack_min < $axis_min[0]) $axis_min[0] = $stack_min; if($axis_max[0] === null || $stack_max > $axis_max[0]) $axis_max[0] = $stack_max; $this->min_values = $axis_min; $this->max_values = $axis_max; } /** * Returns the order that the datasets should appear in */ public function getLegendOrder() { $stack = []; $lines = []; foreach($this->dataset_types as $d => $t) if($t == 'l') $lines[] = $d; else array_unshift($stack, $d); return array_merge($stack, $lines); } }