PluginProbe
E2Pdf – Export Pdf Tool for WordPress / 1.32.53
E2Pdf – Export Pdf Tool for WordPress v1.32.53
1.32.53 1.32.51 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 All 74 releases
e2pdf / vendors / svggraph / Graph.php

Graph.php in E2Pdf – Export Pdf Tool for WordPress 1.32.53, at vendors/svggraph/Graph.php

1,619 lines 46.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) 2019-2023 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 <[email protected]>
20 */
21
22 namespace Goat1000\SVGGraph;
23
24 /**
25 * Base class for all graph types
26 */
27 abstract class Graph {
28
29 public $subgraph = false;
30 protected $width = 0;
31 protected $height = 0;
32 protected $pad_left = 0;
33 protected $pad_right = 0;
34 protected $pad_top = 0;
35 protected $pad_bottom = 0;
36 protected $settings = [];
37 protected $values = [];
38 protected $namespace = false;
39 protected $link_base = '';
40 protected $link_target = '_blank';
41 protected $links = [];
42 public $encoding = 'UTF-8';
43
44 protected $colours = null;
45 public $defs = null;
46 public $figures = null;
47 protected $subgraphs = [];
48 protected $back_matter = '';
49
50 protected $namespaces = [];
51 private static $last_id = 0;
52 public static $key_format = null;
53 protected $legend = null;
54 protected $data_label_style_cache = [];
55 protected $multi_graph;
56
57 private static $javascript = null;
58 private $data_labels = null;
59 private $context_menu = null;
60 private $shapes = null;
61
62 /**
63 * @arg $w = width
64 * @arg $h = height
65 * @arg $settings = user options
66 * @arg $fixed_settings = class options overriding user options
67 */
68 public function __construct($w, $h, array $settings, array $fixed_settings = [])
69 {
70 $this->width = $w;
71 $this->height = $h;
72 $this->defs = new Defs($this);
73
74 // get settings from ini file that are relevant to this class
75 $class = get_class($this);
76 $ini_settings = $this->ini_settings($class);
77
78 // default option overrides - subclasses can override these
79 $fixed_setting_defaults = [
80 'repeated_keys' => 'error',
81 'sort_keys' => true,
82 'require_structured' => false,
83 'require_integer_keys' => true,
84 ];
85 $this->settings = array_merge($this->settings, $ini_settings, $settings,
86 $fixed_setting_defaults, $fixed_settings);
87
88 // set up figures - it can't be dynamic because figures can refer
89 // to other figures via Markers
90 $this->figures = new Figures($this, $this->settings);
91
92 // copy some settings to member variables
93 $opts = ['namespace', 'link_base', 'link_target', 'encoding',
94 'pad_top', 'pad_bottom', 'pad_left', 'pad_right'];
95 foreach($opts as $opt)
96 $this->{$opt} = $this->getOption($opt);
97 }
98
99 /**
100 * Deprecated option/member access - display error and fail
101 */
102 public function __get($name)
103 {
104 trigger_error('Attempt to get $this->' . $name, E_USER_WARNING);
105 debug_print_backtrace(0, 1);
106 exit;
107 }
108 public function __isset($name)
109 {
110 trigger_error('Isset attempt on option $this->' . $name, E_USER_WARNING);
111 debug_print_backtrace(0, 1);
112 exit;
113 }
114 public function __set($name, $value)
115 {
116 trigger_error('Attempt to set $this->' . $name, E_USER_WARNING);
117 debug_print_backtrace(0, 1);
118 exit;
119 }
120
121 /**
122 * Returns the settings from the ini file for a class
123 */
124 protected function ini_settings($class)
125 {
126 $ini_file = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'svggraph.ini';
127 $ini_settings = false;
128 if(file_exists($ini_file))
129 $ini_settings = parse_ini_file($ini_file, true);
130 if($ini_settings === false)
131 throw new \Exception('INI file [' . $ini_file . '] could not be loaded');
132
133 $hierarchy = [$class];
134 while($class = get_parent_class($class))
135 array_unshift($hierarchy, $class);
136
137 $settings = [];
138 while(count($hierarchy)) {
139 $class = array_shift($hierarchy);
140 $ns = strrpos($class, '\\');
141 $class = substr($class, $ns + 1);
142 if(array_key_exists($class, $ini_settings))
143 $settings = array_merge($settings, $ini_settings[$class]);
144 }
145
146 return $settings;
147 }
148
149 /**
150 * Sets the graph values
151 */
152 public function values($values)
153 {
154 $new_values = [];
155 $v = func_get_args();
156 if(count($v) == 1)
157 $v = array_shift($v);
158
159 $set_values = true;
160 if(is_array($v)) {
161 reset($v);
162 $first_key = key($v);
163 if($first_key !== null && is_array($v[$first_key])) {
164 foreach($v as $data_set)
165 $new_values[] = $data_set;
166 $set_values = false;
167 }
168 }
169
170 if($set_values)
171 $new_values[] = $v;
172
173 $require_structured = $this->getOption('require_structured');
174 $structured_data = $this->getOption('structured_data');
175 $structure = $this->getOption('structure');
176 $datetime_keys = $this->getOption('datetime_keys');
177 $datetime_key_format = $this->getOption('datetime_key_format');
178 $force_assoc = $this->getOption('force_assoc');
179
180 if($this->getOption('scatter_2d')) {
181 $this->setOption('scatter_2d', false);
182 if(empty($structure)) {
183 $structure = ['key' => 0, 'value' => 1, 'datasets' => true];
184 $this->setOption('structure', $structure);
185 }
186 }
187
188 if($datetime_keys && $datetime_key_format)
189 Graph::$key_format = $datetime_key_format;
190
191 if($structured_data || is_array($structure)) {
192 $this->setOption('structured_data', true);
193 $this->values = new StructuredData($new_values, $force_assoc,
194 $datetime_keys, $structure,
195 $this->getOption('repeated_keys'), $this->getOption('sort_keys'),
196 $this->getOption('require_integer_keys'), $require_structured);
197 } else {
198 $this->values = new Data($new_values, $force_assoc, $datetime_keys);
199 if(!$this->values->error && !empty($require_structured))
200 $this->values->error = get_class($this) . ' requires structured data';
201 }
202
203 if($this->values->error)
204 return;
205
206 $dataset = $this->getOption('dataset', 0);
207 if($dataset === 0)
208 return;
209
210 $dcount = count($this->values);
211 $dataset = $this->getOption(['dataset', 0], 0);
212 if($dcount <= $dataset) {
213 $this->values->error = 'No valid datasets selected';
214 return;
215 }
216
217 // dataset option doesn't work well without structured data
218 $this->values = StructuredData::convertFrom($this->values, $force_assoc,
219 $datetime_keys, $this->getOption('require_integer_keys'));
220 }
221
222 /**
223 * Sets the links from each item
224 */
225 public function links()
226 {
227 $this->links = func_get_args();
228 }
229
230 /**
231 * Assigns the list of subgraphs
232 */
233 public function subgraphs($subgraphs)
234 {
235 $this->subgraphs = $subgraphs;
236 }
237
238 /**
239 * Set up the colours
240 */
241 public function colours(Colours $colours)
242 {
243 $this->colours = $colours;
244 }
245
246 public function getMinValue()
247 {
248 $d = $this->getOption(['dataset', 0], 0);
249 return $this->values->getMinValue($d);
250 }
251 public function getMaxValue()
252 {
253 $d = $this->getOption(['dataset', 0], 0);
254 return $this->values->getMaxValue($d);
255 }
256 public function getMinKey()
257 {
258 $d = $this->getOption(['dataset', 0], 0);
259 return $this->values->getMinKey($d);
260 }
261 public function getMaxKey()
262 {
263 $d = $this->getOption(['dataset', 0], 0);
264 return $this->values->getMaxKey($d);
265 }
266 public function getKey($i)
267 {
268 return $this->values->getKey($i);
269 }
270
271 /**
272 * Sets up the colours used for the graph
273 */
274 protected function setup()
275 {
276 $dataset = $this->getOption(['dataset', 0], 0);
277 $this->colourSetup($this->values->itemsCount($dataset));
278 }
279
280 /**
281 * Returns the Javascript instance
282 */
283 public function getJavascript()
284 {
285 if(!isset(Graph::$javascript))
286 Graph::$javascript = new Javascript($this);
287 return Graph::$javascript;
288 }
289
290 /**
291 * Returns the DataLabels instance
292 */
293 public function getDataLabels()
294 {
295 if($this->data_labels === null)
296 $this->data_labels = new DataLabels($this);
297 return $this->data_labels;
298 }
299
300 /**
301 * Draws the selected graph
302 */
303 public function drawGraph()
304 {
305 $canvas_id = $this->newID();
306 $this->initLegend();
307 $this->setup();
308 if(!is_numeric($this->width))
309 $this->width = 640;
310 if(!is_numeric($this->height))
311 $this->height = 480;
312
313 $contents = $this->canvas($canvas_id);
314 $contents .= $this->drawTitle();
315 $contents .= $this->draw();
316 $contents .= $this->drawDataLabels();
317 $contents .= $this->drawBackMatter();
318 $contents .= $this->drawLegend();
319
320 foreach($this->subgraphs as $subgraph)
321 $contents .= $subgraph->fetch($this);
322
323 // magnifying means everything must be in a group for transformation
324 if(!$this->subgraph && $this->getOption('magnify')) {
325 $this->getJavascript()->magnifier();
326 $group = ['class' => 'svggraph-magnifier'];
327 $contents = $this->element('g', $group, null, $contents);
328 }
329
330 // rounded rects might need a clip path
331 if($this->getOption('back_round') && $this->getOption('back_round_clip')) {
332 $group = ['clip-path' => 'url(#' . $canvas_id . ')'];
333 return $this->element('g', $group, null, $contents);
334 }
335 return $contents;
336 }
337
338 /**
339 * Adds any markup that goes after the graph
340 */
341 protected function drawBackMatter()
342 {
343 return $this->back_matter;
344 }
345
346 /**
347 * Sets up the legend class
348 */
349 protected function initLegend()
350 {
351 $this->legend = null;
352
353 // see if the legend is needed
354 if(!$this->getOption('show_legend'))
355 return;
356
357 $entries = $this->getOption('legend_entries');
358 $structure = $this->getOption('structure');
359 if(empty($entries) && !isset($structure['legend_text']))
360 return;
361
362 $this->legend = new Legend($this);
363 }
364
365 /**
366 * Returns the ordering for legend entries
367 */
368 public function getLegendOrder()
369 {
370 // null for no special order
371 return null;
372 }
373
374 /**
375 * Draws the legend
376 */
377 protected function drawLegend()
378 {
379 if($this->legend === null)
380 return '';
381 return $this->legend->draw();
382 }
383
384 /**
385 * Parses a position string, returning x and y coordinates
386 */
387 public function parsePosition($pos, $w = 0, $h = 0, $pad = 0)
388 {
389 $inner = true;
390 $parts = preg_split('/\s+/', $pos);
391 if(count($parts)) {
392 // if 'outer' is found after 'inner', it takes precedence
393 $parts = array_reverse($parts);
394 $inner_at = array_search('inner', $parts);
395 $outer_at = array_search('outer', $parts);
396
397 if($outer_at !== false && ($inner_at === false || $inner_at < $outer_at))
398 $inner = false;
399 }
400
401 if($inner) {
402 $t = $this->pad_top;
403 $l = $this->pad_left;
404 $b = $this->height - $this->pad_bottom;
405 $r = $this->width - $this->pad_right;
406 // make sure it fits to keep RelativePosition happy
407 if($w > $r - $l) $w = $r - $l;
408 if($h > $b - $t) $h = $b - $t;
409 } else {
410 $t = $l = 0;
411 $b = $this->height;
412 $r = $this->width;
413 }
414
415 // ParsePosition is always inside canvas or graph, defaulted to top left
416 $pos = 'top left ' . str_replace('outer', 'inner', $pos);
417 return Graph::relativePosition($pos, $t, $l, $b, $r, $w, $h, $pad);
418 }
419
420 /**
421 * Returns [hpos,vpos,offset_x,offset_y] positions derived from full
422 * position string
423 */
424 public static function translatePosition($pos)
425 {
426 $parts = preg_split('/\s+/', strtolower($pos));
427 $offset_x = $offset_y = 0;
428 $inside = true;
429 $vpos = 'm';
430 $hpos = 'c';
431
432 // translated positions:
433 // ot, t, m, b, ob = outside top, top, middle, bottom, outside bottom
434 // ol, l, c, r, or = outside left, left, centre, right, outside right
435 while(count($parts)) {
436 $part = array_shift($parts);
437 switch($part) {
438 case 'outer' :
439 case 'outside' : $inside = false;
440 break;
441 case 'inner' :
442 case 'inside' : $inside = true;
443 break;
444 case 'top' : $vpos = $inside ? 't' : 'ot';
445 break;
446 case 'bottom' : $vpos = $inside ? 'b' : 'ob';
447 break;
448 case 'left' : $hpos = $inside ? 'l' : 'ol';
449 break;
450 case 'right' : $hpos = $inside ? 'r' : 'or';
451 break;
452 case 'above' : $inside = false; $vpos = 'ot';
453 break;
454 case 'below' : $inside = false; $vpos = 'ob';
455 break;
456 // BEGIN e2pdf
457 case 'center' : $hpos = $inside ? 'c' : 'oc';
458 break;
459 case 'middle' : $vpos = $inside ? 'm' : 'om';
460 break;
461 // END e2pdf
462 default:
463 if(is_numeric($part)) {
464 $offset_x = $part;
465 if(count($parts) && is_numeric($parts[0]))
466 $offset_y = array_shift($parts);
467 }
468 }
469 }
470 return [$hpos, $vpos, $offset_x, $offset_y];
471 }
472
473 /**
474 * Returns [x,y,text-anchor,hpos,vpos] position that is $pos relative to the
475 * top, left, bottom and right.
476 * When $text is true, x and y are adjusted for text-anchor position
477 */
478 public static function relativePosition($pos, $top, $left,
479 $bottom, $right, $width, $height, $pad, $text = false)
480 {
481 list($hpos, $vpos, $offset_x, $offset_y) = Graph::translatePosition($pos);
482
483 // if the containers have no thickness, position outside
484 //e2pdf
485 $translate = ['l' => 'ol', 'r' => 'or', 't' => 'ot', 'b' => 'ob', 'c' => 'oc', 'm' => 'om'];
486 if($top == $bottom && isset($translate[$vpos]))
487 $vpos = $translate[$vpos];
488 if($left == $right && isset($translate[$hpos]))
489 $hpos = $translate[$hpos];
490
491 switch($vpos) {
492 case 'ot' : $y = $top - $height - $pad; break;
493 case 't' : $y = $top + $pad; break;
494 case 'b' : $y = $bottom - $height - $pad; break;
495 case 'ob' : $y = $bottom + $pad; break;
496 // e2pdf
497 case 'om': $y = $top + ($bottom - $top - $height) / 2; break;
498 case 'm' :
499 default :
500 $y = $top + ($bottom - $top - $height) / 2; break;
501 }
502
503 if(($hpos == 'r' || $hpos == 'l') && $right - $left - $pad - $width < 0)
504 $hpos = 'c';
505 switch($hpos) {
506 case 'ol' : $x = $left - $width - $pad; break;
507 case 'l' : $x = $left + $pad; break;
508 case 'r' : $x = $right - $width - $pad; break;
509 case 'or' : $x = $right + $pad; break;
510 // e2pdf
511 case 'oc' : $x = $left + ($right - $left - $width - $pad) / 2; break;
512 case 'c' :
513 default :
514 $x = $left + ($right - $left - $width) / 2; break;
515 }
516
517 $y += $offset_y;
518 $x += $offset_x;
519
520 // third return value is text alignment
521 $align_map = [
522 'ol' => 'end', 'l' => 'start', 'c' => 'middle',
523 'r' => 'end', 'or' => 'start'
524 ];
525 $text_align = $align_map[$hpos];
526
527 // in text mode, adjust X for text alignment
528 if($text && $hpos != 'l' && $hpos != 'or') {
529 if($hpos == 'c')
530 $x += $width / 2;
531 else
532 $x += $width;
533 }
534 return [$x, $y, $text_align, $hpos, $vpos];
535 }
536
537 /**
538 * Sets the style info for the legend
539 */
540 protected function setLegendEntry($dataset, $index, $item, $style_info)
541 {
542 if($this->legend === null)
543 return;
544 $this->legend->setEntry($dataset, $index, $item, $style_info);
545 }
546
547 /**
548 * Subclasses must draw the entry, if they can
549 */
550 protected function drawLegendEntry($x, $y, $w, $h, $entry)
551 {
552 return '';
553 }
554
555 /**
556 * Returns details of title and subtitle
557 */
558 protected function getTitle()
559 {
560 $info = [
561 'title' => $this->getOption('graph_title'),
562 'font' => $this->getOption('graph_title_font'),
563 'font_size' => 0, // 0 font size = no title
564 'sfont_size' => 0, // 0 = no subtitle
565 ];
566
567 $svg_text = new Text($this, $info['font']);
568 if($svg_text->strlen($info['title']) <= 0)
569 return $info;
570
571 $info['font_size'] = Number::units($this->getOption('graph_title_font_size'));
572 $info['weight'] = $this->getOption('graph_title_font_weight');
573 $info['colour'] = $this->getOption('graph_title_colour');
574 $info['pos'] = $this->getOption('graph_title_position');
575 $info['space'] = $this->getOption('graph_title_space');
576 $info['line_spacing'] = Number::units($this->getOption('graph_title_line_spacing'));
577 if($info['line_spacing'] === null || $info['line_spacing'] < 1)
578 $info['line_spacing'] = $info['font_size'];
579
580 if($info['pos'] != 'bottom' && $info['pos'] != 'left' && $info['pos'] != 'right')
581 $info['pos'] = 'top';
582 list($width, $height) = $svg_text->measure($info['title'], $info['font_size'],
583 0, $info['line_spacing']);
584 $info['width'] = $width;
585 $info['height'] = $height;
586
587 // now deal with sub-title
588 $info['stitle'] = $this->getOption('graph_subtitle');
589 $info['sfont'] = $this->getOption('graph_subtitle_font', 'graph_title_font');
590 $svg_subtext = new Text($this, $info['sfont']);
591
592 if($svg_subtext->strlen($info['stitle']) > 0) {
593 $info['sfont_size'] = Number::units($this->getOption('graph_subtitle_font_size',
594 'graph_title_font_size'));
595 $info['sweight'] = $this->getOption('graph_subtitle_font_weight',
596 'graph_title_font_weight');
597 $info['scolour'] = $this->getOption('graph_subtitle_colour',
598 'graph_title_colour');
599 $info['sspace'] = $this->getOption('graph_subtitle_space',
600 'graph_title_space');
601 $info['sline_spacing'] = Number::units($this->getOption('graph_subtitle_line_spacing'));
602 if($info['sline_spacing'] === null || $info['sline_spacing'] < 1)
603 $info['sline_spacing'] = $info['sfont_size'];
604
605 list($swidth, $sheight) = $svg_subtext->measure($info['stitle'],
606 $info['sfont_size'], 0, $info['sline_spacing']);
607 $info['swidth'] = $swidth;
608 $info['sheight'] = $sheight;
609 }
610
611 return $info;
612 }
613
614 /**
615 * Draws the graph title, if there is one
616 */
617 protected function drawTitle()
618 {
619 $info = $this->getTitle();
620 if($info['font_size'] == 0)
621 return '';
622
623 $svg_text = new Text($this, $info['font']);
624 $baseline = $svg_text->baseline($info['font_size']);
625 $text = [
626 'font-size' => $info['font_size'],
627 'font-family' => $info['font'],
628 'font-weight' => $info['weight'],
629 'text-anchor' => 'middle',
630 'fill' => new Colour($this, $info['colour']),
631 ];
632
633 // ensure outside padding is at least the title space
634 $pad_side = 'pad_' . $info['pos'];
635 if($this->{$pad_side} < $info['space'])
636 $this->{$pad_side} = $info['space'];
637
638 $xform = new Transform;
639 switch($info['pos']) {
640 case 'left':
641 $text['x'] = $this->pad_left + $baseline;
642 $text['y'] = $this->height / 2;
643 $xform->rotate(270, $text['x'], $text['y']);
644 $text['transform'] = $xform;
645 break;
646 case 'right':
647 $text['x'] = $this->width - $this->pad_right - $baseline;
648 $text['y'] = $this->height / 2;
649 $xform->rotate(90, $text['x'], $text['y']);
650 $text['transform'] = $xform;
651 break;
652 case 'bottom':
653 $text['x'] = $this->width / 2;
654 $text['y'] = $this->height - $this->pad_bottom - $info['height'] + $baseline;
655 break;
656 default:
657 $text['x'] = $this->width / 2;
658 $text['y'] = $this->pad_top + $baseline;
659 }
660 // increase padding by size of text
661 $this->{$pad_side} += $info['height'] + $info['space'];
662
663 // now deal with sub-title
664 $subtitle_text = '';
665 if($info['sfont_size'] != 0) {
666
667 $svg_subtext = new Text($this, $info['sfont']);
668 $sbaseline = $svg_subtext->baseline($info['sfont_size']);
669 $stext = [
670 'font-size' => $info['sfont_size'],
671 'font-family' => $info['sfont'],
672 'font-weight' => $info['sweight'],
673 'text-anchor' => 'middle',
674 'fill' => new Colour($this, $info['scolour']),
675 ];
676
677 $sxform = new Transform;
678 $sub_offset = $sbaseline + $info['sspace'] - $info['space'];
679 switch($info['pos']) {
680 case 'left':
681 $stext['x'] = $this->pad_left + $sub_offset;
682 $stext['y'] = $text['y'];
683 $sxform->rotate(270, $stext['x'], $stext['y']);
684 $stext['transform'] = $sxform;
685 break;
686 case 'right':
687 $stext['x'] = $this->width - $this->pad_right - $sub_offset;
688 $stext['y'] = $text['y'];
689 $sxform->rotate(90, $stext['x'], $stext['y']);
690 $stext['transform'] = $sxform;
691 break;
692 case 'bottom':
693 // complicates things - need to shift title up
694 $stext['x'] = $text['x'];
695 $stext['y'] = $text['y'] - $baseline + $info['height'] + $sbaseline - $info['sheight'];
696 $text['y'] -= $info['sheight'] + $info['sspace'];
697 break;
698 default:
699 $stext['x'] = $text['x'];
700 $stext['y'] = $this->pad_top + $sub_offset;
701 }
702
703 $this->{$pad_side} += $info['sheight'] + $info['sspace'];
704 $subtitle_text = $svg_subtext->text($info['stitle'], $info['sline_spacing'], $stext);
705 }
706
707 // the Text function will break it into lines
708 $title_text = $svg_text->text($info['title'], $info['line_spacing'], $text);
709
710 return $title_text . $subtitle_text;
711 }
712
713 /**
714 * This should be overridden by subclass!
715 */
716 abstract protected function draw();
717
718 /**
719 * Displays the background image
720 */
721 protected function backgroundImage($shadow, $clip_path)
722 {
723 $image_src = $this->getOption('back_image');
724 if(!$image_src)
725 return '';
726
727 $width = $this->getOption('back_image_width');
728 $height = $this->getOption('back_image_height');
729 $left = $this->getOption('back_image_left');
730 $top = $this->getOption('back_image_top');
731 $mode = $this->getOption('back_image_mode');
732
733 if($shadow) {
734 if($width === '100%')
735 $width = new Number($this->width - $shadow);
736 if($height === '100%')
737 $height = new Number($this->height - $shadow);
738 }
739
740 $image = [
741 'width' => $width, 'height' => $height,
742 'x' => $left, 'y' => $top,
743 'xlink:href' => $image_src,
744 'preserveAspectRatio' =>
745 ($mode == 'stretch' ? 'none' : 'xMinYMin')
746 ];
747
748 if($clip_path !== null)
749 $image['clip-path'] = 'url(#' . $clip_path . ')';
750
751 $style = [];
752 if($this->getOption('back_image_opacity'))
753 $style['opacity'] = $this->getOption('back_image_opacity');
754
755 $contents = '';
756 if($mode == 'tile') {
757 $image['x'] = 0; $image['y'] = 0;
758 $im = $this->element('image', $image, $style);
759 $pattern = [
760 'id' => $this->newID(),
761 'width' => $width, 'height' => $height,
762 'x' => $left, 'y' => $top,
763 'patternUnits' => 'userSpaceOnUse'
764 ];
765 // tiled image becomes a pattern to replace background colour
766 $this->defs->add($this->element('pattern', $pattern, null, $im));
767 $this->setOption('back_colour', 'url(#' . $pattern['id'] . ')');
768 } else {
769 $im = $this->element('image', $image, $style);
770 $contents .= $im;
771 }
772 return $contents;
773 }
774
775 /**
776 * Displays the background
777 */
778 protected function canvas($id)
779 {
780 $round = $this->getOption('back_round');
781 $stroke_width = $this->getOption('back_stroke_width');
782 $stroke_colour = new Colour($this, $this->getOption('back_stroke_colour'));
783 $shadow_opacity = $this->getOption('back_shadow_opacity');
784 $shadow_blur = $this->getOption('back_shadow_blur');
785 $shadow = $this->getOption('back_shadow');
786 if($shadow === true)
787 $shadow = 2;
788
789 // background image can replace the back_colour option
790 $bg = $this->backgroundImage($shadow, $round ? $id : null);
791 $colour = new Colour($this, $this->getOption('back_colour'));
792
793 $canvas = [
794 'width' => '100%', 'height' => '100%',
795 'fill' => $colour,
796 'stroke-width' => 0,
797 ];
798 $c_el = '';
799
800 if($shadow) {
801 // shadow means canvas cannot be 100% of document
802 $canvas['x'] = $canvas['y'] = new Number($stroke_width / 2);
803
804 // blurring means using a filter and a larger offset
805 if($shadow_blur) {
806 $filter_opts = [
807 'offset_x' => $shadow,
808 'offset_y' => $shadow,
809 'blur' => $shadow_blur,
810 'opacity' => $shadow_opacity,
811 'shadow_only' => true,
812 ];
813
814 $shadow += $shadow_blur;
815 $filter = $this->defs->addFilter('shadow', $filter_opts);
816 }
817 $canvas['width'] = new Number($this->width - $shadow - $stroke_width);
818 $canvas['height'] = new Number($this->height - $shadow - $stroke_width);
819
820 $filled = [
821 //'x' => $shadow, 'y' => $shadow,
822 'width' => new Number($this->width - $shadow),
823 'height' => new Number($this->height - $shadow),
824 'fill' => '#000',
825 'stroke-width' => 0,
826 //'opacity' => $shadow_opacity,
827 ];
828
829 if($shadow_blur) {
830 $filled['filter'] = 'url(#' . $filter . ')';
831 } else {
832 $filled['x'] = $shadow;
833 $filled['y'] = $shadow;
834 $filled['opacity'] = $shadow_opacity;
835 }
836
837 if($round)
838 $filled['rx'] = $filled['ry'] = new Number($round);
839
840 $c_el .= $this->element('rect', $filled);
841
842 // increase padding to clear shadow
843 $this->pad_right += $shadow;
844 $this->pad_bottom += $shadow;
845 }
846
847 if($colour->opacity() < 1)
848 $canvas['opacity'] = $colour->opacity(true);
849
850 if($round)
851 $canvas['rx'] = $canvas['ry'] = new Number($round);
852 if($bg == '' && $stroke_width) {
853 $canvas['stroke-width'] = $stroke_width;
854 $canvas['stroke'] = $stroke_colour;
855 }
856 $c_el .= $this->element('rect', $canvas);
857
858 // create a clip path for rounded rectangle
859 if($round) {
860 $this->defs->add($this->element('clipPath', ['id' => $id], null,
861 $this->element('rect', $canvas)));
862 }
863
864 // if the background image is an element, insert it between the background
865 // colour and border rect
866 if($bg != '') {
867 $c_el .= $bg;
868 if($stroke_width) {
869 $canvas['stroke-width'] = $stroke_width;
870 $canvas['stroke'] = $stroke_colour;
871 $canvas['fill'] = 'none';
872 $c_el .= $this->element('rect', $canvas);
873 }
874 }
875
876 return $c_el;
877 }
878
879 /**
880 * Displays readable (hopefully) error message
881 */
882 protected function errorText($error)
883 {
884 if(!is_numeric($this->height))
885 $this->height = 100;
886 $text = ['x' => 3, 'y' => $this->height - 3];
887 $style = [
888 'font-family' => 'Courier New',
889 'font-size' => '11px',
890 'font-weight' => 'bold',
891 ];
892
893 $e = $this->contrastText($text['x'], $text['y'], $error, 'blue',
894 'white', $style);
895 return $e;
896 }
897
898 /**
899 * Displays high-contrast text
900 */
901 protected function contrastText($x, $y, $text, $fcolour = 'black',
902 $bcolour = 'white', $properties = null, $styles = null)
903 {
904 $xform = new Transform;
905 $xform->translate($x, $y);
906 $props = ['transform' => $xform, 'fill' => $fcolour];
907 if(is_array($properties))
908 $props = array_merge($properties, $props);
909
910 $bg = $this->element('text', ['stroke-width' => '2px', 'stroke' => $bcolour],
911 null, $text);
912 $fg = $this->element('text', null, null, $text);
913 return $this->element('g', $props, $styles, $bg . $fg);
914 }
915
916 /**
917 * Builds an element
918 */
919 public function element($name, $attribs = null, $styles = null,
920 $content = null, $no_whitespace = false)
921 {
922 if($this->namespace && strpos($name, ':') === false)
923 $name = 'svg:' . $name;
924 $element = '<' . $name;
925 if(is_array($attribs)) {
926 foreach($attribs as $attr => $val) {
927 $value = new Attribute($attr, $val, $this->encoding);
928 $element .= ' ' . $attr . '="' . $value . '"';
929 }
930 }
931
932 if(is_array($styles)) {
933 $element .= ' style="';
934 foreach($styles as $attr => $val) {
935 $value = new Attribute($attr, $val, $this->encoding);
936 $element .= $attr . ':' . $value . ';';
937 }
938 $element .= '"';
939 }
940
941 if($content === null)
942 $element .= "/>";
943 else
944 $element .= '>' . $content . '</' . $name . ">";
945 if(!$no_whitespace)
946 $element .= "\n";
947
948 return $element;
949 }
950
951 /**
952 * Returns a link URL or NULL if none
953 */
954 public function getLinkURL($item, $key, $row = 0)
955 {
956 $link = ($item === null ? null : $item->link);
957 if(is_numeric($key))
958 $key = (int)round($key);
959 if($link === null && is_array($this->links[$row]) &&
960 isset($this->links[$row][$key])) {
961 $link = $this->links[$row][$key];
962 }
963
964 // check for absolute links
965 if($link !== null && strpos($link,'//') === false)
966 $link = $this->link_base . $link;
967
968 return $link;
969 }
970
971 /**
972 * Retrieves a link
973 */
974 public function getLink($item, $key, $content, $row = 0)
975 {
976 $link = $this->getLinkURL($item, $key, $row);
977 if($link === null)
978 return $content;
979
980 $link_attr = ['xlink:href' => $link];
981 if(!empty($this->link_target))
982 $link_attr['target'] = $this->link_target;
983 return $this->element('a', $link_attr, null, $content);
984 }
985
986 /**
987 * Returns TRUE if the item is visible on the graph
988 */
989 public function isVisible($item, $dataset = 0)
990 {
991 // default implementation is for all non-zero values to be visible
992 return ($item->value != 0);
993 }
994
995 /**
996 * Sets up the colour class
997 */
998 protected function colourSetup($count, $datasets = null, $reverse = false)
999 {
1000 $this->colours->setup($count, $datasets, $reverse);
1001 }
1002
1003 /**
1004 * Returns a Colour
1005 */
1006 public function getColour($item, $key, $dataset, $allow_gradient = true,
1007 $allow_pattern = true)
1008 {
1009 if($item !== null && $item->colour !== null)
1010 return new Colour($this, $item->colour, $allow_gradient, $allow_pattern);
1011
1012 $c = $this->colours->getColour($key, $dataset);
1013 if($c === null)
1014 return new Colour($this, null);
1015
1016 $colour = new Colour($this, $c, $allow_gradient, $allow_pattern);
1017
1018 // make key reflect dataset as well (for gradients)
1019 if($dataset !== null)
1020 $key = $dataset . ':' . $key;
1021 if($key !== null)
1022 $colour->setGradientKey($key);
1023
1024 return $colour;
1025 }
1026
1027 /**
1028 * Returns the first non-empty option in named argument list.
1029 * Arguments must be "opt_name" or array("opt_name", $index), optionally
1030 * ending with default value (non-string or array('@', $value))
1031 */
1032 public function getOption($opt, $opt2 = null)
1033 {
1034 // single option - checking for null second option is faster
1035 // than using func_num_args()
1036 if($opt2 === null && is_string($opt)) {
1037 if(isset($this->settings[$opt]) && $this->settings[$opt] !== '')
1038 return $this->settings[$opt];
1039 return null;
1040 }
1041
1042 $opts = func_get_args();
1043 foreach($opts as $opt) {
1044 // not string or array, default value
1045 if(!is_array($opt) && !is_string($opt))
1046 return $opt;
1047
1048 if(is_array($opt)) {
1049 // validate
1050 if(!isset($opt[0]) || !isset($opt[1]) || !is_string($opt[0]))
1051 throw new \InvalidArgumentException('Malformed option array');
1052
1053 // default value
1054 if($opt[0] === '@')
1055 return $opt[1];
1056
1057 list($name, $index) = $opt;
1058 if(isset($this->settings[$name])) {
1059 $val = $this->settings[$name];
1060 if(is_array($val)) {
1061
1062 if(isset($val[$index])) {
1063 $val = $val[$index];
1064 } else {
1065 if(!is_numeric($index))
1066 $index = 0;
1067 $val = $val[$index % count($val)];
1068 }
1069 }
1070
1071 if($val !== null && $val !== '')
1072 return $val;
1073 }
1074 continue;
1075 }
1076
1077 // not an array
1078 if(isset($this->settings[$opt])) {
1079 $val = $this->settings[$opt];
1080 if($val !== null && $val !== '')
1081 return $val;
1082 }
1083 }
1084
1085 return null;
1086 }
1087
1088 /**
1089 * Returns option from data item if present, or from settings if not
1090 */
1091 public function getItemOption($option, $dataset, &$item, $item_option = null)
1092 {
1093 if($item_option === null)
1094 $item_option = $option;
1095 $value = null;
1096 if($item !== null)
1097 $value = $item->data($item_option);
1098 if($value === null)
1099 $value = $this->getOption([$option, $dataset]);
1100 return $value;
1101 }
1102
1103 /**
1104 * Option setter
1105 */
1106 public function setOption($name, $value, $index = null)
1107 {
1108 // very simple for now, might have to revisit it later
1109 if($index === null) {
1110 $this->settings[$name] = $value;
1111 return;
1112 }
1113 $this->settings[$name][$index] = $value;
1114 }
1115
1116 /**
1117 * Returns the graph size and padding
1118 */
1119 public function getDimensions()
1120 {
1121 $dimensions = [
1122 'width' => $this->width,
1123 'height' => $this->height,
1124 'pad_left' => $this->pad_left,
1125 'pad_top' => $this->pad_top,
1126 'pad_right' => $this->pad_right,
1127 'pad_bottom' => $this->pad_bottom,
1128 ];
1129 return $dimensions;
1130 }
1131
1132 /**
1133 * Checks that the data are valid
1134 */
1135 protected function checkValues()
1136 {
1137 if($this->values->error)
1138 throw new \Exception($this->values->error);
1139 }
1140
1141 /**
1142 * Sets the stroke options for an element
1143 */
1144 protected function setStroke(&$attr, &$item, $key, $dataset, $line_join = null)
1145 {
1146 unset($attr['stroke'], $attr['stroke-width'], $attr['stroke-linejoin'],
1147 $attr['stroke-dasharray']);
1148
1149 $stroke_width = $this->getItemOption('stroke_width', $dataset, $item);
1150 if($stroke_width > 0) {
1151 $cg = new ColourGroup($this, $item, $key, $dataset);
1152 $attr['stroke'] = $cg->stroke();
1153
1154 if($attr['stroke']->opacity() < 1)
1155 $attr['stroke-opacity'] = $attr['stroke']->opacity(true);
1156 $attr['stroke-width'] = $stroke_width;
1157 if($line_join !== null)
1158 $attr['stroke-linejoin'] = $line_join;
1159
1160 $dash = $this->getItemOption('stroke_dash', $dataset, $item);
1161 if(!empty($dash))
1162 $attr['stroke-dasharray'] = $dash;
1163 }
1164 }
1165
1166 /**
1167 * Creates a new ID for an element
1168 */
1169 public function newID()
1170 {
1171 $prefix = (string)$this->getOption('id_prefix');
1172 $i = ++Graph::$last_id;
1173
1174 // id is case sensitive, so use lower and upper case
1175 $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
1176 $iid = [];
1177 while($i > 0) {
1178 $c = $i % 62;
1179 $i = floor($i / 62);
1180 $iid[] = $chars[$c];
1181 }
1182 return $prefix . 'e' . implode(array_reverse($iid));
1183 }
1184
1185 /**
1186 * Adds markup to be inserted between graph and legend
1187 */
1188 public function addBackMatter($fragment)
1189 {
1190 $this->back_matter .= $fragment;
1191 }
1192
1193 /**
1194 * Adds context menu for item
1195 */
1196 protected function setContextMenu(&$element, $dataset, &$item,
1197 $duplicate = false)
1198 {
1199 if($this->context_menu === null) {
1200 $js = $this->getJavascript();
1201 $this->context_menu = new ContextMenu($this, $js);
1202 }
1203 $this->context_menu->setMenu($element, $dataset, $item, $duplicate);
1204 }
1205
1206 /**
1207 * Default tooltip contents are key and value, or whatever
1208 * $key is if $value is not set
1209 */
1210 protected function setTooltip(&$element, &$item, $dataset, $key, $value = null,
1211 $duplicate = false)
1212 {
1213 $callback = $this->getOption('tooltip_callback');
1214 $structure = $this->getOption('structure');
1215 if(is_callable($callback)) {
1216 if($value === null)
1217 $value = $key;
1218 $text = call_user_func_array($callback, [$dataset, $key, $value]);
1219 } elseif(is_array($structure) && isset($structure['tooltip'])) {
1220 // use structured data tooltips if specified
1221 $text = $item->tooltip;
1222 } else {
1223 $text = $this->formatTooltip($item, $dataset, $key, $value);
1224 }
1225 if($text === null)
1226 return;
1227 $text = addslashes(str_replace("\n", '\n', $text));
1228 return $this->getJavascript()->setTooltip($element, $text, $duplicate);
1229 }
1230
1231 /**
1232 * Default format is value only
1233 */
1234 protected function formatTooltip(&$item, $dataset, $key, $value)
1235 {
1236 $n = new Number($value, $this->getOption('units_tooltip'),
1237 $this->getOption('units_before_tooltip'));
1238 return $n->format();
1239 }
1240
1241 /**
1242 * Adds a data label to the list
1243 */
1244 protected function addDataLabel($dataset, $index, &$element, &$item,
1245 $x, $y, $w, $h, $content = null, $duplicate = true)
1246 {
1247 if(!$this->getOption(['show_data_labels', $dataset]))
1248 return false;
1249
1250 // set up fading for this label?
1251 $id = null;
1252 $fade_in = $this->getOption(['data_label_fade_in_speed', $dataset]);
1253 $fade_out = $this->getOption(['data_label_fade_out_speed', $dataset]);
1254 $click = $this->getOption(['data_label_click', $dataset]);
1255 $popup = $this->getOption(['data_label_popfront', $dataset]);
1256 if($click == 'hide' || $click == 'show') {
1257 $id = $this->newID();
1258 $this->getJavascript()->setClickShow($element, $id, $click == 'hide', $duplicate);
1259 }
1260 if($popup) {
1261 if(!$id)
1262 $id = $this->newID();
1263 $this->getJavascript()->setPopFront($element, $id, $duplicate);
1264 }
1265 if($fade_in || $fade_out) {
1266 $speed_in = $fade_in ? $fade_in / 100 : 0;
1267 $speed_out = $fade_out ? $fade_out / 100 : 0;
1268 if(!$id)
1269 $id = $this->newID();
1270 $this->getJavascript()->setFader($element, $speed_in, $speed_out, $id, $duplicate);
1271 }
1272 $this->getDataLabels()->addLabel($dataset, $index, $item, $x, $y, $w, $h, $id,
1273 $content, $fade_in, $click);
1274 return true;
1275 }
1276
1277 /**
1278 * Adds an element as a client of existing label
1279 */
1280 protected function addLabelClient($dataset, $index, &$element)
1281 {
1282 $label = $this->getDataLabels()->getLabel($dataset, $index);
1283 if($label === null)
1284 return false;
1285
1286 $id = $label['id'];
1287 $fade_in = $this->getOption(['data_label_fade_in_speed', $dataset]);
1288 $fade_out = $this->getOption(['data_label_fade_out_speed', $dataset]);
1289 $click = $this->getOption(['data_label_click', $dataset]);
1290 $popup = $this->getOption(['data_label_popfront', $dataset]);
1291 if($click == 'hide' || $click == 'show')
1292 $this->getJavascript()->setClickShow($element, $id, $click == 'hide', true);
1293 if($popup)
1294 $this->getJavascript()->setPopFront($element, $id, true);
1295 if($fade_in || $fade_out) {
1296 $speed_in = $fade_in ? $fade_in / 100 : 0;
1297 $speed_out = $fade_out ? $fade_out / 100 : 0;
1298 $this->getJavascript()->setFader($element, $speed_in, $speed_out, $id, true);
1299 }
1300 }
1301
1302 /**
1303 * Adds a label for non-data text
1304 */
1305 protected function addContentLabel($dataset, $index, $x, $y, $w, $h, $content)
1306 {
1307 $this->getDataLabels()->addContentLabel($dataset, $index, $x, $y, $w, $h,
1308 $content);
1309 return true;
1310 }
1311
1312 /**
1313 * Draws the data labels
1314 */
1315 protected function drawDataLabels()
1316 {
1317 if(isset($this->settings['label']))
1318 $this->getDataLabels()->load($this->settings);
1319 return $this->getDataLabels()->getLabels();
1320 }
1321
1322 /**
1323 * Returns the position for a data label
1324 */
1325 public function dataLabelPosition($dataset, $index, &$item, $x, $y, $w, $h,
1326 $label_w, $label_h)
1327 {
1328 $pos = $this->getOption(['data_label_position', $dataset]);
1329 if(empty($pos))
1330 $pos = 'above';
1331 $end = [$x + $w * 0.5, $y + $h * 0.5];
1332 return [$pos, $end];
1333 }
1334
1335 /**
1336 * Returns the ShapeList instance
1337 */
1338 public function getShapeList()
1339 {
1340 if($this->shapes === null) {
1341 $this->shapes = new ShapeList($this);
1342 $this->shapes->load($this->settings);
1343 }
1344 return $this->shapes;
1345 }
1346
1347 public function underShapes()
1348 {
1349 if(!isset($this->settings['shape']))
1350 return '';
1351 return $this->getShapeList()->draw(ShapeList::BELOW);
1352 }
1353
1354 public function overShapes()
1355 {
1356 if(!isset($this->settings['shape']))
1357 return '';
1358 return $this->getShapeList()->draw(ShapeList::ABOVE);
1359 }
1360
1361 /**
1362 * Returns TRUE if the position is inside the item
1363 */
1364 public static function isPositionInside($pos)
1365 {
1366 list($hpos, $vpos) = Graph::translatePosition($pos);
1367 return strpos($hpos . $vpos, 'o') === false;
1368 }
1369
1370 /**
1371 * Sets the styles for data labels
1372 */
1373 public function dataLabelStyle($dataset, $index, &$item)
1374 {
1375 // this function gets called a lot, so cache the return values
1376 if(isset($this->data_label_style_cache[$dataset]))
1377 return $this->data_label_style_cache[$dataset];
1378
1379 $map = $this->getDataLabels()->getStyleMap();
1380 $style = [];
1381 foreach($map as $key => $option) {
1382 $style[$key] = $this->getOption([$option, $dataset]);
1383 }
1384
1385 // padding x/y options override single value
1386 $style['pad_x'] = $this->getOption(['data_label_padding_x', $dataset],
1387 ['data_label_padding', $dataset]);
1388 $style['pad_y'] = $this->getOption(['data_label_padding_y', $dataset],
1389 ['data_label_padding', $dataset]);
1390
1391 $this->data_label_style_cache[$dataset] = $style;
1392 return $style;
1393 }
1394
1395 /**
1396 * Tail direction is required for some types of label
1397 */
1398 public function dataLabelTailDirection($dataset, $index, $hpos, $vpos)
1399 {
1400 // angle starts at right, goes clockwise
1401 $angle = 90;
1402 $pos = str_replace(['i', 'o', 'm'], '', $vpos) .
1403 str_replace(['i', 'o', 'c'], '', $hpos);
1404 switch($pos) {
1405 case 'l' : $angle = 0; break;
1406 case 'tl' : $angle = 45; break;
1407 case 't' : $angle = 90; break;
1408 case 'tr' : $angle = 135; break;
1409 case 'r' : $angle = 180; break;
1410 case 'br' : $angle = 225; break;
1411 case 'b' : $angle = 270; break;
1412 case 'bl' : $angle = 315; break;
1413 }
1414 return $angle;
1415 }
1416
1417 /**
1418 * Builds and returns the body of the graph
1419 */
1420 private function buildGraph()
1421 {
1422 $this->checkValues($this->values);
1423
1424 // body content comes from the subclass
1425 return $this->drawGraph();
1426 }
1427
1428 /**
1429 * Returns the SVG document
1430 */
1431 public function fetch($header = true, $defer_javascript = true)
1432 {
1433 $content = '';
1434 if($header) {
1435 $content .= '<?xml version="1.0"';
1436 // encoding comes before standalone
1437 if(strlen($this->encoding) > 0)
1438 $content .= ' encoding="' . $this->encoding . '"';
1439 $content .= ' standalone="no"?' . ">\n";
1440 if($this->getOption('doctype'))
1441 $content .= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ' .
1442 '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' . "\n";
1443 }
1444
1445 // set the precision - PHP default is 14 digits!
1446 $old_precision = ini_set('precision', $this->settings['precision']);
1447 Number::setup($this->settings['precision'], $this->settings['decimal'],
1448 $this->settings['thousands']);
1449
1450 $heading = $foot = '';
1451 // display title and description if available
1452 if($this->getOption('title'))
1453 $heading .= $this->element('title', null, null, $this->getOption('title'));
1454 if($this->getOption('description'))
1455 $heading .= $this->element('desc', null, null, $this->getOption('description'));
1456
1457 try {
1458 $body = $this->buildGraph();
1459 } catch(\Exception $e) {
1460 if($this->getOption('exception_throw'))
1461 throw $e;
1462
1463 $err = $e->getMessage();
1464 $details = $this->getOption('exception_details');
1465 if($details)
1466 $err .= ' [' . basename($e->getFile()) . ' #' . $e->getLine() . ']';
1467 $body = $this->errorText($err);
1468
1469 if($details) {
1470 $body .= "<!--\nException thrown from " .
1471 $e->getFile() . " @ " . $e->getLine() . "\nTrace: \n" .
1472 $e->getTraceAsString() . "\n-->\n";
1473 }
1474 }
1475
1476 $svg = [
1477 'version' => '1.1',
1478 'width' => new Number($this->width),
1479 'height' => new Number($this->height),
1480 ];
1481
1482 if($this->subgraph) {
1483 // subgraphs need x and y, and can overflow
1484 $x = $this->getOption('graph_x');
1485 $y = $this->getOption('graph_y');
1486 if($x)
1487 $svg['x'] = $x;
1488 if($y)
1489 $svg['y'] = $y;
1490 $svg['overflow'] = 'visible';
1491 } else {
1492 if($this->namespace)
1493 $svg['xmlns:svg'] = 'http://www.w3.org/2000/svg';
1494 else
1495 $svg['xmlns'] = 'http://www.w3.org/2000/svg';
1496 $svg['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
1497
1498 // add any extra namespaces
1499 foreach($this->namespaces as $ns => $url)
1500 $svg['xmlns:' . $ns] = $url;
1501
1502 if($this->getOption('auto_fit')) {
1503 // convert pixel size to viewbox size
1504 $svg['viewBox'] = '0 0 ' . $svg['width'] . ' ' . $svg['height'];
1505 $svg['width'] = $svg['height'] = '100%';
1506 }
1507 }
1508 if($this->getOption('svg_class'))
1509 $svg['class'] = $this->getOption('svg_class');
1510
1511 if(!$defer_javascript)
1512 $foot .= $this->fetchJavascript(true, !$this->namespace);
1513
1514 // add defs to heading
1515 $heading .= $this->defs->get();
1516
1517 // display version string
1518 if($this->getOption('show_version')) {
1519 $text = ['x' => $this->pad_left, 'y' => $this->height - 3];
1520 $style = [
1521 'font-family' => 'Courier New',
1522 'font-size' => '12px',
1523 'font-weight' => 'bold',
1524 ];
1525 $body .= $this->contrastText($text['x'], $text['y'], SVGGraph::VERSION,
1526 'blue', 'white', $style);
1527 }
1528
1529 $content .= $this->element('svg', $svg, null, $heading . $body . $foot);
1530 // replace PHP's precision
1531 ini_set('precision', $old_precision);
1532
1533 if($this->getOption('minify'))
1534 $content = preg_replace('/\>\s+\</', '><', $content);
1535 return $content;
1536 }
1537
1538 /**
1539 * Renders the SVG document
1540 */
1541 public function render($header = true, $content_type = true,
1542 $defer_javascript = false)
1543 {
1544 $mime_header = 'Content-type: image/svg+xml; charset=UTF-8';
1545 if($content_type)
1546 header($mime_header);
1547
1548 try {
1549 echo $this->fetch($header, $defer_javascript);
1550 } catch(\Exception $e) {
1551 if($this->getOption('exception_throw'))
1552 throw $e;
1553 $this->errorText($e);
1554 }
1555 }
1556
1557 /**
1558 * When using the defer_javascript option, this returns the
1559 * Javascript block
1560 */
1561 public function fetchJavascript($cdata = true, $no_namespace = true, $nonce = null)
1562 {
1563 if(!isset(Graph::$javascript))
1564 return '';
1565
1566 $script = Graph::$javascript->getCode($cdata, $this->getOption('minify_js'));
1567 if($script == '')
1568 return '';
1569
1570 $script_attr = ['type' => 'application/ecmascript'];
1571 if($nonce !== null)
1572 $script_attr['nonce'] = $nonce;
1573 $namespace = $this->namespace;
1574 if($no_namespace)
1575 $this->namespace = false;
1576 $js = $this->element('script', $script_attr, null, $script);
1577 if($no_namespace)
1578 $this->namespace = $namespace;
1579 return $js;
1580 }
1581
1582 /**
1583 * Returns the minimum value in the array, ignoring NULLs
1584 */
1585 public static function min(&$a)
1586 {
1587 $min = null;
1588 foreach($a as $v) {
1589 if($v !== null && ($min === null || $v < $min))
1590 $min = $v;
1591 }
1592 return $min;
1593 }
1594
1595 /**
1596 * Converts a string key to a unix timestamp, or NULL if invalid
1597 */
1598 public static function dateConvert($k)
1599 {
1600 // date_create functions return false if the conversion fails
1601 $dt = false;
1602
1603 // if the format is set, try it
1604 if(Graph::$key_format !== null)
1605 $dt = date_create_from_format(Graph::$key_format, $k);
1606
1607 // try default conversion
1608 if($dt === false)
1609 $dt = date_create($k);
1610
1611 // give up
1612 if($dt === false)
1613 return null;
1614
1615 // this works in 64-bit on 32-bit systems, getTimestamp() doesn't
1616 return $dt->format('U');
1617 }
1618 }
1619