PluginProbe
E2Pdf – Export Pdf Tool for WordPress / 1.32.23
E2Pdf – Export Pdf Tool for WordPress v1.32.23
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 / Axis.php

Axis.php in E2Pdf – Export Pdf Tool for WordPress 1.32.23, at vendors/svggraph/Axis.php

643 lines 16.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) 2011-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 * Class for calculating axis measurements
26 */
27 class Axis {
28
29 protected $length;
30 protected $max_value;
31 protected $min_value;
32 protected $unit_size;
33 protected $min_unit;
34 protected $min_space;
35 protected $fit;
36 protected $zero;
37 protected $units_before;
38 protected $units_after;
39 protected $decimal_digits;
40 protected $uneven = false;
41 protected $rounded_up = false;
42 protected $direction = 1;
43 protected $label_callback = false;
44 protected $values = false;
45 protected $tightness = 1;
46 protected $grid_spacing;
47
48 public function __construct($length, $max_val, $min_val, $min_unit, $min_space,
49 $fit, $units_before, $units_after, $decimal_digits, $label_callback, $values)
50 {
51 if($max_val <= $min_val && $min_unit == 0)
52 throw new \Exception('Zero length axis (min >= max)');
53 $this->length = $length;
54 $this->max_value = $max_val;
55 $this->min_value = $min_val;
56 $this->min_unit = $min_unit;
57 $this->min_space = $min_space;
58 $this->fit = $fit;
59 $this->units_before = $units_before;
60 $this->units_after = $units_after;
61 $this->decimal_digits = $decimal_digits;
62 $this->label_callback = $label_callback;
63 $this->values = $values;
64 }
65
66 /**
67 * Returns min for an axis based on its min and max values
68 */
69 public static function calcMinimum($min_value, $max_value, $allow_zero,
70 $prefer_zero)
71 {
72 if($allow_zero && $prefer_zero) {
73 if($min_value > 0)
74 return 0;
75 if($max_value < 0)
76 $max_value = 0;
77 }
78
79 if($min_value > 0) {
80 $mag = floor(log10($min_value));
81 if($allow_zero) {
82 $mag1 = floor(log10($max_value));
83 if($mag1 > $mag)
84 return 0;
85 }
86 $d = pow(10, $mag);
87 $min_value = floor($min_value / $d) * $d;
88 }
89 return $min_value;
90 }
91
92 /**
93 * Returns max for an axis based on its min and max values
94 */
95 public static function calcMaximum($min_value, $max_value, $allow_zero,
96 $prefer_zero)
97 {
98 if($max_value >= 0)
99 return $max_value;
100
101 // instead of duplicating code, negate values and pass to calcMinimum()
102 $neg_max = Axis::calcMinimum(-$max_value, -$min_value, $allow_zero,
103 $prefer_zero);
104 if($neg_max > 0)
105 return -$neg_max;
106 return 0;
107 }
108
109 /**
110 * Allow length adjustment
111 */
112 public function setLength($l)
113 {
114 $this->length = $l;
115 }
116
117 /**
118 * Returns the axis length
119 */
120 public function getLength()
121 {
122 return $this->length;
123 }
124
125 /**
126 * Sets the tightness option
127 */
128 public function setTightness($t)
129 {
130 $this->tightness = $t;
131 }
132
133 /**
134 * Returns a score for "niceness"
135 */
136 private function nice($n)
137 {
138 if($this->min_unit) {
139 $d = $n / $this->min_unit;
140 if($d != floor($d))
141 return 0;
142 }
143
144 // convert to string
145 $nn = new Number($n);
146 $nn->precision = 5;
147 $s = (string)$nn;
148
149 $niceness = [
150 '0.1' => 50,
151 '0.5' => 40,
152 '0.2' => 25,
153 '2.5' => 25,
154 '1.5' => 20,
155 '0.3' => 10,
156 '0.4' => 10,
157 '1' => 100,
158 '5' => 95,
159 '2' => 95,
160 '3' => 45,
161 '4' => 40,
162 '6' => 30,
163 '8' => 20,
164 '7' => 10,
165 '9' => 5,
166 '25' => 95,
167 '15' => 40,
168 '75' => 30,
169 ];
170
171 $digits = $s;
172 if(preg_match('/^([1-9]{1,2})(0*)$/', $s, $parts)) {
173 // integer with one or two non-zero digit
174 $digits = $parts[1];
175 } elseif(preg_match('/^0\.(0+)([1-9]{1,2})$/', $s, $parts)) {
176 // float with leading zeroes
177 $digits = $parts[2];
178 }
179
180 return isset($niceness[$digits]) ? $niceness[$digits] : 0;
181 }
182
183 /**
184 * Determine the axis divisions
185 */
186 private function findDivision($length, $min, &$count, &$neg_count, &$magnitude)
187 {
188 if($this->tightness && $length / $count >= $min) {
189 return;
190 }
191
192 $c = $count - 1;
193 $inc = 0;
194
195 // $max_inc is how many extra steps the axis can grow by
196 if($this->fit)
197 $max_inc = 0;
198 else
199 $max_inc = $count / ($this->tightness ? 5 : 2);
200
201 $candidates = [];
202 while($c > 1) {
203 $m = ($count + $inc) / $c;
204 $new_magnitude = $m * $magnitude;
205 $l = $length / $c;
206 $nc = $neg_count;
207
208 $accept = false;
209 $niceness = $this->nice($new_magnitude);
210 if($niceness > 0 && $l >= $min) {
211 $accept = true;
212
213 // negative values mean an extra check
214 if($nc) {
215 $accept = false;
216 $nm = $nc / $m;
217
218 if(floor($nm) === $nm) {
219 $nc = $nm;
220 $accept = true;
221 } else {
222
223 // negative section doesn't divide cleanly, try adding from $inc
224 if($inc) {
225 for($i = 1; $i <= $inc; ++$i) {
226 $cc = $nc + $i;
227 $nm = ($nc + $i) / $m;
228
229 if(floor($nm) === $nm) {
230 $nc = $nm;
231 $accept = true;
232 break;
233 }
234 }
235 }
236 }
237 }
238 }
239
240 if($accept) {
241 $pos = ($c - $nc) * $new_magnitude;
242 $neg = $nc * $new_magnitude;
243 $pos_niceness = $this->nice($pos);
244 $neg_niceness = $this->nice($neg);
245
246 if($this->tightness || $neg_niceness || $pos_niceness) {
247 // this division is acceptable, cost and store it
248 $cost = $m;
249 if($this->tightness) {
250 $cost += $inc * 1.5;
251 } else {
252 // increasing the length is not as costly
253 $cost += $inc * 0.5;
254
255 // reduce cost for nicer divisions
256 $cost -= $niceness / 50;
257
258 // adjust cost for axis ends
259 if($nc) {
260 if($neg_niceness) {
261 $cost -= $neg_niceness / 100;
262 if($pos_niceness)
263 $cost -= $pos_niceness / 100;
264 } else {
265 // poor choice
266 $cost += 3;
267 }
268 } elseif($pos_niceness) {
269 $cost -= $pos_niceness / 100;
270 }
271 }
272
273 $candidate = [
274 // usort requires ints to work properly
275 'cost' => intval(1e5 * $cost),
276 'magnitude' => $new_magnitude,
277 'count' => $c,
278 'neg_count' => $nc,
279
280 // these are only used for tuning / debugging
281 'm' => $m,
282 'real_cost' => $cost,
283 'max_pos' => $pos,
284 'max_neg' => $neg,
285 'nice_mag' => $niceness,
286 'nice_pos' => $pos_niceness,
287 'nice_neg' => $neg_niceness,
288 ];
289
290 $candidates[] = $candidate;
291 }
292 }
293
294 if($inc < $max_inc) {
295 // increase the number of base divisions
296 ++$inc;
297 continue;
298 }
299
300 --$c;
301 $inc = 0;
302 }
303
304 if(empty($candidates))
305 return;
306
307 usort($candidates, function($a, $b) { return $a['cost'] - $b['cost']; });
308 $winner = $candidates[0];
309 $magnitude = $winner['magnitude'];
310 $count = $winner['count'];
311 $neg_count = $winner['neg_count'];
312 }
313
314 /**
315 * Sets the bar style (which means an extra unit)
316 */
317 public function bar()
318 {
319 if(!$this->rounded_up) {
320 $this->max_value += $this->min_unit;
321 $this->rounded_up = true;
322 }
323 }
324
325 /**
326 * Sets the direction of axis points
327 */
328 public function reverse()
329 {
330 $this->direction = -1;
331 }
332
333 /**
334 * Returns TRUE if the axis is reversed
335 */
336 public function reversed()
337 {
338 return $this->direction < 0;
339 }
340
341 /**
342 * Returns the grid spacing
343 */
344 protected function grid()
345 {
346 $min_space = $this->min_space;
347 $this->uneven = false;
348 $negative = $this->min_value < 0;
349 $min_sub = max($min_space, $this->length / 200);
350
351 if($this->min_value == $this->max_value)
352 $this->max_value += $this->min_unit;
353 $scale = $this->max_value - $this->min_value;
354
355 $abs_min = abs($this->min_value);
356 $magnitude = max(pow(10, floor(log10($scale))), $this->min_unit);
357 if($this->min_value > 0 || $this->fit) {
358 $count = ceil($scale / $magnitude);
359 } else {
360 $count = ceil($this->max_value / $magnitude) -
361 floor($this->min_value / $magnitude);
362 }
363
364 if($count <= 5 && $magnitude > $this->min_unit) {
365 $magnitude *= 0.1;
366 $count = ceil($this->max_value / $magnitude) -
367 floor($this->min_value / $magnitude);
368 }
369
370 $neg_count = $this->min_value < 0 ? ceil($abs_min / $magnitude) : 0;
371 $this->findDivision($this->length, $min_sub, $count, $neg_count,
372 $magnitude);
373 $grid = $this->length / $count;
374
375 // guard this loop in case the numbers are too awkward to fit
376 $guard = 10;
377 while($grid < $min_space && --$guard) {
378 $this->findDivision($this->length, $min_sub, $count, $neg_count,
379 $magnitude);
380 $grid = $this->length / $count;
381 }
382 if($guard == 0) {
383 // could not find a division
384 while($grid < $min_space && $count > 1) {
385 $count *= 0.5;
386 $neg_count *= 0.5;
387 $magnitude *= 2;
388 $grid = $this->length / $count;
389 $this->uneven = true;
390 }
391 }
392
393 $this->unit_size = $this->length / ($magnitude * $count);
394 $this->zero = $negative ? $neg_count * $grid :
395 -$this->min_value * $grid / $magnitude;
396
397 return $grid;
398 }
399
400 /**
401 * Returns the size of a unit in grid space
402 */
403 public function unit()
404 {
405 if(!isset($this->unit_size))
406 $this->grid();
407
408 return $this->unit_size;
409 }
410
411 /**
412 * Returns the distance along the axis where 0 should be
413 */
414 public function zero()
415 {
416 if(!isset($this->zero))
417 $this->grid();
418
419 return $this->zero;
420 }
421
422 /**
423 * Returns TRUE if the grid spacing does not fill the grid
424 */
425 public function uneven()
426 {
427 return $this->uneven;
428 }
429
430 /**
431 * Returns the distance in pixels $u takes from $pos
432 */
433 public function measureUnits($pos, $u)
434 {
435 // on an ordinary axis this works fine
436 $l = $this->position($u);
437 $zero = $this->position(0);
438 return $l - $zero;
439 }
440
441 /**
442 * Returns the position of a value on the axis
443 */
444 public function position($index, $item = null)
445 {
446 $value = $index;
447 if($item !== null && !$this->values->associativeKeys())
448 $value = $item->key;
449 if(!is_numeric($value))
450 return null;
451 return $this->zero() + ($value * $this->unit());
452 }
453
454 /**
455 * Returns the position of an associative key, if possible
456 */
457 public function positionByKey($key)
458 {
459 if($this->values && $this->values->associativeKeys()) {
460
461 // only need to look through dataset 0 because multi-dataset graphs
462 // convert to structured
463 $index = 0;
464 foreach($this->values[0] as $item) {
465 if($item->key == $key) {
466 return $this->zero() + ($index * $this->unit());
467 }
468 ++$index;
469 }
470 }
471 return null;
472 }
473
474 /**
475 * Returns the position of the origin
476 */
477 public function origin()
478 {
479 // for a linear axis, it should be the zero point
480 return $this->zero();
481 }
482
483 /**
484 * Returns the value at a position on the axis
485 */
486 public function value($position)
487 {
488 return ($position - $this->zero()) / $this->unit();
489 }
490
491 /**
492 * Return the before units text
493 */
494 public function beforeUnits()
495 {
496 return $this->units_before;
497 }
498
499 /**
500 * Return the after units text
501 */
502 public function afterUnits()
503 {
504 return $this->units_after;
505 }
506
507 /**
508 * Returns a single GridPoint
509 */
510 protected function getGridPoint($position, $value)
511 {
512 $key = $text = $value;
513 $item = null;
514
515 if($this->values) {
516
517 // try structured data first
518 $item = $this->values->getItem($value);
519 if($item !== null && $this->values->getData($value, 'axis_text', $text))
520 return new GridPoint($position, $text, $value, $item);
521
522 // use the key if it is not the same as the value
523 $key = $this->values->getKey($value);
524 }
525
526 // if there is a callback, use it
527 if(is_callable($this->label_callback)) {
528 // assoc keys should have integer indices
529 if($this->values && $this->values->associativeKeys())
530 $value = (int)round($value);
531 $text = call_user_func($this->label_callback, $value, $key);
532 return new GridPoint($position, $text, $value, $item);
533 }
534
535 if($key !== $value)
536 return new GridPoint($position, $key, $value, $item);
537
538 $n = new Number($value, $this->units_after, $this->units_before);
539 $text = $n->format($this->decimal_digits);
540 return new GridPoint($position, $text, $value, $item);
541 }
542
543 /**
544 * Returns the grid points as an array of GridPoints
545 * if $start is NULL, just set up the grid spacing without returning points
546 */
547 public function getGridPoints($start)
548 {
549 $this->grid_spacing = $spacing = $this->grid();
550 $dlength = $this->length + $spacing * 0.5;
551 if($dlength / $spacing > 10000) {
552 $pcount = $dlength / $spacing;
553 throw new \Exception('Too many grid points (' . $this->min_value . '->' .
554 $this->max_value . ' = ' . $pcount . ' points @ ' . $spacing . 'px separation)');
555 }
556 if($start === null)
557 return;
558
559 $c = $pos = 0;
560 $points = [];
561 while($pos < $dlength) {
562 $value = ($pos - $this->zero) / $this->unit_size;
563 $position = $start + ($this->direction * $pos);
564 $points[] = $this->getGridPoint($position, $value);
565 $pos = ++$c * $spacing;
566 }
567 // uneven means the divisions don't fit exactly, so add the last one in
568 if($this->uneven) {
569 $pos = $this->length - $this->zero;
570 $value = $pos / $this->unit_size;
571 $position = $start + ($this->direction * $this->length);
572 $points[] = $this->getGridPoint($position, $value);
573 }
574
575 if($this->direction < 0) {
576 usort($points, function($a, $b) { return $b->position - $a->position; });
577 } else {
578 usort($points, function($a, $b) { return $a->position - $b->position; });
579 }
580
581 $this->grid_spacing = $spacing;
582 return $points;
583 }
584
585 /**
586 * Returns the grid subdivision points as an array
587 */
588 public function getGridSubdivisions($min_space, $min_unit, $start, $fixed)
589 {
590 if(!$this->grid_spacing)
591 throw new \Exception('grid_spacing not set');
592
593 $subdivs = [];
594 $spacing = $this->findSubdiv($this->grid_spacing, $min_space, $min_unit,
595 $fixed);
596 if(!$spacing)
597 return $subdivs;
598
599 $c = $pos1 = $pos2 = 0;
600 $pos1 = $c * $this->grid_spacing;
601 while($pos1 + $spacing < $this->length) {
602 $d = 1;
603 $pos2 = $d * $spacing;
604 while($pos2 < $this->grid_spacing) {
605 $subdivs[] = new GridPoint($start + (($pos1 + $pos2) * $this->direction), '', 0);
606 ++$d;
607 $pos2 = $d * $spacing;
608 }
609 ++$c;
610 $pos1 = $c * $this->grid_spacing;
611 }
612 return $subdivs;
613 }
614
615 /**
616 * Find the subdivision size
617 */
618 private function findSubdiv($grid_div, $min, $min_unit, $fixed)
619 {
620 if(is_numeric($fixed))
621 return $this->unit_size * $fixed;
622
623 $D = $grid_div / $this->unit_size; // D = actual division size
624 $min = max($min, $min_unit * $this->unit_size); // use the larger minimum value
625 $max_divisions = (int)floor($grid_div / $min);
626
627 // can we subdivide at all?
628 if($max_divisions <= 1)
629 return null;
630
631 // convert $D to an integer in the 100's range
632 $D1 = (int)round(100 * (pow(10,-floor(log10($D)))) * $D);
633 for($divisions = $max_divisions; $divisions > 1; --$divisions) {
634 // if $D1 / $divisions is not an integer, $divisions is no good
635 $dq = $D1 / $divisions;
636 if($dq - floor($dq) == 0)
637 return $grid_div / $divisions;
638 }
639 return null;
640 }
641 }
642
643