PluginProbe
E2Pdf – Export Pdf Tool for WordPress / 1.32.17
E2Pdf – Export Pdf Tool for WordPress v1.32.17
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 / AxisDateTime.php

AxisDateTime.php in E2Pdf – Export Pdf Tool for WordPress 1.32.17, at vendors/svggraph/AxisDateTime.php

725 lines 21.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) 2016-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 date/time axis measurements
26 */
27 class AxisDateTime extends Axis {
28
29 protected $grid_space;
30 protected $grid_split = 0;
31 protected $start = 0;
32 protected $end = 0;
33 protected $duration = 1;
34 protected $grid_units;
35 protected $grid_unit_count;
36 protected $label_callback;
37 protected $axis_text_format = 'Y-m-d';
38 protected $timezone = null;
39 protected $formatter = null;
40 protected $div = null;
41 protected $division = null;
42 protected $levels = null;
43
44 protected static $week_start = 'monday';
45 protected static $weekdays = [
46 'sunday' => 0,
47 'monday' => 1,
48 'tuesday' => 2,
49 'wednesday' => 3,
50 'thursday' => 4,
51 'friday' => 5,
52 'saturday' => 6
53 ];
54
55 /**
56 * The list of possible divisions. Fields are:
57 * 0 - division unit
58 * 1 - number of units in duration
59 * 2 - array of division indices for subdivision
60 */
61 protected static $divisions = [
62 // the indices are numbered for clarity
63 0 => ['second', 1],
64 1 => ['second', 2, [0]],
65 2 => ['second', 5, [0]],
66 3 => ['second', 10, [0, 1, 2]],
67 4 => ['second', 15, [0, 2]],
68 5 => ['second', 20, [0, 1, 2, 3]],
69 6 => ['second', 30, [0, 1, 2, 3, 4]],
70 7 => ['minute', 1, [3, 4, 5, 6]],
71 8 => ['minute', 2, [6, 7]],
72 9 => ['minute', 5, [7]],
73 10 => ['minute', 10, [7, 8, 9]],
74 11 => ['minute', 15, [7, 9]],
75 12 => ['minute', 20, [7, 8, 9, 10]],
76 13 => ['minute', 30, [8, 9, 10, 11]],
77 14 => ['hour', 1, [9, 10, 11, 12, 13]],
78 15 => ['hour', 2, [11, 13, 14]],
79 16 => ['hour', 3, [13, 14]],
80 17 => ['hour', 4, [13, 14, 15]],
81 18 => ['hour', 6, [14, 15, 16]],
82 19 => ['hour', 8, [14, 15, 17]],
83 20 => ['hour', 12, [14, 15, 16, 17, 18, 19]],
84 21 => ['day', 1, [14, 18, 20]],
85 22 => ['day', 7, [21]],
86 23 => ['day', 14, [21, 22]],
87 24 => ['month', 1, [21]],
88 25 => ['month', 2, [21, 24]],
89 26 => ['month', 3, [24]],
90 27 => ['month', 6, [24, 25, 26]],
91 28 => ['year', 1, [24, 25, 26, 27]],
92 29 => ['year', 2, [27, 28]],
93 30 => ['year', 5, [28]],
94 31 => ['year', 10, [28, 29, 30]],
95 32 => ['year', 20, [28, 29, 30, 31]],
96 33 => ['year', 50, [30, 31]],
97 34 => ['year', 100, [31, 32, 33]],
98 35 => ['year', 500, [34]],
99 36 => ['year', 1000, [34, 35]],
100 37 => ['year', 10000],
101 38 => ['year', 100000],
102 39 => ['year', 1000000],
103 ];
104
105 /**
106 * The size of each unit in seconds
107 */
108 protected static $unit_sizes = [
109 'second' => 1,
110 'minute' => 60,
111 'hour' => 3600,
112 'day' => 86400,
113 'month' => 2629800, // avg year / 12
114 'year' => 31557600 // avg year = 365.25 days (ignoring leap centuries)
115 ];
116
117 /**
118 * Default format strings for each unit size
119 */
120 protected static $formats = [
121 'second' => 'Y-m-d H:i:s',
122 'minute' => 'Y-m-d H:i',
123 'hour' => 'Y-m-d H:i',
124 'day' => 'Y-m-d',
125 'month' => 'Y-m',
126 'year' => 'Y'
127 ];
128
129 /**
130 * Multi-level format strings
131 */
132 protected static $formats_level = [
133 'second' => ['H:i:s', 'd', 'F', 'Y'],
134 'minute' => ['H:i', 'd', 'F', 'Y'],
135 'hour' => ['H:i', 'd', 'F', 'Y'],
136 'day' => ['d', 'M', 'Y'],
137 'month' => ['M', 'Y'],
138 'year' => ['Y'],
139 ];
140
141 public function __construct($length, $max_val, $min_val, $min_space,
142 $fixed_division, $levels, $options)
143 {
144 if($max_val < $min_val)
145 throw new \Exception('Zero length axis (min >= max)');
146 $this->length = $length;
147 // if $min_space > $length, use $length instead
148 $this->min_space = $min_space = min($length, $min_space);
149 $this->uneven = false;
150
151 // convert actual min/max to start/end times
152 $start_date = new \DateTime('@' . $min_val);
153 $end_date = new \DateTime('@' . $max_val);
154 $this->timezone = new \DateTimeZone(date_default_timezone_get());
155 $start_date->setTimezone($this->timezone);
156 $end_date->setTimezone($this->timezone);
157 $this->formatter = new DateTimeFormatter;
158
159 // set the week start day before finding divisions
160 if(isset($options['datetime_week_start']) &&
161 isset(AxisDateTime::$weekdays[$options['datetime_week_start']]))
162 AxisDateTime::$week_start = $options['datetime_week_start'];
163
164 if(!empty($fixed_division)) {
165 list($units, $count) = AxisDateTime::parseFixedDivisions($fixed_division,
166 $min_val, $max_val, $length);
167 $start = AxisDateTime::startTime($start_date, $units, $count);
168 $end = AxisDateTime::endTime($end_date, $units, $count, $start);
169
170 $this->start = $start->format('U');
171 $this->end = $end->format('U');
172 $this->duration = ($this->end - $this->start) + 1;
173 $this->grid_units = $units;
174 $this->grid_unit_count = $count;
175
176 // set the division number (if it is a standard division)
177 $this->division = 0;
178 foreach(AxisDateTime::$divisions as $key => $div) {
179 if($div[0] == $units && $div[1] == $count)
180 $this->division = $key;
181 }
182
183 } else {
184
185 // find a sensible division
186 $div = AxisDateTime::findBestDivision($start_date, $end_date, $length,
187 $min_space);
188 $this->div = $div;
189 $this->start = $div[0]->format('U');
190 $this->end = $div[1]->format('U');
191 $this->duration = ($this->end - $this->start) + 1;
192
193 $this->division = $div[2];
194 $this->grid_units = AxisDateTime::$divisions[$this->division][0];
195 $this->grid_unit_count = AxisDateTime::$divisions[$this->division][1];
196 }
197 $this->label_callback = [$this, 'dateText'];
198
199 // get the axis text format from the options, or use defaults
200 $this->axis_text_format = AxisDateTime::$formats[$this->grid_units];
201 if(is_numeric($levels) && $levels > 1) {
202 $this->levels = (int)$levels;
203 $this->axis_text_format = AxisDateTime::$formats_level[$this->grid_units];
204 }
205
206 $text_format = null;
207 if(isset($options['datetime_text_format'])) {
208 $fmt = $options['datetime_text_format'];
209 if(is_array($fmt) && isset($fmt[$this->grid_units])) {
210 $text_format = $fmt[$this->grid_units];
211 } elseif(!empty($fmt)) {
212 $text_format = $fmt;
213 }
214 }
215
216 if($text_format !== null)
217 $this->axis_text_format = $text_format;
218 }
219
220 /**
221 * Finds the best division for the given start and end date/time
222 * @param DateTime $start
223 * @param DateTime $end
224 * @param number $length
225 * @param number $min_space
226 * @param number $subdivision
227 * Returns array($start, $end, $div_index, $div_count) or NULL if there is no
228 * subdivision possible
229 */
230 private static function findBestDivision($start, $end, $length, $min_space,
231 $subdivision = false)
232 {
233 $max_divisions = floor($length / $min_space);
234 $duration_s = $end->format('U') - $start->format('U');
235 $avg_duration = ceil($duration_s / $max_divisions);
236
237 $choice = null;
238 $divisions = 1;
239 $subdivide = false;
240 if($subdivision === false) {
241 $d_list = array_keys(AxisDateTime::$divisions);
242 } else {
243 // give up now if this can't be subdivided
244 if(!isset(AxisDateTime::$divisions[$subdivision][2]))
245 return null;
246 $d_list = AxisDateTime::$divisions[$subdivision][2];
247 $subdivide = true;
248 }
249
250 foreach($d_list as $i) {
251 $d = AxisDateTime::$divisions[$i];
252 $div_duration = $d[1] * AxisDateTime::$unit_sizes[$d[0]];
253
254 if($div_duration >= $avg_duration) {
255 $divisions = floor($duration_s / $div_duration);
256 $unit = $d[0];
257 $nunits = $d[1];
258
259 // get the updated start and end times to fit with the spacing
260 $new_start = AxisDateTime::startTime($start, $unit, $nunits);
261 $new_end = AxisDateTime::endTime($end, $unit, $nunits, $new_start);
262 $new_duration = $new_end->format('U') - $new_start->format('U');
263 $new_avg_duration = (int)ceil($new_duration / $max_divisions);
264
265 if($div_duration >= $new_avg_duration) {
266 $choice = $d;
267 break;
268 }
269 }
270 }
271 if($choice === null) {
272 if($subdivide)
273 return null;
274 throw new \Exception('Unable to find divisions for DateTime axis');
275 }
276
277 return [$new_start, $new_end, $i, $divisions];
278 }
279
280 /**
281 * Returns the start of the current $n $units of $time
282 */
283 private static function startTime($time, $unit, $n)
284 {
285 $formats = [
286 'year' => '00:00:00 January 1',
287 'month' => '00:00:00 first day of',
288 'day' => '00:00:00',
289 ];
290 $datetime = clone $time;
291 if($n == 1 && isset($formats[$unit])) {
292 $datetime->modify($formats[$unit]);
293
294 } else {
295 switch($unit) {
296 case 'year':
297 $y = $time->format('Y');
298 $y -= $y % $n;
299 $datetime->setDate($y, 1, 1);
300 $datetime->setTime(0, 0);
301 break;
302
303 case 'month':
304 $y = $time->format('Y');
305 $m = $time->format('n') - 1;
306 $m -= $m % $n;
307 $datetime->setDate($y, $m + 1, 1);
308 $datetime->setTime(0, 0);
309 break;
310
311 case 'day':
312 $day = $datetime->format('w'); // 0-6, Sun-Sat
313 $dow = AxisDateTime::$weekdays[AxisDateTime::$week_start];
314
315 // always start on the right weekday
316 if($day == $dow) {
317 $datetime->modify('00:00:00');
318 } else {
319 $datetime->modify('00:00:00 last ' . AxisDateTime::$week_start);
320 }
321 break;
322
323 case 'hour':
324 $h = $datetime->format('H');
325 if($n > 1)
326 $h = $h - ($h % $n);
327 $newtime = sprintf('%02d:00:00', $h);
328 $datetime->modify($newtime);
329 break;
330
331 case 'minute':
332 $m = $datetime->format('i');
333 if($n > 1)
334 $m = $m - ($m % $n);
335 $newtime = $datetime->format(sprintf('H:%02d:00', $m));
336 $datetime->modify($newtime);
337 break;
338
339 case 'second':
340 $s = $datetime->format('s');
341 if($n > 1)
342 $s = $s - ($s % $n);
343 $newtime = $datetime->format(sprintf('H:i:%02d', $s));
344 $datetime->modify($newtime);
345 break;
346 }
347 }
348 return $datetime;
349 }
350
351 /**
352 * Returns the end of the current $n $units of $time, started at $start
353 */
354 private static function endTime($time, $unit, $n, $start)
355 {
356 $formats = [
357 'year' => '23:59:59 December 31',
358 'month' => '23:59:59 last day of',
359 'day' => '23:59:59',
360 ];
361 $datetime = clone $time;
362 if($n == 1 && isset($formats[$unit])) {
363 $datetime->modify($formats[$unit]);
364
365 } else {
366 switch($unit) {
367 case 'year':
368 $y = $time->format('Y');
369 $new_y = new Number($y - ($y % $n) + $n - 1);
370 $datetime->modify($new_y . '-12-31 23:59:59');
371 break;
372
373 case 'month':
374 $datetime->modify('00:00:00 first day of');
375 $diff = $datetime->diff($start);
376 $months = ($diff->y * 12) + $diff->m;
377 $new_months = new Number($months - ($months % $n) + $n - 1);
378 $datetime = clone $start;
379 $datetime->modify('+' . $new_months . ' month 23:59:59 last day of');
380 break;
381
382 case 'day':
383 $datetime->modify('00:00:00');
384 $diff = $datetime->diff($start);
385 $days = new Number($diff->days - ($diff->days % $n) + $n - 1);
386 $datetime = clone $start;
387 $datetime->modify('+' . $days . ' day 23:59:59');
388 break;
389
390 case 'hour':
391 if($n > 1) {
392 $diff = $datetime->diff($start);
393 $hours = ($diff->days * 24) + $diff->h;
394 $hours = new Number($hours - ($hours % $n) + $n - 1);
395 $datetime = clone $start;
396 $datetime->modify('+' . $hours . ' hour 59 minute 59 second');
397 } else {
398 $h = $datetime->format('H');
399 $newtime = sprintf('%02d:59:59', $h);
400 $datetime->modify($newtime);
401 }
402 break;
403
404 case 'minute':
405 if($n > 1) {
406 $diff = $datetime->diff($start);
407 $minutes = ((($diff->days * 24) + $diff->h) * 60) + $diff->i;
408 $minutes = new Number($minutes - ($minutes % $n) + $n - 1);
409 $datetime = clone $start;
410 $datetime->modify('+' . $minutes . ' minute 59 second');
411 } else {
412 $m = $datetime->format('i');
413 $newtime = $datetime->format(sprintf('H:%02d:59', $m));
414 $datetime->modify($newtime);
415 }
416 break;
417
418 case 'second':
419 if($n > 1) {
420 $diff = $datetime->diff($start);
421 $seconds = ($diff->days * 86400) + ($diff->h * 3600) +
422 ($diff->i * 60) + $diff->s;
423 $seconds = new Number($seconds - ($seconds % $n) + $n - 1);
424 $datetime = clone $start;
425 $datetime->modify('+' . $seconds . ' second');
426 }
427 // if $n == 1, no modifications are required
428 break;
429 }
430 }
431 return $datetime;
432 }
433
434 /**
435 * Returns the distance in pixels $u takes from $pos
436 */
437 public function measureUnits($pos, $u)
438 {
439 $i = Coords::parseValue($pos);
440
441 // start with a plain date
442 $datetime = new \DateTime('@0');
443 $datetime->setTimezone($this->timezone);
444 if($i['simple']) {
445 $a = new Number($pos);
446 $datetime = new \DateTime('@' . $a);
447 $datetime->setTimezone($this->timezone);
448 } elseif($i['grid']) {
449 if($i['units']) {
450 list($units, $unit_count) = AxisDateTime::parseFixedDivisions($i['value'],
451 $this->start, $this->end, $this->length);
452 $datetime->setTimezone($this->timezone);
453 $uc = new Number($unit_count);
454 $datetime->modify('+' . $uc . ' ' . $units);
455 } else {
456 $v = Graph::dateConvert($i['value']);
457 $a = new Number($v);
458 $datetime = new \DateTime('@' . $a);
459 $datetime->setTimezone($this->timezone);
460 }
461 }
462
463 $start_value = $datetime->format('U');
464 $start_pos = $this->length * ($start_value - $this->start) / $this->duration;
465
466 list($units, $unit_count) = AxisDateTime::parseFixedDivisions($u,
467 $this->start, $this->end, $this->length);
468
469 $datetime->setTimezone($this->timezone);
470 $uc = new Number($unit_count);
471 $datetime->modify('+' . $uc . ' ' . $units);
472 $value = $datetime->format('U');
473 $end_pos = $this->length * ($value - $this->start) / $this->duration;
474 return $end_pos - $start_pos;
475 }
476
477 /**
478 * Returns the position of a value on the axis
479 */
480 public function position($index, $item = null)
481 {
482 if($item === null) {
483 $value = $index;
484
485 // support '10 hours' type of position
486 if(is_string($index) && strpos($index, ' ') !== false) {
487 list($units, $unit_count) = AxisDateTime::parseFixedDivisions($value,
488 $this->start, $this->end, $this->length);
489
490 // initialise with 0, not the current time/date
491 $datetime = new \DateTime('@0');
492 $datetime->setTimezone($this->timezone);
493 $uc = new Number($unit_count);
494 $datetime->modify('+' . $uc . ' ' . $units);
495 $value = $datetime->format('U');
496 }
497 } else {
498 $value = $item->key;
499 }
500 return $this->length * ($value - $this->start) / $this->duration;
501 }
502
503 /**
504 * Returns the position by key, which is a datetime string
505 */
506 public function positionByKey($key)
507 {
508 // ignore grid-relative positions
509 if(in_array($key, ['t', 'l', 'b', 'r', 'h', 'w', 'cx', 'cy']))
510 return null;
511 $value = Graph::dateConvert($key);
512 if($value)
513 return $this->position($value);
514 return null;
515 }
516
517 /**
518 * Returns the value at a position on the axis
519 */
520 public function value($position)
521 {
522 return $this->start + $position * $this->duration / $this->length;
523 }
524
525 /**
526 * Returns the position of the origin
527 */
528 public function origin()
529 {
530 // time started before whatever date the graph starts with
531 return 0;
532 }
533
534 /**
535 * Returns the unit size
536 */
537 public function unit()
538 {
539 $u = AxisDateTime::$unit_sizes[$this->grid_units];
540 $w = $this->length * $u / $this->duration;
541 return max(1, $w);
542 }
543
544 /**
545 * Not actually 0, but the position of the axis
546 */
547 public function zero()
548 {
549 return 0;
550 }
551
552 /**
553 * Returns the grid points as an array of GridPoints
554 */
555 public function getGridPoints($start)
556 {
557 if($start === null)
558 return;
559 $c = $pos = 0;
560 $dlength = $this->length + 1; // allow 1 pixel overflow
561
562 $units = $this->grid_units;
563 $unit_count = $this->grid_unit_count;
564 $value = $this->start;
565
566 // prevent too many grid points if something goes wrong
567 $limit = 1000;
568
569 $points = [];
570 while(floor($pos) < $dlength && ++$c < $limit) {
571
572 $position = $start + ($pos * $this->direction);
573 $points[] = $this->getGridPoint($position, $value);
574
575 $datetime = new \DateTime('@' . $this->start);
576 $datetime->setTimezone($this->timezone);
577 $offset = new Number($c * $unit_count);
578 $datetime->modify('+' . $offset . ' ' . $units);
579 $value = $datetime->format('U');
580 $pos = $this->position($value);
581 }
582
583 return $points;
584 }
585
586 /**
587 * Returns the grid subdivision points as an array
588 */
589 public function getGridSubdivisions($min_space, $min_unit, $start, $fixed)
590 {
591 $subdivs = [];
592 if(!empty($fixed)) {
593 list($units, $unit_count) = AxisDateTime::parseFixedDivisions($fixed,
594 $this->start, $this->end, $this->length);
595
596 } else {
597 // if the main division is the lowest level, there is no subdivision
598 if($this->division == 0)
599 return $subdivs;
600
601 $start_date = new \DateTime('@' . $this->start);
602 $start_date->setTimezone($this->timezone);
603 $end_date = new \DateTime('@' . $this->end);
604 $end_date->setTimezone($this->timezone);
605
606 $div = AxisDateTime::findBestDivision($start_date, $end_date,
607 $this->length, $min_space, $this->division);
608
609 // if no divisions found, stop now
610 if($div === null)
611 return $subdivs;
612 $division = $div[2];
613
614 $units = AxisDateTime::$divisions[$division][0];
615 $unit_count = AxisDateTime::$divisions[$division][1];
616 }
617 $value = $this->start;
618
619 // get the main divisions, turn them into a map of where not to put a
620 // subdivision
621 $main_divisions = $this->getGridPoints($start);
622 $not_here = [];
623 foreach($main_divisions as $d) {
624 $not_here[floor($d->position)] = $d->value;
625 }
626
627 // prevent too many grid points if something goes wrong
628 $limit = 1000;
629
630 $c = $pos = 0;
631 $dlength = $this->length + 1; // allow 1 pixel overflow
632 $text = '';
633 while(floor($pos) < $dlength && ++$c < $limit) {
634
635 $position = $start + ($pos * $this->direction);
636 if(!isset($not_here[floor($position)]) &&
637 !isset($not_here[ceil($position)]))
638 $subdivs[] = new GridPoint($position, $text, $value);
639
640 $datetime = new \DateTime('@' . $this->start);
641 $datetime->setTimezone($this->timezone);
642 $offset = new Number($c * $unit_count);
643 $datetime->modify('+' . $offset . ' ' . $units);
644 $value = $datetime->format('U');
645 $pos = $this->position($value);
646 }
647
648 return $subdivs;
649 }
650
651 /**
652 * Converts a fixed division option to a unit size and count.
653 * $start_time and $end_time are unix timestamps
654 * Returns array($unit, $count)
655 */
656 private static function parseFixedDivisions($fixed_opt, $start_time,
657 $end_time, $axis_length)
658 {
659 if(strpos($fixed_opt, ' ') !== false) {
660 // number and unit
661 list($unit_count, $units) = explode(' ', $fixed_opt);
662
663 } elseif(is_numeric($fixed_opt)) {
664 // number without units
665 $unit_count = $fixed_opt * 1;
666 // make a guess at the units to use
667 $min_interval = ($end_time - $start_time) / $axis_length;
668 foreach(AxisDateTime::$unit_sizes as $unit => $size) {
669 if($size > $min_interval)
670 break;
671 }
672 $units = $unit;
673
674 } else {
675 // unit without number
676 $unit_count = 1;
677 $units = $fixed_opt;
678 }
679
680 $units = rtrim($units, 's');
681 if(!isset(AxisDateTime::$unit_sizes[$units]))
682 throw new \Exception('Unrecognized datetime units [' . $units . ']');
683 if(!is_numeric($unit_count) || $unit_count < 1)
684 $unit_count = 1;
685
686 return [$units, $unit_count];
687 }
688
689 /**
690 * Formats the axis text
691 */
692 public function dateText($f)
693 {
694 $dt = new \DateTime('@' . $f);
695 $dt->setTimezone($this->timezone);
696
697 if(!is_array($this->axis_text_format))
698 return $this->formatter->format($dt, $this->axis_text_format);
699
700 $strings = [];
701 foreach($this->axis_text_format as $fmt)
702 $strings[] = $this->formatter->format($dt, $fmt);
703 return $strings;
704 }
705
706 /**
707 * Returns the format in use
708 */
709 public function getFormat($level = 0)
710 {
711 if(is_array($this->axis_text_format))
712 return $this->axis_text_format[$level];
713 return $this->axis_text_format;
714 }
715
716 /**
717 * Returns the formatted, localized date/time
718 */
719 public function format($dt, $fmt)
720 {
721 return $this->formatter->format($dt, $fmt);
722 }
723 }
724
725