PluginProbe
E2Pdf – Export Pdf Tool for WordPress / 1.32.49
E2Pdf – Export Pdf Tool for WordPress v1.32.49
1.32.49 1.32.48 1.32.43 1.32.40 1.32.34 1.32.32 1.32.31 1.32.26 1.32.22 1.32.23 1.32.18 1.32.17 1.32.15 trunk 1.00.00 1.00.13 1.01.01 1.02.02 1.03.07 1.04.07 1.05.03 1.06.02 1.07.11 1.08.00 1.08.06 All 72 releases
e2pdf / vendors / svggraph / PieGraph.php

PieGraph.php in E2Pdf – Export Pdf Tool for WordPress 1.32.49, at vendors/svggraph/PieGraph.php

526 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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