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 / RadarGraph.php

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

599 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Copyright (C) 2012-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 /**
25 * RadarGraph - a line graph that goes around in circles
26 */
27 class RadarGraph extends LineGraph {
28
29 protected $xc;
30 protected $yc;
31 protected $radius;
32 protected $arad;
33
34 public function __construct($w, $h, array $settings, array $fixed_settings = [])
35 {
36 // in the case of radar graphs, $label_centre means we want an axis that
37 // ends at N points + 1
38 $fs = [
39 'label_centre' => true,
40 'single_axis' => true,
41 ];
42 $fs = array_merge($fs, $fixed_settings);
43 parent::__construct($w, $h, $settings, $fs);
44 }
45
46 protected function draw()
47 {
48 $body = $this->grid() . $this->underShapes();
49
50 $bnum = 0;
51 $cmd = 'M';
52 $y_axis = $this->y_axes[$this->main_y_axis];
53 $dataset = $this->getOption(['dataset', 0], 0);
54
55 $graph_line = '';
56 $line_breaks = $this->getOption(['line_breaks', $dataset]);
57 $points = [];
58 $first_point = null;
59 foreach($this->values[$dataset] as $item) {
60 if($line_breaks && $item->value === null && count($points) > 0) {
61 $graph_line .= $this->drawLine($dataset, $points, 0);
62 $points = [];
63 } else {
64 $point_pos = $this->gridPosition($item, $bnum);
65 if($item->value !== null && $point_pos !== null) {
66 $val = $y_axis->position($item->value);
67 $angle = $this->arad + $point_pos / $this->g_height;
68 $x = $this->xc + ($val * sin($angle));
69 $y = $this->yc + ($val * cos($angle));
70 $points[] = [$x, $y, $item, $dataset, $bnum];
71 if($first_point === null)
72 $first_point = $points[0];
73 }
74 }
75 ++$bnum;
76 }
77
78 // close graph or segment?
79 if($first_point && (!$line_breaks || $first_point[4] == 0)) {
80 $first_point[2] = null;
81 $points[] = $first_point;
82 }
83
84 $graph_line .= $this->drawLine($dataset, $points, 0);
85 $group = [];
86 if($this->getOption('semantic_classes'))
87 $group['class'] = 'series';
88 $graph_line = $this->element('g', $group, null, $graph_line);
89
90 $group = [];
91 $shadow_id = $this->defs->getShadow();
92 if($shadow_id !== null)
93 $group['filter'] = 'url(#' . $shadow_id . ')';
94 if(!empty($group))
95 $graph_line = $this->element('g', $group, null, $graph_line);
96
97 $body .= $graph_line;
98 $body .= $this->overShapes();
99 $body .= $this->axes();
100 $body .= $this->drawMarkers();
101 return $body;
102 }
103
104 /**
105 * Fill from centre point
106 */
107 protected function fillFrom(Number $x, Number $y)
108 {
109 return new PathData('M', $this->xc, $this->yc);
110 }
111
112 /**
113 * Fill to centre point
114 */
115 protected function fillTo(Number $x, Number $y)
116 {
117 return new PathData('L', $this->xc, $this->yc, 'z');
118 }
119
120 /**
121 * Finds the grid position for radar graphs, returns NULL if not on graph
122 */
123 protected function gridPosition($item, $ikey)
124 {
125 $gkey = $this->values->associativeKeys() ? $ikey : $item->key;
126 $axis = $this->x_axes[$this->main_x_axis];
127 $offset = $axis->position($gkey);
128 if($offset >= 0 && $offset < $this->g_width)
129 return $this->getOption('reverse') ? -$offset : $offset;
130 return null;
131 }
132
133 /**
134 * Returns the $x value as a grid position
135 */
136 public function gridX($x, $axis_no = null)
137 {
138 $p = $this->unitsX($x, $axis_no);
139 return $p;
140 }
141
142 /**
143 * Returns the $y value as a grid position
144 */
145 public function gridY($y, $axis_no = null)
146 {
147 $p = $this->unitsY($y, $axis_no);
148 return $p;
149 }
150
151 /**
152 * Returns a DisplayAxisRadar for the round axis
153 */
154 protected function getDisplayAxis($axis, $axis_no, $orientation, $type)
155 {
156 $var = 'main_' . $type . '_axis';
157 $main = ($axis_no == $this->{$var});
158 if($orientation == 'h')
159 return new DisplayAxisRadar($this, $axis, $axis_no, $orientation, $type,
160 $main, $this->xc, $this->yc, $this->radius);
161
162 return new DisplayAxisRotated($this, $axis, $axis_no, $orientation, $type,
163 $main, $this->arad);
164 }
165
166 /**
167 * Returns the location of an axis
168 */
169 protected function getAxisLocation($orientation, $axis_no)
170 {
171 return [$this->xc, $this->yc];
172 }
173
174 /**
175 * Convert X, Y in grid space to radial position
176 */
177 public function transformCoords($x, $y)
178 {
179 $angle = $x / $this->g_height;
180 if($this->getOption('grid_straight')) {
181 // this complicates things...
182
183 // get all the grid points, div and subdiv
184 $points = array_merge($this->getGridPointsX(0), $this->getSubDivsX(0));
185 $grid_angles = [];
186 foreach($points as $point) {
187 $grid_angles[] = $point->position / $this->radius;
188 }
189 sort($grid_angles);
190
191 // find angle between (sub)divisions
192 $div_angle = $grid_angles[1] - $grid_angles[0];
193
194 // use trig to find length of Y
195 $a = fmod($angle, $div_angle);
196 $t = ($div_angle / 2) - $a;
197
198 $y2 = $y * cos($div_angle / 2);
199 $y = $y2 / cos($t);
200 }
201 $angle += $this->arad;
202 $new_x = $this->xc + ($y * sin($angle));
203 $new_y = $this->yc + ($y * cos($angle));
204
205 return [$new_x, $new_y];
206 }
207
208 /**
209 * Find the bounding box of the axis text for given axis lengths
210 */
211 protected function findAxisBBox($length_x, $length_y, $x_axes, $y_axes)
212 {
213 $this->xc = $length_x / 2;
214 $this->yc = $length_y / 2;
215 $diameter = min($length_x, $length_y);
216 $length_y = $diameter / 2;
217 $length_x = 2 * M_PI * $length_y;
218 $this->radius = $length_y;
219 foreach($x_axes as $a)
220 $a->setLength($length_x);
221 foreach($y_axes as $a)
222 $a->setLength($length_y);
223
224 $bb = new BoundingBox($this->width, $this->height, 0, 0);
225
226 $display_axis = $this->getDisplayAxis($x_axes[0], 0, 'h', 'x');
227 $axis_m = $display_axis->measure();
228 $bb->grow($axis_m->x1, $axis_m->y1, $axis_m->x2, $axis_m->y2);
229
230 $display_axis = $this->getDisplayAxis($y_axes[0], 0, 'v', 'y');
231 $axis_m = $display_axis->measure();
232 $bb->grow($axis_m->x1 + $this->xc, $axis_m->y1 + $this->yc,
233 $axis_m->x2 + $this->xc, $axis_m->y2 + $this->yc);
234
235 $this->radius = null;
236 return [
237 'min_x' => $bb->x1, 'min_y' => $bb->y1,
238 'max_x' => $bb->x2, 'max_y' => $bb->y2
239 ];
240 }
241
242 /**
243 * Draws concentric Y grid lines
244 */
245 public function yGrid(&$y_points)
246 {
247 $path = new PathData;
248
249 if($this->getOption('grid_straight')) {
250 $grid_angles = [];
251 $points = array_merge($this->getGridPointsX(0), $this->getSubDivsX(0));
252 foreach($points as $point) {
253 $new_x = $point->position - $this->pad_left;
254 $grid_angles[] = $this->arad + $new_x / $this->radius;
255 }
256 // put the grid angles in order
257 sort($grid_angles);
258 foreach($y_points as $y) {
259 $y = $y->position;
260 $x1 = $this->xc + $y * sin($this->arad);
261 $y1 = $this->yc + $y * cos($this->arad);
262 $path->add('M', $x1, $y1, 'L');
263 foreach($grid_angles as $a) {
264 $x1 = $this->xc + $y * sin($a);
265 $y1 = $this->yc + $y * cos($a);
266 $path->add($x1, $y1);
267 }
268 $path->add('z');
269 }
270 } else {
271 $yc_num = new Number($this->yc);
272 foreach($y_points as $y) {
273 $y = $y->position;
274 $p1 = new Number($this->xc - $y);
275 $p2 = new Number($this->xc + $y);
276 $y = new Number($y);
277 $path->add('M', $p1, $yc_num, 'A', $y, $y, 0, 1, 1, $p2, $yc_num);
278 $path->add('M', $p2, $yc_num, 'A', $y, $y, 0, 1, 1, $p1, $yc_num);
279 }
280 }
281 return $path;
282 }
283
284 /**
285 * Draws radiating X grid lines
286 */
287 protected function xGrid(&$x_points)
288 {
289 $path = new PathData;
290 $xc_num = new Number($this->xc);
291 $yc_num = new Number($this->yc);
292 foreach($x_points as $x) {
293 $x = $x->position - $this->pad_left;
294 $angle = $this->arad + $x / $this->radius;
295 $p1 = $this->radius * sin($angle);
296 $p2 = $this->radius * cos($angle);
297 $path->add('M', $xc_num, $yc_num, 'l', $p1, $p2);
298 }
299 return $path;
300 }
301
302 /**
303 * Draws the grid behind the graph
304 */
305 protected function grid()
306 {
307 $this->calcAxes();
308 $this->calcGrid();
309 if(!$this->getOption('show_grid') ||
310 (!$this->getOption('show_grid_h') && !$this->getOption('show_grid_v')))
311 return '';
312
313 $back = $subpath = '';
314 $back_colour = new Colour($this, $this->getOption('grid_back_colour'),
315 true, true, true);
316 $y_points = $this->getGridPointsY(0);
317 $x_points = $this->getGridPointsX(0);
318 $y_subdivs = $this->getSubDivsY(0);
319 $x_subdivs = $this->getSubDivsX(0);
320 if(!$back_colour->isNone()) {
321 // use the YGrid function to get the path
322 $points = [new GridPoint($this->radius, '', 0)];
323 $bpath = [
324 'd' => $this->yGrid($points),
325 'fill' => $back_colour
326 ];
327 if($this->getOption('grid_back_opacity') != 1)
328 $bpath['fill-opacity'] = $this->getOption('grid_back_opacity');
329 $back = $this->element('path', $bpath);
330 }
331 if($this->getOption('grid_back_stripe')) {
332 // use array of colours if available, otherwise stripe a single colour
333 $colours = is_array($this->getOption('grid_back_stripe_colour')) ?
334 $this->getOption('grid_back_stripe_colour') :
335 [null, $this->getOption('grid_back_stripe_colour')];
336 $c = 0;
337 $num_colours = count($colours);
338 $num_points = count($y_points);
339 while($c < $num_points - 1) {
340 $cc = $colours[$c % $num_colours];
341 if($cc !== null) {
342 $s_points = [$y_points[$c], $y_points[$c + 1]];
343 $c1 = new Colour($this, $cc);
344 if($c1->isGradient())
345 $c1 = $this->remapGradient($cc, $s_points);
346
347 $bpath = [
348 'fill' => $c1,
349 'd' => $this->yGrid($s_points),
350 'fill-rule' => 'evenodd',
351 ];
352 if($this->getOption('grid_back_stripe_opacity') != 1)
353 $bpath['fill-opacity'] = $this->getOption('grid_back_stripe_opacity');
354 $back .= $this->element('path', $bpath);
355 }
356 ++$c;
357 }
358 }
359 if($this->getOption('show_grid_subdivisions')) {
360 $subpath_h = new PathData;
361 $subpath_v = new PathData;
362 if($this->getOption('show_grid_h'))
363 $subpath_h = $this->yGrid($y_subdivs);
364 if($this->getOption('show_grid_v'))
365 $subpath_v = $this->xGrid($x_subdivs);
366 if(!($subpath_h->isEmpty() && $subpath_v->isEmpty())) {
367 $colour_h = $this->getOption('grid_subdivision_colour_h',
368 'grid_subdivision_colour', 'grid_colour_h', 'grid_colour');
369 $colour_v = $this->getOption('grid_subdivision_colour_v',
370 'grid_subdivision_colour', 'grid_colour_v', 'grid_colour');
371 $dash_h = $this->getOption('grid_subdivision_dash_h',
372 'grid_subdivision_dash', 'grid_dash_h', 'grid_dash');
373 $dash_v = $this->getOption('grid_subdivision_dash_v',
374 'grid_subdivision_dash', 'grid_dash_v', 'grid_dash');
375 $width_h = $this->getOption('grid_subdivision_stroke_width_h',
376 'grid_subdivision_stroke_width', 'grid_stroke_width_h',
377 'grid_stroke_width');
378 $width_v = $this->getOption('grid_subdivision_stroke_width_v',
379 'grid_subdivision_stroke_width', 'grid_stroke_width_v',
380 'grid_stroke_width');
381
382 if($dash_h == $dash_v && $colour_h == $colour_v && $width_h == $width_v) {
383 $subpath_h->add($subpath_v);
384 $subpath = $this->gridLines($subpath_h, $colour_h, $dash_h, $width_h,
385 ['fill' => 'none']);
386 } else {
387 $subpath = $this->gridLines($subpath_h, $colour_h, $dash_h, $width_h,
388 ['fill' => 'none']) .
389 $this->gridLines($subpath_v, $colour_v, $dash_v, $width_v,
390 ['fill' => 'none']);
391 }
392 }
393 }
394
395 $path_v = new PathData;
396 $path_h = new PathData;
397 if($this->getOption('show_grid_h'))
398 $path_v = $this->yGrid($y_points);
399 if($this->getOption('show_grid_v'))
400 $path_h = $this->xGrid($x_points);
401
402 $colour_h = $this->getOption('grid_colour_h', 'grid_colour');
403 $colour_v = $this->getOption('grid_colour_v', 'grid_colour');
404 $dash_h = $this->getOption('grid_dash_h', 'grid_dash');
405 $dash_v = $this->getOption('grid_dash_v', 'grid_dash');
406 $width_h = $this->getOption('grid_stroke_width_h', 'grid_stroke_width');
407 $width_v = $this->getOption('grid_stroke_width_v', 'grid_stroke_width');
408
409 if($dash_h == $dash_v && $colour_h == $colour_v && $width_h == $width_v) {
410 $path_v->add($path_h);
411 $path = $this->gridLines($path_v, $colour_h, $dash_h, $width_h,
412 ['fill' => 'none']);
413 } else {
414 $path = $this->gridLines($path_h, $colour_h, $dash_h, $width_h,
415 ['fill' => 'none']) .
416 $this->gridLines($path_v, $colour_v, $dash_v, $width_v,
417 ['fill' => 'none']);
418 }
419
420 return $back . $subpath . $path;
421 }
422
423 /**
424 * Sets the grid size as circumference x radius
425 */
426 protected function setGridDimensions()
427 {
428 if($this->radius === null) {
429 $w = $this->width - $this->pad_left - $this->pad_right;
430 $h = $this->height - $this->pad_top - $this->pad_bottom;
431 $this->xc = $this->pad_left + $w / 2;
432 $this->yc = $this->pad_top + $h / 2;
433 $this->radius = min($w, $h) / 2;
434 }
435 $this->g_height = $this->radius;
436 $this->g_width = 2 * M_PI * $this->radius;
437 }
438
439 /**
440 * Calculate the extra details for radar axes
441 */
442 protected function calcAxes()
443 {
444 $this->arad = (90 + $this->getOption('start_angle')) * M_PI / 180;
445 $this->setOption('axis_right', false);
446 parent::calcAxes();
447 }
448
449 /**
450 * Returns the grid points for a Y-axis
451 */
452 protected function getGridPointsY($axis)
453 {
454 $points = $this->y_axes[$axis]->getGridPoints(0);
455 foreach($points as $k => $p)
456 $points[$k]->position = -$p->position;
457 return $points;
458 }
459
460 /**
461 * Returns the subdivisions for a Y-axis
462 */
463 protected function getSubDivsY($axis)
464 {
465 $points = $this->y_axes[$axis]->getGridSubdivisions(
466 $this->getOption('minimum_subdivision'),
467 $this->getOption(['minimum_units_y', $axis]), 0,
468 $this->getOption(['subdivision_v', $axis]));
469 foreach($points as $k => $p)
470 $points[$k]->position = -$p->position;
471 return $points;
472 }
473
474 /**
475 * Both of these functions need to exist, just call one from the other
476 */
477 public function guidelinePathAbove($axis, $value, &$x, &$y, &$w, &$h,
478 $reverse_length)
479 {
480 return $this->guidelinePathBelow($axis, $value, $x, $y, $w, $h, $reverse_length);
481 }
482
483 /**
484 * Draws the path for a guideline
485 */
486 public function guidelinePathBelow($axis, $value, &$x, &$y, &$w, &$h,
487 $reverse_length)
488 {
489 $coords = new Coords($this);
490 $path = new PathData;
491
492 $grid_value = 'g' . (is_numeric($value) ? new Number($value) : $value);
493 if($axis == 'x') {
494
495 // radial line
496 $x = $coords->transform($grid_value, 'x', null);
497 if($x === null)
498 return $path;
499
500 if(is_string($h) || $h > 0) {
501 $h = $coords->transform($h, 'y');
502 } else {
503 $h = $this->radius;
504 }
505
506 if($reverse_length) {
507 list($p1x, $p1y) = $this->transformCoords($x, $this->radius - $h);
508 $h = $this->radius;
509 } else {
510 $p1x = $this->xc;
511 $p1y = $this->yc;
512 }
513
514 list($p2x, $p2y) = $this->transformCoords($x, $h);
515 $path->add('M', $p1x, $p1y, 'L', $p2x, $p2y);
516
517 // text position values - not very useful though
518 $x = min($p1x, $p2x);
519 $y = min($p1y, $p2y);
520 $w = abs($p2x - $p1x);
521 $h = abs($p2y - $p1y);
522
523 } else {
524 $x = $coords->transform('gl', 'x');
525 $y = $coords->transform($grid_value, 'y', null);
526 if($y === null)
527 return $path;
528
529 if(is_string($w) || $w > 0) {
530 $w = $coords->transform($w, 'x');
531 } else {
532 $w = $coords->transform('gw', 'x');
533 }
534 if($reverse_length)
535 $x = $coords->transform('gr', 'x') - $w;
536 $h = 0;
537
538 // round or segmented line
539 if($this->getOption('grid_straight')) {
540 $grid_angles = [];
541 $points = array_merge($this->getGridPointsX(0), $this->getSubDivsX(0));
542 foreach($points as $point) {
543 $new_x = $point->position - $this->pad_left;
544 $grid_angles[] = $this->arad + $new_x / $this->radius;
545 }
546 // put the grid angles in order
547 sort($grid_angles);
548 $x1 = $this->xc + $y * sin($this->arad);
549 $y1 = $this->yc + $y * cos($this->arad);
550 $path->add('M', $x1, $y1, 'L');
551 foreach($grid_angles as $a) {
552 $x1 = $this->xc + $y * sin($a);
553 $y1 = $this->yc + $y * cos($a);
554 $path->add($x1, $y1);
555 }
556 $path->add('z');
557
558 } else {
559 $yc_num = new Number($this->yc);
560 $p1 = new Number($this->xc - $y);
561 $p2 = new Number($this->xc + $y);
562 $path->add('M', $p1, $yc_num, 'A', $y, $y, 0, 1, 1, $p2, $yc_num);
563 $path->add('M', $p2, $yc_num, 'A', $y, $y, 0, 1, 1, $p1, $yc_num);
564 }
565
566 // update values for text position
567 $x = $this->xc;
568 $y = $this->yc - $y;
569 $w = 0;
570 $h = 0;
571 }
572
573 return $path;
574 }
575
576 /**
577 * Remaps gradient to make it start at the right radius
578 */
579 protected function remapGradient($g, $points)
580 {
581 $gl = new GradientList($this);
582 $d = $gl->decompose($g);
583
584 $p1 = $points[0]->position;
585 $p2 = $points[1]->position;
586 $offset = 100 * $p1 / $p2;
587 $mult = (100 - $offset) / 100;
588
589 $remapped = [ rtrim(implode(':', $d[0]), ':') ];
590 foreach($d as $stop) {
591 $stop[0] = $offset + $stop[0] * $mult;
592 $remapped[] = rtrim(implode(':', $stop), ':');
593 }
594
595 return new Colour($this, $remapped, true, true, true);
596 }
597 }
598
599