| 1 |
<?php |
| 2 |
/** |
| 3 |
* Copyright (C) 2009-2022 Graham Breach |
| 4 |
* |
| 5 |
* This program is free software: you can redistribute it and/or modify |
| 6 |
* it under the terms of the GNU Lesser General Public License as published by |
| 7 |
* the Free Software Foundation, either version 3 of the License, or |
| 8 |
* (at your option) any later version. |
| 9 |
* |
| 10 |
* This program is distributed in the hope that it will be useful, |
| 11 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 |
* GNU Lesser General Public License for more details. |
| 14 |
* |
| 15 |
* You should have received a copy of the GNU Lesser General Public License |
| 16 |
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17 |
*/ |
| 18 |
/** |
| 19 |
* For more information, please contact <graham@goat1000.com> |
| 20 |
*/ |
| 21 |
|
| 22 |
namespace Goat1000\SVGGraph; |
| 23 |
|
| 24 |
class PieGraph extends Graph { |
| 25 |
|
| 26 |
// for internal use |
| 27 |
protected $x_centre; |
| 28 |
protected $y_centre; |
| 29 |
protected $radius_x; |
| 30 |
protected $radius_y; |
| 31 |
public $start_angle; |
| 32 |
public $end_angle; |
| 33 |
protected $s_angle; // start_angle in radians |
| 34 |
protected $full_angle; // amount of pie in radians |
| 35 |
protected $aspect_ratio; |
| 36 |
protected $calc_done; |
| 37 |
protected $slice_info = []; |
| 38 |
protected $total = 0; |
| 39 |
protected $legend_order = []; |
| 40 |
protected $dataset = 0; |
| 41 |
|
| 42 |
private $sub_total = 0; |
| 43 |
|
| 44 |
public function __construct($w, $h, array $settings, array $fixed_settings = []) |
| 45 |
{ |
| 46 |
// backwards compatibility |
| 47 |
$copy = [ |
| 48 |
'show_labels' => 'show_data_labels', |
| 49 |
'label_fade_in_speed' => 'data_label_fade_in_speed', |
| 50 |
'label_fade_out_speed' => 'data_label_fade_out_speed', |
| 51 |
]; |
| 52 |
foreach($copy as $from => $to) |
| 53 |
if(isset($settings[$from]) && !isset($settings[$to])) |
| 54 |
$settings[$to] = $settings[$from]; |
| 55 |
|
| 56 |
$fs = ['repeated_keys' => 'accept']; |
| 57 |
$fs = array_merge($fs, $fixed_settings); |
| 58 |
parent::__construct($w, $h, $settings, $fs); |
| 59 |
} |
| 60 |
|
| 61 |
/** |
| 62 |
* Calculates position of pie |
| 63 |
*/ |
| 64 |
protected function calc() |
| 65 |
{ |
| 66 |
$bound_x_left = $this->pad_left; |
| 67 |
$bound_y_top = $this->pad_top; |
| 68 |
$bound_x_right = $this->width - $this->pad_right; |
| 69 |
$bound_y_bottom = $this->height - $this->pad_bottom; |
| 70 |
|
| 71 |
$w = $bound_x_right - $bound_x_left; |
| 72 |
$h = $bound_y_bottom - $bound_y_top; |
| 73 |
|
| 74 |
$this->x_centre = (($bound_x_right - $bound_x_left) / 2) + $bound_x_left; |
| 75 |
$this->y_centre = (($bound_y_bottom - $bound_y_top) / 2) + $bound_y_top; |
| 76 |
$start_angle = $this->getOption('start_angle') % 360; |
| 77 |
while($start_angle < 0) |
| 78 |
$start_angle += 360; |
| 79 |
$this->start_angle = $start_angle; |
| 80 |
$this->s_angle = deg2rad($start_angle); |
| 81 |
|
| 82 |
// sanitize aspect ratio |
| 83 |
$aspect = $this->getOption('aspect_ratio'); |
| 84 |
$this->aspect_ratio = ($aspect != 'auto' && $aspect <= 0 ? 1.0 : $aspect); |
| 85 |
|
| 86 |
$end_angle = $this->getOption('end_angle'); |
| 87 |
if($end_angle === null || !is_numeric($end_angle) || |
| 88 |
$end_angle == $start_angle || abs($end_angle - $start_angle) % 360 == 0) { |
| 89 |
$this->full_angle = M_PI * 2.0; |
| 90 |
|
| 91 |
$this->setupAspectRatio($w, $h); |
| 92 |
|
| 93 |
} else { |
| 94 |
|
| 95 |
while($end_angle < $start_angle) |
| 96 |
$end_angle += 360; |
| 97 |
$this->end_angle = $end_angle; |
| 98 |
$this->setOption('end_angle', $end_angle); |
| 99 |
|
| 100 |
$full_angle = $end_angle - $start_angle; |
| 101 |
if($full_angle > 360) |
| 102 |
$full_angle %= 360; |
| 103 |
$this->full_angle = deg2rad($full_angle); |
| 104 |
|
| 105 |
if($this->getOption('slice_fit')) { |
| 106 |
// not a full pie, position based on actual shape |
| 107 |
$sw = 100; |
| 108 |
$sh = 100; |
| 109 |
if($this->aspect_ratio != 'auto') |
| 110 |
$sw /= $this->aspect_ratio; |
| 111 |
|
| 112 |
$all_slice = new SliceInfo($this->s_angle, |
| 113 |
deg2rad($end_angle), $sw, $sh); |
| 114 |
$bbox = $all_slice->boundingBox($this->getOption('reverse')); |
| 115 |
|
| 116 |
$bw = $bbox[2] - $bbox[0]; |
| 117 |
$bh = $bbox[3] - $bbox[1]; |
| 118 |
$scale_x = $bw / $w; |
| 119 |
$scale_y = $bh / $h; |
| 120 |
|
| 121 |
if($this->aspect_ratio == 'auto') { |
| 122 |
$this->x_centre = $bound_x_left + ($bbox[0] / -$scale_x); |
| 123 |
$this->y_centre = $bound_y_top + ($bbox[1] / -$scale_y); |
| 124 |
$w *= $scale_x; |
| 125 |
$h *= $scale_y; |
| 126 |
|
| 127 |
$this->aspect_ratio = $bh / $bw; |
| 128 |
} else { |
| 129 |
|
| 130 |
// calculate size and position from aspect ratio |
| 131 |
$scale_x = $scale_y = max($scale_x, $scale_y); |
| 132 |
$bw = ($bbox[2] - $bbox[0]) / $scale_x; |
| 133 |
$bh = ($bbox[3] - $bbox[1]) / $scale_y; |
| 134 |
$offset_x = $bbox[0] / -$scale_x; |
| 135 |
$offset_y = $bbox[1] / -$scale_y; |
| 136 |
|
| 137 |
$this->x_centre = $bound_x_left + ($w - $bw) / 2 + $offset_x; |
| 138 |
$this->y_centre = $bound_y_top + ($h - $bh) / 2 + $offset_y; |
| 139 |
} |
| 140 |
$this->radius_x = $sw / $scale_x; |
| 141 |
$this->radius_y = $sh / $scale_y; |
| 142 |
} else { |
| 143 |
$this->setupAspectRatio($w, $h); |
| 144 |
} |
| 145 |
} |
| 146 |
|
| 147 |
$this->calc_done = true; |
| 148 |
$this->sub_total = 0; |
| 149 |
} |
| 150 |
|
| 151 |
/** |
| 152 |
* Sets the aspect ratio and radius members |
| 153 |
*/ |
| 154 |
private function setupAspectRatio($w, $h) |
| 155 |
{ |
| 156 |
if($this->aspect_ratio == 'auto') |
| 157 |
$this->aspect_ratio = $h/$w; |
| 158 |
|
| 159 |
if($h / $w > $this->aspect_ratio) { |
| 160 |
$this->radius_x = $w / 2.0; |
| 161 |
$this->radius_y = $this->radius_x * $this->aspect_ratio; |
| 162 |
} else { |
| 163 |
$this->radius_y = $h / 2.0; |
| 164 |
$this->radius_x = $this->radius_y / $this->aspect_ratio; |
| 165 |
} |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Draws the pie graph |
| 170 |
*/ |
| 171 |
protected function draw() |
| 172 |
{ |
| 173 |
if(!$this->calc_done) |
| 174 |
$this->calc(); |
| 175 |
|
| 176 |
$min_slice_angle = $this->getOption(['data_label_min_space', $this->dataset]); |
| 177 |
$vcount = 0; |
| 178 |
|
| 179 |
// need to store the original position of each value, because the |
| 180 |
// sorted list must still refer to the relevant legend entries |
| 181 |
$values = []; |
| 182 |
foreach($this->values[$this->dataset] as $position => $item) { |
| 183 |
$values[] = [$position, $item->value, $item]; |
| 184 |
if($item->value !== null) |
| 185 |
++$vcount; |
| 186 |
} |
| 187 |
if($this->getOption('sort')) { |
| 188 |
uasort($values, function($a, $b) { |
| 189 |
return $b[1] - $a[1]; |
| 190 |
}); |
| 191 |
} |
| 192 |
|
| 193 |
$body = $this->underShapes(); |
| 194 |
$slice = 0; |
| 195 |
$slices = []; |
| 196 |
$slice_no = 0; |
| 197 |
$legend_entries = []; |
| 198 |
$legend_order = []; |
| 199 |
foreach($values as $value) { |
| 200 |
|
| 201 |
// get the original array position of the value |
| 202 |
$original_position = $value[0]; |
| 203 |
$item = $value[2]; |
| 204 |
$value = $value[1]; |
| 205 |
$key = $item->key; |
| 206 |
$colour_index = $this->getOption('keep_colour_order') ? $original_position : $slice; |
| 207 |
if($this->getOption('legend_show_empty') || $item->value != 0) { |
| 208 |
$attr = [ |
| 209 |
'fill' => $this->getColour($item, $colour_index, $this->dataset, false, true) |
| 210 |
]; |
| 211 |
$this->setStroke($attr, $item, $colour_index, $this->dataset, 'round'); |
| 212 |
|
| 213 |
// use the original position for legend index |
| 214 |
$legend_entries[] = [$original_position, $item, $attr]; |
| 215 |
$legend_order[] = $original_position; |
| 216 |
++$slice; |
| 217 |
} |
| 218 |
|
| 219 |
if(!$this->getSliceInfo($slice_no++, $item, $angle_start, $angle_end, |
| 220 |
$radius_x, $radius_y)) |
| 221 |
continue; |
| 222 |
|
| 223 |
// store details for label position and tail |
| 224 |
$this->slice_info[$original_position] = new SliceInfo($angle_start, |
| 225 |
$angle_end, $radius_x, $radius_y); |
| 226 |
|
| 227 |
// add the data label if the slice angle is big enough |
| 228 |
if($this->slice_info[$original_position]->degrees() >= $min_slice_angle) { |
| 229 |
$parts = []; |
| 230 |
if($this->getOption('show_label_key')) { |
| 231 |
$label_key = $this->getKey($this->values->associativeKeys() ? |
| 232 |
$original_position : $key); |
| 233 |
if($this->getOption('datetime_keys')) { |
| 234 |
$number_key = new Number($label_key); |
| 235 |
$dtf = new DateTimeFormatter; |
| 236 |
$dt = new \DateTime('@' . $number_key); |
| 237 |
$label_key = $dtf->format($dt, $this->getOption('data_label_datetime_format')); |
| 238 |
} |
| 239 |
$parts = explode("\n", $label_key); |
| 240 |
} |
| 241 |
if($this->getOption('show_label_amount')) { |
| 242 |
if($value === null) { |
| 243 |
$parts[] = ''; |
| 244 |
} else { |
| 245 |
$num = new Number($value * 1.0, $this->getOption('units_label'), |
| 246 |
$this->getOption('units_before_label')); |
| 247 |
$parts[] = $num->format(); |
| 248 |
} |
| 249 |
} |
| 250 |
if($this->getOption('show_label_percent')) { |
| 251 |
$num = new Number($value / $this->total * 100.0, '%'); |
| 252 |
$parts[] = $num->format($this->getOption('label_percent_decimals')); |
| 253 |
} |
| 254 |
$label_content = implode("\n", $parts); |
| 255 |
|
| 256 |
$this->addDataLabel($this->dataset, $original_position, $attr, $item, |
| 257 |
$this->x_centre, $this->y_centre, 1, 1, $label_content); |
| 258 |
} |
| 259 |
|
| 260 |
if($radius_x || $radius_y) { |
| 261 |
|
| 262 |
$this->calcSlice($angle_start, $angle_end, $radius_x, $radius_y, |
| 263 |
$x1, $y1, $x2, $y2); |
| 264 |
$single_slice = ($vcount == 1) || |
| 265 |
((string)$x1 == (string)$x2 && (string)$y1 == (string)$y2 && |
| 266 |
(string)$angle_start != (string)$angle_end); |
| 267 |
|
| 268 |
if($this->getOption('semantic_classes')) |
| 269 |
$attr['class'] = 'series0'; |
| 270 |
|
| 271 |
$this_slice = [ |
| 272 |
'original_position' => $original_position, |
| 273 |
'attr' => $attr, |
| 274 |
'item' => $item, |
| 275 |
'angle_start' => $angle_start, |
| 276 |
'angle_end' => $angle_end, |
| 277 |
'radius_x' => $radius_x, |
| 278 |
'radius_y' => $radius_y, |
| 279 |
'single_slice' => $single_slice, |
| 280 |
'colour_index' => $colour_index, |
| 281 |
]; |
| 282 |
if($single_slice) |
| 283 |
array_unshift($slices, $this_slice); |
| 284 |
else |
| 285 |
$slices[] = $this_slice; |
| 286 |
} |
| 287 |
} |
| 288 |
|
| 289 |
// put the slices back in natural order for the legend |
| 290 |
usort($legend_entries, function($a, $b) { return $a[0] - $b[0]; }); |
| 291 |
foreach($legend_entries as $e) { |
| 292 |
$this->setLegendEntry(0, $e[0], $e[1], $e[2]); |
| 293 |
} |
| 294 |
$this->legend_order = $legend_order; |
| 295 |
|
| 296 |
$group = []; |
| 297 |
$series = $this->drawSlices($slices); |
| 298 |
if($this->getOption('semantic_classes')) |
| 299 |
$group['class'] = 'series'; |
| 300 |
$shadow_id = $this->defs->getShadow(); |
| 301 |
if($shadow_id !== null) |
| 302 |
$group['filter'] = 'url(#' . $shadow_id . ')'; |
| 303 |
if(!empty($group)) |
| 304 |
$series = $this->element('g', $group, null, $series); |
| 305 |
$body .= $series; |
| 306 |
|
| 307 |
$body .= $this->overShapes(); |
| 308 |
$extras = $this->pieExtras(); |
| 309 |
return $body . $extras; |
| 310 |
} |
| 311 |
|
| 312 |
/** |
| 313 |
* Returns the SVG markup to draw all slices |
| 314 |
*/ |
| 315 |
protected function drawSlices($slice_list) |
| 316 |
{ |
| 317 |
$slices = []; |
| 318 |
foreach($slice_list as $slice) { |
| 319 |
$item = $slice['item']; |
| 320 |
|
| 321 |
if($this->getOption('show_tooltips')) |
| 322 |
$this->setTooltip($slice['attr'], $item, $this->dataset, $item->key, |
| 323 |
$item->value, true); |
| 324 |
if($this->getOption('show_context_menu')) |
| 325 |
$this->setContextMenu($slice['attr'], $this->dataset, $item, true); |
| 326 |
$path = $this->getSlice($item, |
| 327 |
$slice['angle_start'], $slice['angle_end'], |
| 328 |
$slice['radius_x'], $slice['radius_y'], |
| 329 |
$slice['attr'], $slice['single_slice'], $slice['colour_index']); |
| 330 |
$this_slice = $this->getLink($item, $item->key, $path); |
| 331 |
$slices[] = $this_slice; |
| 332 |
} |
| 333 |
return implode($slices); |
| 334 |
} |
| 335 |
|
| 336 |
/** |
| 337 |
* Returns a single slice of pie |
| 338 |
*/ |
| 339 |
protected function getSlice($item, $angle_start, $angle_end, |
| 340 |
$radius_x, $radius_y, &$attr, $single_slice, $colour_index) |
| 341 |
{ |
| 342 |
$x_start = $y_start = $x_end = $y_end = 0; |
| 343 |
$angle_start += $this->s_angle; |
| 344 |
$angle_end += $this->s_angle; |
| 345 |
$this->calcSlice($angle_start, $angle_end, $radius_x, $radius_y, |
| 346 |
$x_start, $y_start, $x_end, $y_end); |
| 347 |
if($single_slice && $this->full_angle >= M_PI * 2.0) { |
| 348 |
$attr['cx'] = $this->x_centre; |
| 349 |
$attr['cy'] = $this->y_centre; |
| 350 |
$attr['rx'] = $radius_x; |
| 351 |
$attr['ry'] = $radius_y; |
| 352 |
return $this->element('ellipse', $attr); |
| 353 |
} else { |
| 354 |
$outer = ($angle_end - $angle_start > M_PI ? 1 : 0); |
| 355 |
$sweep = ($this->getOption('reverse') ? 0 : 1); |
| 356 |
$d = new PathData('M', $this->x_centre, $this->y_centre, 'L', $x_start, |
| 357 |
$y_start, 'A', $radius_x, $radius_y, 0, $outer, $sweep, $x_end, |
| 358 |
$y_end, 'z'); |
| 359 |
$attr['d'] = $d; |
| 360 |
return $this->element('path', $attr); |
| 361 |
} |
| 362 |
} |
| 363 |
|
| 364 |
/** |
| 365 |
* Calculates start and end points of slice |
| 366 |
*/ |
| 367 |
protected function calcSlice($angle_start, $angle_end, $radius_x, $radius_y, |
| 368 |
&$x_start, &$y_start, &$x_end, &$y_end) |
| 369 |
{ |
| 370 |
$reverse = $this->getOption('reverse'); |
| 371 |
$x_start = ($radius_x * cos($angle_start)); |
| 372 |
$y_start = ($reverse ? -1 : 1) * |
| 373 |
($radius_y * sin($angle_start)); |
| 374 |
$x_end = ($radius_x * cos($angle_end)); |
| 375 |
$y_end = ($reverse ? -1 : 1) * |
| 376 |
($radius_y * sin($angle_end)); |
| 377 |
|
| 378 |
$x_start += $this->x_centre; |
| 379 |
$y_start += $this->y_centre; |
| 380 |
$x_end += $this->x_centre; |
| 381 |
$y_end += $this->y_centre; |
| 382 |
} |
| 383 |
|
| 384 |
/** |
| 385 |
* Finds the angles and radii for a slice |
| 386 |
*/ |
| 387 |
protected function getSliceInfo($num, $item, &$angle_start, &$angle_end, |
| 388 |
&$radius_x, &$radius_y) |
| 389 |
{ |
| 390 |
if(!$item->value) |
| 391 |
return false; |
| 392 |
|
| 393 |
$unit_slice = $this->full_angle / $this->total; |
| 394 |
$angle_start = $this->sub_total * $unit_slice; |
| 395 |
$angle_end = ($this->sub_total + $item->value) * $unit_slice; |
| 396 |
$radius_x = $this->radius_x; |
| 397 |
$radius_y = $this->radius_y; |
| 398 |
|
| 399 |
$this->sub_total += $item->value; |
| 400 |
return true; |
| 401 |
} |
| 402 |
|
| 403 |
/** |
| 404 |
* Checks that the data are valid |
| 405 |
*/ |
| 406 |
protected function checkValues() |
| 407 |
{ |
| 408 |
$this->dataset = $this->getOption(['dataset',0], 0); |
| 409 |
parent::checkValues(); |
| 410 |
if($this->values->getMinValue($this->dataset) < 0) |
| 411 |
throw new \Exception('Negative value for pie chart'); |
| 412 |
|
| 413 |
$sum = 0; |
| 414 |
foreach($this->values[$this->dataset] as $item) { |
| 415 |
if($item->value !== null && !is_numeric($item->value)) |
| 416 |
throw new \Exception('Non-numeric value'); |
| 417 |
$sum += $item->value; |
| 418 |
} |
| 419 |
if($sum <= 0) |
| 420 |
throw new \Exception('Empty pie chart'); |
| 421 |
|
| 422 |
$this->total = $sum; |
| 423 |
} |
| 424 |
|
| 425 |
/** |
| 426 |
* Returns extra drawing code that goes between pie and labels |
| 427 |
*/ |
| 428 |
protected function pieExtras() |
| 429 |
{ |
| 430 |
return ''; |
| 431 |
} |
| 432 |
|
| 433 |
/** |
| 434 |
* Return box for legend |
| 435 |
*/ |
| 436 |
public function drawLegendEntry($x, $y, $w, $h, $entry) |
| 437 |
{ |
| 438 |
$bar = ['x' => $x, 'y' => $y, 'width' => $w, 'height' => $h]; |
| 439 |
return $this->element('rect', $bar, $entry->style); |
| 440 |
} |
| 441 |
|
| 442 |
/** |
| 443 |
* Returns the position for the label and its target |
| 444 |
*/ |
| 445 |
public function dataLabelPosition($dataset, $index, &$item, $x, $y, $w, $h, |
| 446 |
$label_w, $label_h) |
| 447 |
{ |
| 448 |
if(isset($this->slice_info[$index])) { |
| 449 |
$a = $this->slice_info[$index]->midAngle(); |
| 450 |
$rx = $this->slice_info[$index]->radius_x; |
| 451 |
$ry = $this->slice_info[$index]->radius_y; |
| 452 |
|
| 453 |
// place it at the label_position distance from centre |
| 454 |
$pos_radius = $this->getOption('label_position'); |
| 455 |
$reverse = $this->getOption('reverse'); |
| 456 |
$ac = $this->s_angle + $a; |
| 457 |
$xc = $pos_radius * $rx * cos($ac); |
| 458 |
$yc = ($reverse ? -1 : 1) * $pos_radius * $ry * sin($ac); |
| 459 |
$pos = new Number($xc) . ' ' . new Number($yc); |
| 460 |
|
| 461 |
if($pos_radius > 1) { |
| 462 |
$space = $this->getOption(['data_label_space', $dataset]); |
| 463 |
$xt = ($rx + $space) * cos($ac); |
| 464 |
$yt = ($reverse ? -1 : 1) * ($ry + $space) * sin($ac); |
| 465 |
} else { |
| 466 |
$xt = $rx * 0.5 * cos($ac); |
| 467 |
$yt = ($reverse ? -1 : 1) * $ry * 0.5 * sin($ac); |
| 468 |
} |
| 469 |
$target = [$x + $xt, $y + $yt]; |
| 470 |
} else { |
| 471 |
$pos = 'middle centre'; |
| 472 |
$target = [$x, $y]; |
| 473 |
} |
| 474 |
return [$pos, $target]; |
| 475 |
} |
| 476 |
|
| 477 |
/** |
| 478 |
* Returns the style options for bar labels |
| 479 |
*/ |
| 480 |
public function dataLabelStyle($dataset, $index, &$item) |
| 481 |
{ |
| 482 |
$style = parent::dataLabelStyle($dataset, $index, $item); |
| 483 |
|
| 484 |
// old pie label settings can override global data_label settings |
| 485 |
$opts = [ |
| 486 |
'font' => 'label_font', |
| 487 |
'font_size' => 'label_font_size', |
| 488 |
'font_weight' => 'label_font_weight', |
| 489 |
'colour' => 'label_colour', |
| 490 |
'back_colour' => 'label_back_colour', |
| 491 |
]; |
| 492 |
foreach($opts as $key => $opt) |
| 493 |
if(isset($this->settings[$opt])) |
| 494 |
$style[$key] = $this->settings[$opt]; |
| 495 |
|
| 496 |
return $style; |
| 497 |
} |
| 498 |
|
| 499 |
/** |
| 500 |
* Overload to return the direction of the pie centre |
| 501 |
*/ |
| 502 |
public function dataLabelTailDirection($dataset, $index, $hpos, $vpos) |
| 503 |
{ |
| 504 |
if(isset($this->slice_info[$index])) { |
| 505 |
$a = rad2deg($this->slice_info[$index]->midAngle()); |
| 506 |
// tail direction is opposite slice direction |
| 507 |
if($this->getOption('reverse')) |
| 508 |
return fmod(900 - $this->start_angle - $a, 360); // 900 == 360 + 360 + 180 |
| 509 |
else |
| 510 |
return fmod(180 + $this->start_angle + $a, 360); |
| 511 |
} |
| 512 |
|
| 513 |
// fall back to default |
| 514 |
return parent::dataLabelTailDirection($dataset, $index, $hpos, $vpos); |
| 515 |
} |
| 516 |
|
| 517 |
/** |
| 518 |
* Returns the order that the slices appear in |
| 519 |
*/ |
| 520 |
public function getLegendOrder() |
| 521 |
{ |
| 522 |
return $this->legend_order; |
| 523 |
} |
| 524 |
} |
| 525 |
|
| 526 |
|