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

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

1,612 lines 45.8 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 <graham@goat1000.com>
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 case 'center' : $hpos = $inside ? 'c' : 'oc';
457 break;
458 case 'middle' : $vpos = $inside ? 'm' : 'om';
459 break;
460 default:
461 if(is_numeric($part)) {
462 $offset_x = $part;
463 if(count($parts) && is_numeric($parts[0]))
464 $offset_y = array_shift($parts);
465 }
466 }
467 }
468 return [$hpos, $vpos, $offset_x, $offset_y];
469 }
470
471 /**
472 * Returns [x,y,text-anchor,hpos,vpos] position that is $pos relative to the
473 * top, left, bottom and right.
474 * When $text is true, x and y are adjusted for text-anchor position
475 */
476 public static function relativePosition($pos, $top, $left,
477 $bottom, $right, $width, $height, $pad, $text = false)
478 {
479 list($hpos, $vpos, $offset_x, $offset_y) = Graph::translatePosition($pos);
480
481 // if the containers have no thickness, position outside
482 $translate = ['l' => 'ol', 'r' => 'or', 't' => 'ot', 'b' => 'ob', 'c' => 'oc', 'm' => 'om'];
483 if($top == $bottom && isset($translate[$vpos]))
484 $vpos = $translate[$vpos];
485 if($left == $right && isset($translate[$hpos]))
486 $hpos = $translate[$hpos];
487
488 switch($vpos) {
489 case 'ot' : $y = $top - $height - $pad; break;
490 case 't' : $y = $top + $pad; break;
491 case 'b' : $y = $bottom - $height - $pad; break;
492 case 'ob' : $y = $bottom + $pad; break;
493 case 'om': $y = $top + ($bottom - $top - $height) / 2; break;
494 case 'm' :
495 default :
496 $y = $top + ($bottom - $top - $height) / 2; break;
497 }
498
499 if(($hpos == 'r' || $hpos == 'l') && $right - $left - $pad - $width < 0)
500 $hpos = 'c';
501 switch($hpos) {
502 case 'ol' : $x = $left - $width - $pad; break;
503 case 'l' : $x = $left + $pad; break;
504 case 'r' : $x = $right - $width - $pad; break;
505 case 'or' : $x = $right + $pad; break;
506 case 'oc' : $x = $left + ($right - $left - $width - $pad) / 2; break;
507 case 'c' :
508 default :
509 $x = $left + ($right - $left - $width) / 2; break;
510 }
511
512 $y += $offset_y;
513 $x += $offset_x;
514
515 // third return value is text alignment
516 $align_map = [
517 'ol' => 'end', 'l' => 'start', 'c' => 'middle',
518 'r' => 'end', 'or' => 'start'
519 ];
520 $text_align = $align_map[$hpos];
521
522 // in text mode, adjust X for text alignment
523 if($text && $hpos != 'l' && $hpos != 'or') {
524 if($hpos == 'c')
525 $x += $width / 2;
526 else
527 $x += $width;
528 }
529 return [$x, $y, $text_align, $hpos, $vpos];
530 }
531
532 /**
533 * Sets the style info for the legend
534 */
535 protected function setLegendEntry($dataset, $index, $item, $style_info)
536 {
537 if($this->legend === null)
538 return;
539 $this->legend->setEntry($dataset, $index, $item, $style_info);
540 }
541
542 /**
543 * Subclasses must draw the entry, if they can
544 */
545 protected function drawLegendEntry($x, $y, $w, $h, $entry)
546 {
547 return '';
548 }
549
550 /**
551 * Returns details of title and subtitle
552 */
553 protected function getTitle()
554 {
555 $info = [
556 'title' => $this->getOption('graph_title'),
557 'font' => $this->getOption('graph_title_font'),
558 'font_size' => 0, // 0 font size = no title
559 'sfont_size' => 0, // 0 = no subtitle
560 ];
561
562 $svg_text = new Text($this, $info['font']);
563 if($svg_text->strlen($info['title']) <= 0)
564 return $info;
565
566 $info['font_size'] = Number::units($this->getOption('graph_title_font_size'));
567 $info['weight'] = $this->getOption('graph_title_font_weight');
568 $info['colour'] = $this->getOption('graph_title_colour');
569 $info['pos'] = $this->getOption('graph_title_position');
570 $info['space'] = $this->getOption('graph_title_space');
571 $info['line_spacing'] = Number::units($this->getOption('graph_title_line_spacing'));
572 if($info['line_spacing'] === null || $info['line_spacing'] < 1)
573 $info['line_spacing'] = $info['font_size'];
574
575 if($info['pos'] != 'bottom' && $info['pos'] != 'left' && $info['pos'] != 'right')
576 $info['pos'] = 'top';
577 list($width, $height) = $svg_text->measure($info['title'], $info['font_size'],
578 0, $info['line_spacing']);
579 $info['width'] = $width;
580 $info['height'] = $height;
581
582 // now deal with sub-title
583 $info['stitle'] = $this->getOption('graph_subtitle');
584 $info['sfont'] = $this->getOption('graph_subtitle_font', 'graph_title_font');
585 $svg_subtext = new Text($this, $info['sfont']);
586
587 if($svg_subtext->strlen($info['stitle']) > 0) {
588 $info['sfont_size'] = Number::units($this->getOption('graph_subtitle_font_size',
589 'graph_title_font_size'));
590 $info['sweight'] = $this->getOption('graph_subtitle_font_weight',
591 'graph_title_font_weight');
592 $info['scolour'] = $this->getOption('graph_subtitle_colour',
593 'graph_title_colour');
594 $info['sspace'] = $this->getOption('graph_subtitle_space',
595 'graph_title_space');
596 $info['sline_spacing'] = Number::units($this->getOption('graph_subtitle_line_spacing'));
597 if($info['sline_spacing'] === null || $info['sline_spacing'] < 1)
598 $info['sline_spacing'] = $info['sfont_size'];
599
600 list($swidth, $sheight) = $svg_subtext->measure($info['stitle'],
601 $info['sfont_size'], 0, $info['sline_spacing']);
602 $info['swidth'] = $swidth;
603 $info['sheight'] = $sheight;
604 }
605
606 return $info;
607 }
608
609 /**
610 * Draws the graph title, if there is one
611 */
612 protected function drawTitle()
613 {
614 $info = $this->getTitle();
615 if($info['font_size'] == 0)
616 return '';
617
618 $svg_text = new Text($this, $info['font']);
619 $baseline = $svg_text->baseline($info['font_size']);
620 $text = [
621 'font-size' => $info['font_size'],
622 'font-family' => $info['font'],
623 'font-weight' => $info['weight'],
624 'text-anchor' => 'middle',
625 'fill' => new Colour($this, $info['colour']),
626 ];
627
628 // ensure outside padding is at least the title space
629 $pad_side = 'pad_' . $info['pos'];
630 if($this->{$pad_side} < $info['space'])
631 $this->{$pad_side} = $info['space'];
632
633 $xform = new Transform;
634 switch($info['pos']) {
635 case 'left':
636 $text['x'] = $this->pad_left + $baseline;
637 $text['y'] = $this->height / 2;
638 $xform->rotate(270, $text['x'], $text['y']);
639 $text['transform'] = $xform;
640 break;
641 case 'right':
642 $text['x'] = $this->width - $this->pad_right - $baseline;
643 $text['y'] = $this->height / 2;
644 $xform->rotate(90, $text['x'], $text['y']);
645 $text['transform'] = $xform;
646 break;
647 case 'bottom':
648 $text['x'] = $this->width / 2;
649 $text['y'] = $this->height - $this->pad_bottom - $info['height'] + $baseline;
650 break;
651 default:
652 $text['x'] = $this->width / 2;
653 $text['y'] = $this->pad_top + $baseline;
654 }
655 // increase padding by size of text
656 $this->{$pad_side} += $info['height'] + $info['space'];
657
658 // now deal with sub-title
659 $subtitle_text = '';
660 if($info['sfont_size'] != 0) {
661
662 $svg_subtext = new Text($this, $info['sfont']);
663 $sbaseline = $svg_subtext->baseline($info['sfont_size']);
664 $stext = [
665 'font-size' => $info['sfont_size'],
666 'font-family' => $info['sfont'],
667 'font-weight' => $info['sweight'],
668 'text-anchor' => 'middle',
669 'fill' => new Colour($this, $info['scolour']),
670 ];
671
672 $sxform = new Transform;
673 $sub_offset = $sbaseline + $info['sspace'] - $info['space'];
674 switch($info['pos']) {
675 case 'left':
676 $stext['x'] = $this->pad_left + $sub_offset;
677 $stext['y'] = $text['y'];
678 $sxform->rotate(270, $stext['x'], $stext['y']);
679 $stext['transform'] = $sxform;
680 break;
681 case 'right':
682 $stext['x'] = $this->width - $this->pad_right - $sub_offset;
683 $stext['y'] = $text['y'];
684 $sxform->rotate(90, $stext['x'], $stext['y']);
685 $stext['transform'] = $sxform;
686 break;
687 case 'bottom':
688 // complicates things - need to shift title up
689 $stext['x'] = $text['x'];
690 $stext['y'] = $text['y'] - $baseline + $info['height'] + $sbaseline - $info['sheight'];
691 $text['y'] -= $info['sheight'] + $info['sspace'];
692 break;
693 default:
694 $stext['x'] = $text['x'];
695 $stext['y'] = $this->pad_top + $sub_offset;
696 }
697
698 $this->{$pad_side} += $info['sheight'] + $info['sspace'];
699 $subtitle_text = $svg_subtext->text($info['stitle'], $info['sline_spacing'], $stext);
700 }
701
702 // the Text function will break it into lines
703 $title_text = $svg_text->text($info['title'], $info['line_spacing'], $text);
704
705 return $title_text . $subtitle_text;
706 }
707
708 /**
709 * This should be overridden by subclass!
710 */
711 abstract protected function draw();
712
713 /**
714 * Displays the background image
715 */
716 protected function backgroundImage($shadow, $clip_path)
717 {
718 $image_src = $this->getOption('back_image');
719 if(!$image_src)
720 return '';
721
722 $width = $this->getOption('back_image_width');
723 $height = $this->getOption('back_image_height');
724 $left = $this->getOption('back_image_left');
725 $top = $this->getOption('back_image_top');
726 $mode = $this->getOption('back_image_mode');
727
728 if($shadow) {
729 if($width === '100%')
730 $width = new Number($this->width - $shadow);
731 if($height === '100%')
732 $height = new Number($this->height - $shadow);
733 }
734
735 $image = [
736 'width' => $width, 'height' => $height,
737 'x' => $left, 'y' => $top,
738 'xlink:href' => $image_src,
739 'preserveAspectRatio' =>
740 ($mode == 'stretch' ? 'none' : 'xMinYMin')
741 ];
742
743 if($clip_path !== null)
744 $image['clip-path'] = 'url(#' . $clip_path . ')';
745
746 $style = [];
747 if($this->getOption('back_image_opacity'))
748 $style['opacity'] = $this->getOption('back_image_opacity');
749
750 $contents = '';
751 if($mode == 'tile') {
752 $image['x'] = 0; $image['y'] = 0;
753 $im = $this->element('image', $image, $style);
754 $pattern = [
755 'id' => $this->newID(),
756 'width' => $width, 'height' => $height,
757 'x' => $left, 'y' => $top,
758 'patternUnits' => 'userSpaceOnUse'
759 ];
760 // tiled image becomes a pattern to replace background colour
761 $this->defs->add($this->element('pattern', $pattern, null, $im));
762 $this->setOption('back_colour', 'url(#' . $pattern['id'] . ')');
763 } else {
764 $im = $this->element('image', $image, $style);
765 $contents .= $im;
766 }
767 return $contents;
768 }
769
770 /**
771 * Displays the background
772 */
773 protected function canvas($id)
774 {
775 $round = $this->getOption('back_round');
776 $stroke_width = $this->getOption('back_stroke_width');
777 $stroke_colour = new Colour($this, $this->getOption('back_stroke_colour'));
778 $shadow_opacity = $this->getOption('back_shadow_opacity');
779 $shadow_blur = $this->getOption('back_shadow_blur');
780 $shadow = $this->getOption('back_shadow');
781 if($shadow === true)
782 $shadow = 2;
783
784 // background image can replace the back_colour option
785 $bg = $this->backgroundImage($shadow, $round ? $id : null);
786 $colour = new Colour($this, $this->getOption('back_colour'));
787
788 $canvas = [
789 'width' => '100%', 'height' => '100%',
790 'fill' => $colour,
791 'stroke-width' => 0,
792 ];
793 $c_el = '';
794
795 if($shadow) {
796 // shadow means canvas cannot be 100% of document
797 $canvas['x'] = $canvas['y'] = new Number($stroke_width / 2);
798
799 // blurring means using a filter and a larger offset
800 if($shadow_blur) {
801 $filter_opts = [
802 'offset_x' => $shadow,
803 'offset_y' => $shadow,
804 'blur' => $shadow_blur,
805 'opacity' => $shadow_opacity,
806 'shadow_only' => true,
807 ];
808
809 $shadow += $shadow_blur;
810 $filter = $this->defs->addFilter('shadow', $filter_opts);
811 }
812 $canvas['width'] = new Number($this->width - $shadow - $stroke_width);
813 $canvas['height'] = new Number($this->height - $shadow - $stroke_width);
814
815 $filled = [
816 //'x' => $shadow, 'y' => $shadow,
817 'width' => new Number($this->width - $shadow),
818 'height' => new Number($this->height - $shadow),
819 'fill' => '#000',
820 'stroke-width' => 0,
821 //'opacity' => $shadow_opacity,
822 ];
823
824 if($shadow_blur) {
825 $filled['filter'] = 'url(#' . $filter . ')';
826 } else {
827 $filled['x'] = $shadow;
828 $filled['y'] = $shadow;
829 $filled['opacity'] = $shadow_opacity;
830 }
831
832 if($round)
833 $filled['rx'] = $filled['ry'] = new Number($round);
834
835 $c_el .= $this->element('rect', $filled);
836
837 // increase padding to clear shadow
838 $this->pad_right += $shadow;
839 $this->pad_bottom += $shadow;
840 }
841
842 if($colour->opacity() < 1)
843 $canvas['opacity'] = $colour->opacity(true);
844
845 if($round)
846 $canvas['rx'] = $canvas['ry'] = new Number($round);
847 if($bg == '' && $stroke_width) {
848 $canvas['stroke-width'] = $stroke_width;
849 $canvas['stroke'] = $stroke_colour;
850 }
851 $c_el .= $this->element('rect', $canvas);
852
853 // create a clip path for rounded rectangle
854 if($round) {
855 $this->defs->add($this->element('clipPath', ['id' => $id], null,
856 $this->element('rect', $canvas)));
857 }
858
859 // if the background image is an element, insert it between the background
860 // colour and border rect
861 if($bg != '') {
862 $c_el .= $bg;
863 if($stroke_width) {
864 $canvas['stroke-width'] = $stroke_width;
865 $canvas['stroke'] = $stroke_colour;
866 $canvas['fill'] = 'none';
867 $c_el .= $this->element('rect', $canvas);
868 }
869 }
870
871 return $c_el;
872 }
873
874 /**
875 * Displays readable (hopefully) error message
876 */
877 protected function errorText($error)
878 {
879 if(!is_numeric($this->height))
880 $this->height = 100;
881 $text = ['x' => 3, 'y' => $this->height - 3];
882 $style = [
883 'font-family' => 'Courier New',
884 'font-size' => '11px',
885 'font-weight' => 'bold',
886 ];
887
888 $e = $this->contrastText($text['x'], $text['y'], $error, 'blue',
889 'white', $style);
890 return $e;
891 }
892
893 /**
894 * Displays high-contrast text
895 */
896 protected function contrastText($x, $y, $text, $fcolour = 'black',
897 $bcolour = 'white', $properties = null, $styles = null)
898 {
899 $xform = new Transform;
900 $xform->translate($x, $y);
901 $props = ['transform' => $xform, 'fill' => $fcolour];
902 if(is_array($properties))
903 $props = array_merge($properties, $props);
904
905 $bg = $this->element('text', ['stroke-width' => '2px', 'stroke' => $bcolour],
906 null, $text);
907 $fg = $this->element('text', null, null, $text);
908 return $this->element('g', $props, $styles, $bg . $fg);
909 }
910
911 /**
912 * Builds an element
913 */
914 public function element($name, $attribs = null, $styles = null,
915 $content = null, $no_whitespace = false)
916 {
917 if($this->namespace && strpos($name, ':') === false)
918 $name = 'svg:' . $name;
919 $element = '<' . $name;
920 if(is_array($attribs)) {
921 foreach($attribs as $attr => $val) {
922 $value = new Attribute($attr, $val, $this->encoding);
923 $element .= ' ' . $attr . '="' . $value . '"';
924 }
925 }
926
927 if(is_array($styles)) {
928 $element .= ' style="';
929 foreach($styles as $attr => $val) {
930 $value = new Attribute($attr, $val, $this->encoding);
931 $element .= $attr . ':' . $value . ';';
932 }
933 $element .= '"';
934 }
935
936 if($content === null)
937 $element .= "/>";
938 else
939 $element .= '>' . $content . '</' . $name . ">";
940 if(!$no_whitespace)
941 $element .= "\n";
942
943 return $element;
944 }
945
946 /**
947 * Returns a link URL or NULL if none
948 */
949 public function getLinkURL($item, $key, $row = 0)
950 {
951 $link = ($item === null ? null : $item->link);
952 if(is_numeric($key))
953 $key = (int)round($key);
954 if($link === null && is_array($this->links[$row]) &&
955 isset($this->links[$row][$key])) {
956 $link = $this->links[$row][$key];
957 }
958
959 // check for absolute links
960 if($link !== null && strpos($link,'//') === false)
961 $link = $this->link_base . $link;
962
963 return $link;
964 }
965
966 /**
967 * Retrieves a link
968 */
969 public function getLink($item, $key, $content, $row = 0)
970 {
971 $link = $this->getLinkURL($item, $key, $row);
972 if($link === null)
973 return $content;
974
975 $link_attr = ['xlink:href' => $link];
976 if(!empty($this->link_target))
977 $link_attr['target'] = $this->link_target;
978 return $this->element('a', $link_attr, null, $content);
979 }
980
981 /**
982 * Returns TRUE if the item is visible on the graph
983 */
984 public function isVisible($item, $dataset = 0)
985 {
986 // default implementation is for all non-zero values to be visible
987 return ($item->value != 0);
988 }
989
990 /**
991 * Sets up the colour class
992 */
993 protected function colourSetup($count, $datasets = null, $reverse = false)
994 {
995 $this->colours->setup($count, $datasets, $reverse);
996 }
997
998 /**
999 * Returns a Colour
1000 */
1001 public function getColour($item, $key, $dataset, $allow_gradient = true,
1002 $allow_pattern = true)
1003 {
1004 if($item !== null && $item->colour !== null)
1005 return new Colour($this, $item->colour, $allow_gradient, $allow_pattern);
1006
1007 $c = $this->colours->getColour($key, $dataset);
1008 if($c === null)
1009 return new Colour($this, null);
1010
1011 $colour = new Colour($this, $c, $allow_gradient, $allow_pattern);
1012
1013 // make key reflect dataset as well (for gradients)
1014 if($dataset !== null)
1015 $key = $dataset . ':' . $key;
1016 if($key !== null)
1017 $colour->setGradientKey($key);
1018
1019 return $colour;
1020 }
1021
1022 /**
1023 * Returns the first non-empty option in named argument list.
1024 * Arguments must be "opt_name" or array("opt_name", $index), optionally
1025 * ending with default value (non-string or array('@', $value))
1026 */
1027 public function getOption($opt, $opt2 = null)
1028 {
1029 // single option - checking for null second option is faster
1030 // than using func_num_args()
1031 if($opt2 === null && is_string($opt)) {
1032 if(isset($this->settings[$opt]) && $this->settings[$opt] !== '')
1033 return $this->settings[$opt];
1034 return null;
1035 }
1036
1037 $opts = func_get_args();
1038 foreach($opts as $opt) {
1039 // not string or array, default value
1040 if(!is_array($opt) && !is_string($opt))
1041 return $opt;
1042
1043 if(is_array($opt)) {
1044 // validate
1045 if(!isset($opt[0]) || !isset($opt[1]) || !is_string($opt[0]))
1046 throw new \InvalidArgumentException('Malformed option array');
1047
1048 // default value
1049 if($opt[0] === '@')
1050 return $opt[1];
1051
1052 list($name, $index) = $opt;
1053 if(isset($this->settings[$name])) {
1054 $val = $this->settings[$name];
1055 if(is_array($val)) {
1056
1057 if(isset($val[$index])) {
1058 $val = $val[$index];
1059 } else {
1060 if(!is_numeric($index))
1061 $index = 0;
1062 $val = $val[$index % count($val)];
1063 }
1064 }
1065
1066 if($val !== null && $val !== '')
1067 return $val;
1068 }
1069 continue;
1070 }
1071
1072 // not an array
1073 if(isset($this->settings[$opt])) {
1074 $val = $this->settings[$opt];
1075 if($val !== null && $val !== '')
1076 return $val;
1077 }
1078 }
1079
1080 return null;
1081 }
1082
1083 /**
1084 * Returns option from data item if present, or from settings if not
1085 */
1086 public function getItemOption($option, $dataset, &$item, $item_option = null)
1087 {
1088 if($item_option === null)
1089 $item_option = $option;
1090 $value = null;
1091 if($item !== null)
1092 $value = $item->data($item_option);
1093 if($value === null)
1094 $value = $this->getOption([$option, $dataset]);
1095 return $value;
1096 }
1097
1098 /**
1099 * Option setter
1100 */
1101 public function setOption($name, $value, $index = null)
1102 {
1103 // very simple for now, might have to revisit it later
1104 if($index === null) {
1105 $this->settings[$name] = $value;
1106 return;
1107 }
1108 $this->settings[$name][$index] = $value;
1109 }
1110
1111 /**
1112 * Returns the graph size and padding
1113 */
1114 public function getDimensions()
1115 {
1116 $dimensions = [
1117 'width' => $this->width,
1118 'height' => $this->height,
1119 'pad_left' => $this->pad_left,
1120 'pad_top' => $this->pad_top,
1121 'pad_right' => $this->pad_right,
1122 'pad_bottom' => $this->pad_bottom,
1123 ];
1124 return $dimensions;
1125 }
1126
1127 /**
1128 * Checks that the data are valid
1129 */
1130 protected function checkValues()
1131 {
1132 if($this->values->error)
1133 throw new \Exception($this->values->error);
1134 }
1135
1136 /**
1137 * Sets the stroke options for an element
1138 */
1139 protected function setStroke(&$attr, &$item, $key, $dataset, $line_join = null)
1140 {
1141 unset($attr['stroke'], $attr['stroke-width'], $attr['stroke-linejoin'],
1142 $attr['stroke-dasharray']);
1143
1144 $stroke_width = $this->getItemOption('stroke_width', $dataset, $item);
1145 if($stroke_width > 0) {
1146 $cg = new ColourGroup($this, $item, $key, $dataset);
1147 $attr['stroke'] = $cg->stroke();
1148
1149 if($attr['stroke']->opacity() < 1)
1150 $attr['stroke-opacity'] = $attr['stroke']->opacity(true);
1151 $attr['stroke-width'] = $stroke_width;
1152 if($line_join !== null)
1153 $attr['stroke-linejoin'] = $line_join;
1154
1155 $dash = $this->getItemOption('stroke_dash', $dataset, $item);
1156 if(!empty($dash))
1157 $attr['stroke-dasharray'] = $dash;
1158 }
1159 }
1160
1161 /**
1162 * Creates a new ID for an element
1163 */
1164 public function newID()
1165 {
1166 $prefix = (string)$this->getOption('id_prefix');
1167 $i = ++Graph::$last_id;
1168
1169 // id is case sensitive, so use lower and upper case
1170 $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
1171 $iid = [];
1172 while($i > 0) {
1173 $c = $i % 62;
1174 $i = floor($i / 62);
1175 $iid[] = $chars[$c];
1176 }
1177 return $prefix . 'e' . implode(array_reverse($iid));
1178 }
1179
1180 /**
1181 * Adds markup to be inserted between graph and legend
1182 */
1183 public function addBackMatter($fragment)
1184 {
1185 $this->back_matter .= $fragment;
1186 }
1187
1188 /**
1189 * Adds context menu for item
1190 */
1191 protected function setContextMenu(&$element, $dataset, &$item,
1192 $duplicate = false)
1193 {
1194 if($this->context_menu === null) {
1195 $js = $this->getJavascript();
1196 $this->context_menu = new ContextMenu($this, $js);
1197 }
1198 $this->context_menu->setMenu($element, $dataset, $item, $duplicate);
1199 }
1200
1201 /**
1202 * Default tooltip contents are key and value, or whatever
1203 * $key is if $value is not set
1204 */
1205 protected function setTooltip(&$element, &$item, $dataset, $key, $value = null,
1206 $duplicate = false)
1207 {
1208 $callback = $this->getOption('tooltip_callback');
1209 $structure = $this->getOption('structure');
1210 if(is_callable($callback)) {
1211 if($value === null)
1212 $value = $key;
1213 $text = call_user_func_array($callback, [$dataset, $key, $value]);
1214 } elseif(is_array($structure) && isset($structure['tooltip'])) {
1215 // use structured data tooltips if specified
1216 $text = $item->tooltip;
1217 } else {
1218 $text = $this->formatTooltip($item, $dataset, $key, $value);
1219 }
1220 if($text === null)
1221 return;
1222 $text = addslashes(str_replace("\n", '\n', $text));
1223 return $this->getJavascript()->setTooltip($element, $text, $duplicate);
1224 }
1225
1226 /**
1227 * Default format is value only
1228 */
1229 protected function formatTooltip(&$item, $dataset, $key, $value)
1230 {
1231 $n = new Number($value, $this->getOption('units_tooltip'),
1232 $this->getOption('units_before_tooltip'));
1233 return $n->format();
1234 }
1235
1236 /**
1237 * Adds a data label to the list
1238 */
1239 protected function addDataLabel($dataset, $index, &$element, &$item,
1240 $x, $y, $w, $h, $content = null, $duplicate = true)
1241 {
1242 if(!$this->getOption(['show_data_labels', $dataset]))
1243 return false;
1244
1245 // set up fading for this label?
1246 $id = null;
1247 $fade_in = $this->getOption(['data_label_fade_in_speed', $dataset]);
1248 $fade_out = $this->getOption(['data_label_fade_out_speed', $dataset]);
1249 $click = $this->getOption(['data_label_click', $dataset]);
1250 $popup = $this->getOption(['data_label_popfront', $dataset]);
1251 if($click == 'hide' || $click == 'show') {
1252 $id = $this->newID();
1253 $this->getJavascript()->setClickShow($element, $id, $click == 'hide', $duplicate);
1254 }
1255 if($popup) {
1256 if(!$id)
1257 $id = $this->newID();
1258 $this->getJavascript()->setPopFront($element, $id, $duplicate);
1259 }
1260 if($fade_in || $fade_out) {
1261 $speed_in = $fade_in ? $fade_in / 100 : 0;
1262 $speed_out = $fade_out ? $fade_out / 100 : 0;
1263 if(!$id)
1264 $id = $this->newID();
1265 $this->getJavascript()->setFader($element, $speed_in, $speed_out, $id, $duplicate);
1266 }
1267 $this->getDataLabels()->addLabel($dataset, $index, $item, $x, $y, $w, $h, $id,
1268 $content, $fade_in, $click);
1269 return true;
1270 }
1271
1272 /**
1273 * Adds an element as a client of existing label
1274 */
1275 protected function addLabelClient($dataset, $index, &$element)
1276 {
1277 $label = $this->getDataLabels()->getLabel($dataset, $index);
1278 if($label === null)
1279 return false;
1280
1281 $id = $label['id'];
1282 $fade_in = $this->getOption(['data_label_fade_in_speed', $dataset]);
1283 $fade_out = $this->getOption(['data_label_fade_out_speed', $dataset]);
1284 $click = $this->getOption(['data_label_click', $dataset]);
1285 $popup = $this->getOption(['data_label_popfront', $dataset]);
1286 if($click == 'hide' || $click == 'show')
1287 $this->getJavascript()->setClickShow($element, $id, $click == 'hide', true);
1288 if($popup)
1289 $this->getJavascript()->setPopFront($element, $id, true);
1290 if($fade_in || $fade_out) {
1291 $speed_in = $fade_in ? $fade_in / 100 : 0;
1292 $speed_out = $fade_out ? $fade_out / 100 : 0;
1293 $this->getJavascript()->setFader($element, $speed_in, $speed_out, $id, true);
1294 }
1295 }
1296
1297 /**
1298 * Adds a label for non-data text
1299 */
1300 protected function addContentLabel($dataset, $index, $x, $y, $w, $h, $content)
1301 {
1302 $this->getDataLabels()->addContentLabel($dataset, $index, $x, $y, $w, $h,
1303 $content);
1304 return true;
1305 }
1306
1307 /**
1308 * Draws the data labels
1309 */
1310 protected function drawDataLabels()
1311 {
1312 if(isset($this->settings['label']))
1313 $this->getDataLabels()->load($this->settings);
1314 return $this->getDataLabels()->getLabels();
1315 }
1316
1317 /**
1318 * Returns the position for a data label
1319 */
1320 public function dataLabelPosition($dataset, $index, &$item, $x, $y, $w, $h,
1321 $label_w, $label_h)
1322 {
1323 $pos = $this->getOption(['data_label_position', $dataset]);
1324 if(empty($pos))
1325 $pos = 'above';
1326 $end = [$x + $w * 0.5, $y + $h * 0.5];
1327 return [$pos, $end];
1328 }
1329
1330 /**
1331 * Returns the ShapeList instance
1332 */
1333 public function getShapeList()
1334 {
1335 if($this->shapes === null) {
1336 $this->shapes = new ShapeList($this);
1337 $this->shapes->load($this->settings);
1338 }
1339 return $this->shapes;
1340 }
1341
1342 public function underShapes()
1343 {
1344 if(!isset($this->settings['shape']))
1345 return '';
1346 return $this->getShapeList()->draw(ShapeList::BELOW);
1347 }
1348
1349 public function overShapes()
1350 {
1351 if(!isset($this->settings['shape']))
1352 return '';
1353 return $this->getShapeList()->draw(ShapeList::ABOVE);
1354 }
1355
1356 /**
1357 * Returns TRUE if the position is inside the item
1358 */
1359 public static function isPositionInside($pos)
1360 {
1361 list($hpos, $vpos) = Graph::translatePosition($pos);
1362 return strpos($hpos . $vpos, 'o') === false;
1363 }
1364
1365 /**
1366 * Sets the styles for data labels
1367 */
1368 public function dataLabelStyle($dataset, $index, &$item)
1369 {
1370 // this function gets called a lot, so cache the return values
1371 if(isset($this->data_label_style_cache[$dataset]))
1372 return $this->data_label_style_cache[$dataset];
1373
1374 $map = $this->getDataLabels()->getStyleMap();
1375 $style = [];
1376 foreach($map as $key => $option) {
1377 $style[$key] = $this->getOption([$option, $dataset]);
1378 }
1379
1380 // padding x/y options override single value
1381 $style['pad_x'] = $this->getOption(['data_label_padding_x', $dataset],
1382 ['data_label_padding', $dataset]);
1383 $style['pad_y'] = $this->getOption(['data_label_padding_y', $dataset],
1384 ['data_label_padding', $dataset]);
1385
1386 $this->data_label_style_cache[$dataset] = $style;
1387 return $style;
1388 }
1389
1390 /**
1391 * Tail direction is required for some types of label
1392 */
1393 public function dataLabelTailDirection($dataset, $index, $hpos, $vpos)
1394 {
1395 // angle starts at right, goes clockwise
1396 $angle = 90;
1397 $pos = str_replace(['i', 'o', 'm'], '', $vpos) .
1398 str_replace(['i', 'o', 'c'], '', $hpos);
1399 switch($pos) {
1400 case 'l' : $angle = 0; break;
1401 case 'tl' : $angle = 45; break;
1402 case 't' : $angle = 90; break;
1403 case 'tr' : $angle = 135; break;
1404 case 'r' : $angle = 180; break;
1405 case 'br' : $angle = 225; break;
1406 case 'b' : $angle = 270; break;
1407 case 'bl' : $angle = 315; break;
1408 }
1409 return $angle;
1410 }
1411
1412 /**
1413 * Builds and returns the body of the graph
1414 */
1415 private function buildGraph()
1416 {
1417 $this->checkValues($this->values);
1418
1419 // body content comes from the subclass
1420 return $this->drawGraph();
1421 }
1422
1423 /**
1424 * Returns the SVG document
1425 */
1426 public function fetch($header = true, $defer_javascript = true)
1427 {
1428 $content = '';
1429 if($header) {
1430 $content .= '<?xml version="1.0"';
1431 // encoding comes before standalone
1432 if(strlen($this->encoding) > 0)
1433 $content .= ' encoding="' . $this->encoding . '"';
1434 $content .= ' standalone="no"?' . ">\n";
1435 if($this->getOption('doctype'))
1436 $content .= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ' .
1437 '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' . "\n";
1438 }
1439
1440 // set the precision - PHP default is 14 digits!
1441 $old_precision = ini_set('precision', $this->settings['precision']);
1442 Number::setup($this->settings['precision'], $this->settings['decimal'],
1443 $this->settings['thousands']);
1444
1445 $heading = $foot = '';
1446 // display title and description if available
1447 if($this->getOption('title'))
1448 $heading .= $this->element('title', null, null, $this->getOption('title'));
1449 if($this->getOption('description'))
1450 $heading .= $this->element('desc', null, null, $this->getOption('description'));
1451
1452 try {
1453 $body = $this->buildGraph();
1454 } catch(\Exception $e) {
1455 if($this->getOption('exception_throw'))
1456 throw $e;
1457
1458 $err = $e->getMessage();
1459 $details = $this->getOption('exception_details');
1460 if($details)
1461 $err .= ' [' . basename($e->getFile()) . ' #' . $e->getLine() . ']';
1462 $body = $this->errorText($err);
1463
1464 if($details) {
1465 $body .= "<!--\nException thrown from " .
1466 $e->getFile() . " @ " . $e->getLine() . "\nTrace: \n" .
1467 $e->getTraceAsString() . "\n-->\n";
1468 }
1469 }
1470
1471 $svg = [
1472 'version' => '1.1',
1473 'width' => new Number($this->width),
1474 'height' => new Number($this->height),
1475 ];
1476
1477 if($this->subgraph) {
1478 // subgraphs need x and y, and can overflow
1479 $x = $this->getOption('graph_x');
1480 $y = $this->getOption('graph_y');
1481 if($x)
1482 $svg['x'] = $x;
1483 if($y)
1484 $svg['y'] = $y;
1485 $svg['overflow'] = 'visible';
1486 } else {
1487 if($this->namespace)
1488 $svg['xmlns:svg'] = 'http://www.w3.org/2000/svg';
1489 else
1490 $svg['xmlns'] = 'http://www.w3.org/2000/svg';
1491 $svg['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
1492
1493 // add any extra namespaces
1494 foreach($this->namespaces as $ns => $url)
1495 $svg['xmlns:' . $ns] = $url;
1496
1497 if($this->getOption('auto_fit')) {
1498 // convert pixel size to viewbox size
1499 $svg['viewBox'] = '0 0 ' . $svg['width'] . ' ' . $svg['height'];
1500 $svg['width'] = $svg['height'] = '100%';
1501 }
1502 }
1503 if($this->getOption('svg_class'))
1504 $svg['class'] = $this->getOption('svg_class');
1505
1506 if(!$defer_javascript)
1507 $foot .= $this->fetchJavascript(true, !$this->namespace);
1508
1509 // add defs to heading
1510 $heading .= $this->defs->get();
1511
1512 // display version string
1513 if($this->getOption('show_version')) {
1514 $text = ['x' => $this->pad_left, 'y' => $this->height - 3];
1515 $style = [
1516 'font-family' => 'Courier New',
1517 'font-size' => '12px',
1518 'font-weight' => 'bold',
1519 ];
1520 $body .= $this->contrastText($text['x'], $text['y'], SVGGraph::VERSION,
1521 'blue', 'white', $style);
1522 }
1523
1524 $content .= $this->element('svg', $svg, null, $heading . $body . $foot);
1525 // replace PHP's precision
1526 ini_set('precision', $old_precision);
1527
1528 if($this->getOption('minify'))
1529 $content = preg_replace('/\>\s+\</', '><', $content);
1530 return $content;
1531 }
1532
1533 /**
1534 * Renders the SVG document
1535 */
1536 public function render($header = true, $content_type = true,
1537 $defer_javascript = false)
1538 {
1539 $mime_header = 'Content-type: image/svg+xml; charset=UTF-8';
1540 if($content_type)
1541 header($mime_header);
1542
1543 try {
1544 echo $this->fetch($header, $defer_javascript);
1545 } catch(\Exception $e) {
1546 if($this->getOption('exception_throw'))
1547 throw $e;
1548 $this->errorText($e);
1549 }
1550 }
1551
1552 /**
1553 * When using the defer_javascript option, this returns the
1554 * Javascript block
1555 */
1556 public function fetchJavascript($cdata = true, $no_namespace = true)
1557 {
1558 if(!isset(Graph::$javascript))
1559 return '';
1560
1561 $script = Graph::$javascript->getCode($cdata, $this->getOption('minify_js'));
1562 if($script == '')
1563 return '';
1564
1565 $script_attr = ['type' => 'application/ecmascript'];
1566 $namespace = $this->namespace;
1567 if($no_namespace)
1568 $this->namespace = false;
1569 $js = $this->element('script', $script_attr, null, $script);
1570 if($no_namespace)
1571 $this->namespace = $namespace;
1572 return $js;
1573 }
1574
1575 /**
1576 * Returns the minimum value in the array, ignoring NULLs
1577 */
1578 public static function min(&$a)
1579 {
1580 $min = null;
1581 foreach($a as $v) {
1582 if($v !== null && ($min === null || $v < $min))
1583 $min = $v;
1584 }
1585 return $min;
1586 }
1587
1588 /**
1589 * Converts a string key to a unix timestamp, or NULL if invalid
1590 */
1591 public static function dateConvert($k)
1592 {
1593 // date_create functions return false if the conversion fails
1594 $dt = false;
1595
1596 // if the format is set, try it
1597 if(Graph::$key_format !== null)
1598 $dt = date_create_from_format(Graph::$key_format, $k);
1599
1600 // try default conversion
1601 if($dt === false)
1602 $dt = date_create($k);
1603
1604 // give up
1605 if($dt === false)
1606 return null;
1607
1608 // this works in 64-bit on 32-bit systems, getTimestamp() doesn't
1609 return $dt->format('U');
1610 }
1611 }
1612