. */ /** * For more information, please contact */ namespace Goat1000\SVGGraph; class BoxAndWhiskerGraph extends PointGraph { private $min_value = null; private $max_value = null; public function __construct($w, $h, array $settings, array $fixed_settings = []) { $fs = [ 'label_centre' => !isset($settings['datetime_keys']), 'require_structured' => ['top', 'bottom', 'wtop', 'wbottom'], ]; $fs = array_merge($fs, $fixed_settings); parent::__construct($w, $h, $settings, $fs); } protected function draw() { $body = $this->grid() . $this->underShapes(); $bar_width = $this->barWidth(); $dataset = $this->getOption(['dataset', 0], 0); $x_axis = $this->x_axes[$this->main_x_axis]; $bspace = max(0, ($this->x_axes[$this->main_x_axis]->unit() - $bar_width) / 2); $whisker_width = $this->getOption('whisker_width'); $whisker_dash = $this->getOption('whisker_dash'); $median_width = $this->getOption('median_stroke_width'); $median_dash = $this->getOption('median_dash'); $median_colour = $this->getOption('median_colour'); $bnum = 0; $series = ''; foreach($this->values[$dataset] as $item) { $bar_pos = $this->gridPosition($item, $bnum); if($item->value !== null && $bar_pos !== null) { $box_style = ['fill' => $this->getColour($item, $bnum, $dataset)]; $this->setStroke($box_style, $item, $bnum, $dataset); $this->setLegendEntry($dataset, $bnum, $item, $box_style); $top = $item->top; $bottom = $item->bottom; $round = max($this->getItemOption('bar_round', $dataset, $item), 0); $m_colour = null; if(!empty($median_colour)) { $cg = new ColourGroup($this, $item, $bnum, $dataset, 'median_colour'); $m_colour = $cg->stroke(); } $shape = $this->whiskerBox($bspace + $bar_pos, $bar_width, $item->value, $top, $bottom, $item->wtop, $item->wbottom, $round, $whisker_width, $whisker_dash, $median_width, $median_dash, $m_colour); // wrap the whisker box in a group $g = []; $show_label = $this->addDataLabel($dataset, $bnum, $g, $item, $bspace + $bar_pos, $this->gridY($top), $bar_width, $this->gridY($bottom) - $this->gridY($top)); if($this->getOption('show_tooltips')) $this->setTooltip($g, $item, $dataset, $item->key, $item->value, $show_label); if($this->getOption('show_context_menu')) $this->setContextMenu($g, $dataset, $item, $show_label); if($this->getOption('semantic_classes')) $g['class'] = 'series0'; $group = $this->element('g', array_merge($g, $box_style), null, $shape); $series .= $this->getLink($item, $item->key, $group); // add outliers as markers $x = $bar_pos + $x_axis->unit() / 2; foreach($this->getOutliers($item) as $ovalue) { $y = $this->gridY($ovalue); $this->addMarker($x, $y, $item); } } ++$bnum; } $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)) $series = $this->element('g', $group, null, $series); $body .= $series; $body .= $this->overShapes(); $body .= $this->axes(); $body .= $this->drawMarkers(); return $body; } /** * Returns the width of a bar */ protected function barWidth() { $bw = $this->getOption('bar_width'); if(is_numeric($bw) && $bw >= 1) return $bw; $unit_w = $this->x_axes[$this->main_x_axis]->unit(); $bw = $unit_w - $this->getOption('bar_space'); return max(1, $bw, $this->getOption('bar_width_min')); } /** * Returns the code for a box with whiskers */ protected function whiskerBox($x, $w, $median, $top, $bottom, $wtop, $wbottom, $round, $whisker_width, $whisker_dash, $median_width, $median_dash, Colour $median_colour = null) { $t = $this->gridY($top); $b = $this->gridY($bottom); $wt = $this->gridY($wtop); $wb = $this->gridY($wbottom); $box = ['x' => $x, 'y' => $t, 'width' => $w, 'height' => $b - $t]; if($round > 0) { $box['rx'] = $box['ry'] = min($round, $box['width'] / 2, $box['height'] / 2); } $rect = $this->element('rect', $box); // whisker lines $lg = $w * (1 - $whisker_width) * 0.5; $ll = $x + $lg; $lr = $x + $w - $lg; $l = ['x1' => $ll, 'x2' => $lr]; $l['y1'] = $l['y2'] = $wt; $l1 = $this->element('line', $l); $l['y1'] = $l['y2'] = $wb; $l2 = $this->element('line', $l); // median line $l['x1'] = $x; $l['x2'] = $x + $w; $l['y1'] = $l['y2'] = $this->gridY($median); $style = [ 'stroke-width' => $median_width ]; if($median_colour !== null) $style['stroke'] = $median_colour; if(!empty($median_dash)) $style['stroke-dasharray'] = $median_dash; $l3 = $this->element('line', array_merge($l, $style)); // whisker dashed lines $style = [ 'stroke-dasharray' => $whisker_dash ]; $l['x1'] = $l['x2'] = $x + $w / 2; $l['y1'] = $wt; $l['y2'] = $t; $w1 = $this->element('line', array_merge($l, $style)); $l['y1'] = $wb; $l['y2'] = $b; $w2 = $this->element('line', array_merge($l, $style)); return $rect . $w1 . $w2 . $l1 . $l2 . $l3; } /** * Checks that the data contains sensible values */ protected function checkValues() { parent::checkValues(); foreach($this->values[0] as $item) { $val = $item->value; if($val === null) continue; $wb = $item->wbottom; $wt = $item->wtop; $b = $item->bottom; $t = $item->top; if($wb > $b || $wt < $t || $val < $b || $val > $t) { $wb = new Number($wb); $b = new Number($b); $wt = new Number($wt); $t = new Number($t); $val = new Number($val); throw new \Exception('Data problem: ' . $wb . '--[' . $b . ' ' . $val . ' ' . $t . ']--' . $wt); } } } /** * Return box for legend */ public function drawLegendEntry($x, $y, $w, $h, $entry) { $box = ['x' => $x, 'y' => $y, 'width' => $w, 'height' => $h]; return $this->element('rect', $box, $entry->style); } /** * Returns the maximum bar end */ public function getMaxValue() { if($this->max_value !== null) return $this->max_value; $max = null; $dataset = $this->getOption(['dataset', 0], 0); foreach($this->values[$dataset] as $item) { if($item->value === null) continue; $points = [$item->wtop]; $points = array_merge($points, $this->getOutliers($item)); $m = max($points); if($max === null || $m > $max) $max = $m; } return ($this->max_value = $max); } /** * Returns the minimum bar end */ public function getMinValue() { if($this->min_value !== null) return $this->min_value; $min = null; $dataset = $this->getOption(['dataset', 0], 0); foreach($this->values[$dataset] as $item) { if($item->value === null) continue; $points = [$item->wbottom]; $points = array_merge($points, $this->getOutliers($item)); $m = min($points); if($min === null || $m < $min) $min = $m; } return ($this->min_value = $min); } /** * Returns the list of outliers for an item */ protected function getOutliers(&$item) { $outliers = []; $structure = $this->getOption('structure'); if(!isset($structure['outliers']) || !is_array($structure['outliers'])) return $outliers; $min = $item->wbottom; $max = $item->wtop; foreach($structure['outliers'] as $o) { $v = $item->rawData($o); if($v !== null && ($v > $max || $v < $min)) $outliers[] = $v; } return $outliers; } }