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

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

1,614 lines 49.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) 2015-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 class DataLabels {
25
26 protected $graph;
27 protected $have_filters = false;
28 private $labels = [];
29 private $max_values = [];
30 private $min_values = [];
31 private $start_indices = [];
32 private $end_indices = [];
33 private $peak_indices = [];
34 private $trough_indices = [];
35 private $directions = [];
36 private $last = [];
37 private $max_labels = 1000;
38 private $coords = null;
39
40 private $semantic_classes;
41 private $units_before;
42 private $units;
43 private $filter;
44 private $same_size;
45 private $callback;
46
47 /**
48 * Details of each label type
49 */
50 private $types_info = [
51 'box' => ['shape' => 'boxLabel', 'tail' => false, 'pad' => true],
52 'bubble' => ['shape' => 'bubbleLabel', 'tail' => true, 'pad' => true],
53 'circle' => ['shape' => 'circleLabel', 'tail' => false, 'pad' => true],
54 'line' => ['shape' => 'lineLabel', 'tail' => true, 'pad' => false],
55 'line2' => ['shape' => 'lineLabel2', 'tail' => true, 'pad' => true],
56 'linebox' => ['shape' => 'boxLineLabel', 'tail' => true, 'pad' => true],
57 'linecircle' => ['shape' => 'circleLineLabel', 'tail' => true, 'pad' => true],
58 'linesquare' => ['shape' => 'squareLineLabel', 'tail' => true, 'pad' => true],
59 'plain' => ['shape' => null, 'tail' => false, 'pad' => false],
60 'square' => ['shape' => 'squareLabel', 'tail' => false, 'pad' => true],
61 ];
62
63 /**
64 * Mapping between label style array members and options
65 */
66 protected $style_map = [
67 'type' => 'data_label_type',
68 'font' => 'data_label_font',
69 'font_size' => 'data_label_font_size',
70 'font_adjust' => 'data_label_font_adjust',
71 'font_weight' => 'data_label_font_weight',
72 'colour' => 'data_label_colour',
73 'altcolour' => 'data_label_colour_outside',
74 'back_colour' => 'data_label_back_colour',
75 'back_altcolour' => 'data_label_back_colour_outside',
76 'space' => 'data_label_space',
77 'angle' => 'data_label_angle',
78 'pad_x' => 'data_label_padding_x',
79 'pad_y' => 'data_label_padding_y',
80 'round' => 'data_label_round',
81 'stroke' => 'data_label_outline_colour',
82 'stroke_width' => 'data_label_outline_thickness',
83 'fill' => 'data_label_fill',
84 'tail_width' => 'data_label_tail_width',
85 'tail_length' => 'data_label_tail_length',
86 'tail_end' => 'data_label_tail_end',
87 'tail_end_angle' => 'data_label_tail_end_angle',
88 'tail_end_width' => 'data_label_tail_end_width',
89 'shadow_opacity' => 'data_label_shadow_opacity',
90 'opacity' => 'data_label_opacity',
91 'line_spacing' => 'data_label_line_spacing',
92 'align' => 'data_label_align',
93 ];
94
95 /**
96 * Options that are colours, so need translation
97 */
98 protected $colour_options = [
99 'colour', 'altcolour', 'back_colour', 'back_altcolour', 'stroke', 'fill',
100 ];
101
102 function __construct(&$graph)
103 {
104 $this->graph =& $graph;
105 $this->filter = $graph->getOption('data_label_filter');
106 $this->have_filters = (!empty($this->filter) && $this->filter !== 'all');
107
108 $max_labels = $graph->getOption('data_label_max_count');
109 if($max_labels >= 1)
110 $this->max_labels = (int)$max_labels;
111 $this->semantic_classes = $graph->getOption('semantic_classes');
112 $this->units_before = $graph->getOption('units_before_label');
113 $this->units = $graph->getOption('units_label');
114 $this->same_size = $graph->getOption('data_label_same_size');
115 $this->callback = $graph->getOption('data_label_callback');
116 }
117
118 /**
119 * Adds a label to the list
120 */
121 public function addLabel($dataset, $index, &$item, $x, $y, $w, $h,
122 $id = null, $content = null, $fade_in = null, $click = null)
123 {
124 if(!isset($this->labels[$dataset]))
125 $this->labels[$dataset] = [];
126 $this->labels[$dataset][$index] = [
127 'item' => $item, 'id' => $id, 'content' => $content,
128 'x' => $x, 'y' => $y, 'width' => $w, 'height' => $h,
129 'fade' => $fade_in, 'click' => $click,
130 ];
131
132 if($this->have_filters)
133 $this->setupFilters($dataset, $index, $item->value);
134 }
135
136 /**
137 * Adds a content (non-data) label
138 */
139 public function addContentLabel($dataset, $index, $x, $y, $w, $h, $content)
140 {
141 if(!isset($this->labels[$dataset]))
142 $this->labels[$dataset] = [];
143 $this->labels[$dataset][$index] = [
144 'item' => null, 'id' => null, 'content' => $content,
145 'x' => $x, 'y' => $y, 'width' => $w, 'height' => $h,
146 'fade' => null, 'click' => null,
147 ];
148 }
149
150 /**
151 * Adds a user-defined label from a label option
152 */
153 public function addUserLabel($label_array)
154 {
155 if(!isset($this->labels['_user']))
156 $this->labels['_user'] = [];
157
158 if(!isset($label_array[0]) || !isset($label_array[1]) || !isset($label_array[2]))
159 throw new \Exception('Malformed label option - required fields missing');
160
161 $x = $label_array[0];
162 $y = $label_array[1];
163 $content = $label_array[2];
164 $w = 0;
165 $h = 0;
166 // merge the options with required fields
167 $this->labels['_user'][] = array_merge($label_array, [
168 'item' => null, 'id' => null, 'content' => $content,
169 'x' => $x, 'y' => $y, 'width' => $w, 'height' => $h,
170 'fade' => null, 'click' => null,
171 ]);
172 }
173
174 /**
175 * Returns label details
176 */
177 public function getLabel($dataset, $index)
178 {
179 if(isset($this->labels[$dataset][$index]))
180 return $this->labels[$dataset][$index];
181 return null;
182 }
183
184 /**
185 * Updates filter information from label
186 */
187 protected function setupFilters($dataset, $index, $value)
188 {
189 // set up filtering info
190 if(!isset($this->max_values[$dataset]) ||
191 $this->max_values[$dataset] < $value)
192 $this->max_values[$dataset] = $value;
193 if(!isset($this->min_values[$dataset]) ||
194 $this->min_values[$dataset] > $value)
195 $this->min_values[$dataset] = $value;
196 if(!isset($this->start_indices[$dataset]) ||
197 $this->start_indices[$dataset] > $index)
198 $this->start_indices[$dataset] = $index;
199 if(!isset($this->end_indices[$dataset]) ||
200 $this->end_indices[$dataset] < $index)
201 $this->end_indices[$dataset] = $index;
202
203 // peaks and troughs are a bit more complicated
204 if(!isset($this->last[$dataset])) {
205 $this->last[$dataset] = [$index, $value];
206 $this->directions[$dataset] = null;
207 $this->peak_indices[$dataset] = [];
208 $this->trough_indices[$dataset] = [];
209 return;
210 }
211
212 if($this->last[$dataset][1] != $value) {
213 $last = $this->last[$dataset];
214 $diff = $value - $last[1];
215 $direction = ($diff > 0);
216 if($this->directions[$dataset] !== null &&
217 $direction !== $this->directions[$dataset]) {
218 if($diff > 0)
219 $this->trough_indices[$dataset][] = $last[0];
220 else
221 $this->peak_indices[$dataset][] = $last[0];
222 }
223 $this->last[$dataset] = [$index, $value];
224 $this->directions[$dataset] = $direction;
225 }
226 }
227
228
229 /**
230 * Load user-defined labels
231 */
232 public function load(&$settings)
233 {
234 if(!is_array($settings['label']) || !isset($settings['label'][0]))
235 throw new \Exception('Malformed label option');
236
237 if(!is_array($settings['label'][0])) {
238 $this->addUserLabel($settings['label']);
239 $this->coords = new Coords($this->graph);
240 return;
241 }
242 $count = 0;
243 foreach($settings['label'] as $label) {
244 $this->addUserLabel($label);
245 ++$count;
246 }
247 if($count)
248 $this->coords = new Coords($this->graph);
249 }
250
251 /**
252 * Returns all the labels as a string
253 */
254 public function getLabels()
255 {
256 $filter_count = is_array($this->filter) ? count($this->filter) : 1;
257 $label_list = [];
258 foreach($this->labels as $dataset => $label_set) {
259
260 $set_filter = 'all';
261 if(is_numeric($dataset)) {
262 $set_filter = is_array($this->filter) ?
263 $this->filter[$dataset % $filter_count] : $this->filter;
264 }
265 $count = 0;
266 foreach($label_set as $i => $label) {
267 if($this->filter($set_filter, $dataset, $label, $i)) {
268 $content = $this->getLabelText($dataset, $label);
269 if($content !== null && $content != '') {
270
271 list($w, $h) = $this->measureLabel($content, $dataset, $i, $label);
272
273 $label_list[] = compact('content', 'w', 'h', 'dataset', 'i', 'label');
274 if(++$count >= $this->max_labels)
275 break;
276 }
277 }
278 }
279 }
280
281 $this->setLabelSizes($label_list);
282 $labels = '';
283 foreach($label_list as $l) {
284 $labels .= $this->drawLabel($l['content'], $l['w'], $l['h'],
285 $l['dataset'], $l['i'], $l['label']);
286 }
287 if($labels != '') {
288 $group = [];
289 if($this->semantic_classes)
290 $group['class'] = 'data-labels';
291 $labels = $this->graph->element('g', $group, null, $labels);
292 }
293 return $labels;
294 }
295
296 /**
297 * Adjusts the label sizes
298 */
299 protected function setLabelSizes(&$labels)
300 {
301 if(!$this->same_size)
302 return;
303
304 // globally equal sizes
305 if(!is_array($this->same_size)) {
306 $max_w = 0;
307 $max_h = 0;
308 foreach($labels as $l) {
309 if(is_numeric($l['dataset'])) {
310 if($l['w'] > $max_w)
311 $max_w = $l['w'];
312 if($l['h'] > $max_h)
313 $max_h = $l['h'];
314 }
315 }
316
317 foreach($labels as $k => $l) {
318 if(is_numeric($l['dataset'])) {
319 $labels[$k]['w'] = $max_w;
320 $labels[$k]['h'] = $max_h;
321 }
322 }
323 return;
324 }
325
326 // per-dataset maxima (20 datasets should be enough for anybody)
327 $max_w = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
328 $max_h = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
329
330 foreach($labels as $l) {
331 $d = $l['dataset'];
332 if(is_numeric($d)) {
333 if($l['w'] > $max_w[$d])
334 $max_w[$d] = $l['w'];
335 if($l['h'] > $max_h[$d])
336 $max_h[$d] = $l['h'];
337 }
338 }
339
340 foreach($labels as $k => $l) {
341 $d = $l['dataset'];
342 if(is_numeric($d) &&
343 $this->graph->getOption(['data_label_same_size', $d])) {
344 $labels[$k]['w'] = $max_w[$d];
345 $labels[$k]['h'] = $max_h[$d];
346 }
347 }
348 }
349
350 /**
351 * Returns the text for a label
352 */
353 protected function getLabelText($dataset, &$gobject)
354 {
355 if($gobject['item'] === null && empty($gobject['content']))
356 return '';
357
358 if($gobject['item'] === null)
359 return (string)$gobject['content'];
360
361 if(is_callable($this->callback)) {
362 $content = call_user_func($this->callback, $dataset,
363 $gobject['item']->key, $gobject['item']->value);
364 return $content === null ? '' : $content;
365 }
366
367 $content = $gobject['item']->label;
368 if($content !== null)
369 return $content;
370
371 if($gobject['content'] !== null)
372 return $gobject['content'];
373
374 $n = new Number($gobject['item']->value, $this->units, $this->units_before);
375 return $n->format();
376 }
377
378 /**
379 * Returns the style details for a label
380 */
381 protected function getStyle($dataset, $index, &$gobject)
382 {
383 // global styles filled in by graph class
384 $style = $this->graph->dataLabelStyle($dataset, $index, $gobject['item']);
385
386 // structured styles and user defined label styles
387 if($gobject['item'] !== null)
388 $this->itemStyles($style, $gobject['item'], $index, $dataset);
389 elseif($dataset === '_user')
390 $this->userStyles($style, $gobject);
391
392 // deal with fill/fillColour
393 foreach($this->colour_options as $s) {
394 if(isset($style[$s]) && is_string($style[$s]) &&
395 strpos($style[$s], 'fill') !== false) {
396 $cg = new ColourGroup($this->graph, $gobject['item'], $index, $dataset,
397 $style[$s], null, null, true);
398 $style[$s] = $cg->stroke();
399 }
400 }
401 return $style;
402 }
403
404 /**
405 * Returns the font size and line spacing for a style as an array
406 */
407 protected function getFontSize($style)
408 {
409 $font_size = $line_spacing = max(4, Number::units($style['font_size']));
410 if($style['line_spacing'] !== null)
411 $line_spacing = max(1, Number::units($style['line_spacing']));
412 return [$font_size, $line_spacing];
413 }
414
415 /**
416 * Returns size of a label as array (w, h)
417 */
418 protected function measureLabel($content, $dataset, $index, &$gobject)
419 {
420 $style = $this->getStyle($dataset, $index, $gobject);
421
422 // get size of text
423 list($font_size, $line_spacing) = $this->getFontSize($style);
424 $svg_text = new Text($this->graph, $style['font'], $style['font_adjust']);
425 list($w, $h) = $svg_text->measure($content, $font_size, $style['angle'],
426 $line_spacing);
427
428 // if this label type uses padding, add it in
429 if($this->getTypeInfo($style['type'], 'pad')) {
430 $w += $style['pad_x'] * 2;
431 $h += $style['pad_y'] * 2;
432 }
433
434 return [$w, $h];
435 }
436
437 /**
438 * Returns some or all info about a type
439 */
440 protected function getTypeInfo($type, $field = null)
441 {
442 if(!isset($this->types_info[$type]))
443 $type = 'plain';
444 $type_info = $this->types_info[$type];
445 return $field === null ? $type_info : $type_info[$field];
446 }
447
448 /**
449 * Returns the foreground and background colours, dependent on relative
450 * position
451 */
452 protected function getColours($hpos, $vpos, $style)
453 {
454 // if the position is outside, use the alternative colours
455 $colour = new Colour($this->graph, $style['colour']);
456 $back_colour = new Colour($this->graph, $style['back_colour']);
457 if(strpos($hpos . $vpos, 'o') !== false) {
458 $alt = new Colour($this->graph, $style['altcolour']);
459 $back_alt = new Colour($this->graph, $style['back_altcolour']);
460 if(!$alt->isNone())
461 $colour = $alt;
462 if(!$back_alt->isNone())
463 $back_colour = $back_alt;
464 }
465 return [$colour, $back_colour];
466 }
467
468 /**
469 * Draws a label
470 */
471 protected function drawLabel($content, $label_w, $label_h, $dataset, $index,
472 &$gobject)
473 {
474 $style = $this->getStyle($dataset, $index, $gobject);
475 $style['target'] = [$gobject['x'], $gobject['y']];
476
477 $space = (float)$style['space'];
478 $pos = null;
479 if($dataset === '_user') {
480 // user label, so convert coordinates
481 $pos = isset($gobject['position']) ? $gobject['position'] : 'above';
482 $xy = $this->coords->transformCoords($gobject['x'], $gobject['y']);
483 $gobject['x'] = $xy[0];
484 $gobject['y'] = $xy[1];
485 $style['target'] = [$gobject['x'], $gobject['y']];
486 } else {
487 // try to get position from item
488 if($gobject['item'] !== null)
489 $pos = $gobject['item']->data_label_position;
490
491 // find out from graph class where this label should go
492 if($pos === null) {
493 $label_wp = $label_w + $space * 2;
494 $label_hp = $label_h + $space * 2;
495
496 // get the label position and the target for tail
497 list($pos, $target) = $this->graph->dataLabelPosition($dataset,
498 $index, $gobject['item'], $gobject['x'], $gobject['y'],
499 $gobject['width'], $gobject['height'], $label_wp, $label_hp);
500 $style['target'] = $target;
501 }
502 }
503
504 // convert position string to an actual location
505 list($x, $y, $anchor, $hpos, $vpos) = Graph::relativePosition($pos,
506 $gobject['y'], $gobject['x'],
507 $gobject['y'] + $gobject['height'], $gobject['x'] + $gobject['width'],
508 $label_w, $label_h, $space, true);
509
510 list($colour, $back_colour) = $this->getColours($hpos, $vpos, $style);
511 list($font_size, $line_spacing) = $this->getFontSize($style);
512 $text = [
513 'font-family' => $style['font'],
514 'font-size' => $font_size,
515 'fill' => $colour,
516 ];
517
518 $label_markup = '';
519 $label_pad_x = $label_pad_y = 0;
520 $type_info = $this->getTypeInfo($style['type']);
521 if($type_info['pad']) {
522 $label_pad_x = $style['pad_x'];
523 $label_pad_y = $style['pad_y'];
524 }
525
526 // need text size without padding, rotation, etc.
527 $svg_text = new Text($this->graph, $style['font'], $style['font_adjust']);
528 list($tbw, $tbh) = $svg_text->measure($content, $font_size, 0, $line_spacing);
529 $text_baseline = $svg_text->baseline($font_size);
530
531 // allow overriding text alignment
532 $align_map = ['left' => 'start', 'centre' => 'middle', 'right' => 'end'];
533 if($style['align'] !== null && isset($align_map[$style['align']])) {
534 $anchor_new = $align_map[$style['align']];
535 if($anchor_new != $anchor) {
536 // adjust label position for new anchor
537 $pos_remap = [
538 'middle' => ['start' => -0.5, 'end' => 0.5],
539 'start' => ['middle' => 0.5, 'end' => 1.0],
540 'end' => ['start' => -1.0, 'middle' => -0.5],
541 ];
542
543 $x += ($label_w * $pos_remap[$anchor][$anchor_new]);
544 $anchor = $anchor_new;
545 }
546 }
547
548 $text['y'] = $y + ($label_h - $tbh) / 2 + $text_baseline;
549 if($style['angle'] != 0) {
550
551 if($anchor == 'middle') {
552 $text['x'] = $x;
553 } elseif($anchor == 'start') {
554 $text['x'] = $x + ($label_w - $tbw) / 2;
555 } else {
556 $text['x'] = $x - ($label_w - $tbw) / 2;
557 }
558
559 } else {
560
561 if($anchor == 'start') {
562 $text['x'] = $x + $label_pad_x;
563 } elseif($anchor == 'end') {
564 $text['x'] = $x - $label_pad_x;
565 } else {
566 $text['x'] = $x;
567 }
568 }
569
570 // make x right for bounding box
571 if($anchor == 'middle') {
572 $x -= $label_w / 2;
573 } elseif($anchor == 'end') {
574 $x -= $label_w;
575 }
576
577 if($style['angle'] != 0) {
578 // rotate text around centre of box
579 $rx = $x + $label_w / 2;
580 $ry = $y + $label_h / 2;
581 $xform = new Transform;
582 $xform->rotate($style['angle'], $rx, $ry);
583 $text['transform'] = $xform;
584 }
585
586 if($anchor != 'start')
587 $text['text-anchor'] = $anchor;
588 if(!empty($style['font_weight']) && $style['font_weight'] != 'normal')
589 $text['font-weight'] = $style['font_weight'];
590
591 $surround = [];
592 $element = null;
593 $shape_func = null;
594 $need_tail = false;
595
596 if($type_info['shape']) {
597 if($type_info['tail']) {
598 $style['tail_direction'] = $this->graph->dataLabelTailDirection($dataset,
599 $index, $hpos, $vpos);
600 }
601
602 // make the shape
603 $element = $this->{$type_info['shape']}($x, $y, $label_w, $label_h,
604 $style, $surround);
605 if($element) {
606 $surround['stroke'] = new Colour($this->graph, $style['stroke']);
607 if($style['stroke_width'] != 1)
608 $surround['stroke-width'] = (float)$style['stroke_width'];
609
610 // add shadow if not completely transparent
611 if($style['shadow_opacity'] > 0) {
612 $shadow = $surround;
613 $offset = 2 + floor($style['stroke_width'] / 2);
614 $xform = new Transform;
615 $xform->translate($offset, $offset);
616 $shadow['transform'] = $xform;
617 $shadow['fill'] = $shadow['stroke'] = '#000';
618 $shadow['opacity'] = $style['shadow_opacity'];
619 $label_markup .= $this->graph->element($element, $shadow);
620 }
621 $label_markup .= $this->graph->element($element, $surround);
622 }
623 }
624
625 //$back_colour = new Colour($this, $back_colour);
626 if(!$back_colour->isNone()) {
627 $outline = [
628 'stroke-width' => '3px',
629 'stroke' => $back_colour,
630 'stroke-linejoin' => 'round',
631 ];
632 $t1 = array_merge($outline, $text);
633 $label_markup .= $svg_text->text($content, $line_spacing, $t1);
634 }
635 $label_markup .= $svg_text->text($content, $line_spacing, $text);
636
637 $group = [];
638 if(isset($gobject['id']) && $gobject['id'] !== null)
639 $group['id'] = $gobject['id'];
640
641 // opacity is required when set or using click-show-hide
642 $opacity = max(0, min(1, $style['opacity']));
643 if($opacity < 1 || $gobject['click'] == 'show')
644 $group['opacity'] = $opacity;
645 elseif($gobject['click'] == 'hide' || $gobject['fade'])
646 $group['opacity'] = 0;
647
648 $label_markup = $this->graph->element('g', $group, null, $label_markup);
649 return $label_markup;
650 }
651
652 /**
653 * Returns the mapping between style members and option names
654 */
655 public function getStyleMap()
656 {
657 return $this->style_map;
658 }
659
660 /**
661 * Individual label styles from the structured data item
662 */
663 protected function itemStyles(&$style, &$item, $index, $dataset)
664 {
665 // overwrite any style options that the item has set
666 $v = $item->data_label_padding;
667 if($v !== null)
668 $style['pad_x'] = $style['pad_y'] = $v;
669 foreach($this->style_map as $s => $k) {
670 $v = $item->data($k);
671 if($v !== null)
672 $style[$s] = $v;
673 }
674 }
675
676 /**
677 * Styles from the label option
678 */
679 protected function userStyles(&$style, &$label_array)
680 {
681 // pad_x and pad_y will override single padding option
682 if(isset($label_array['padding']))
683 $style['pad_x'] = $style['pad_y'] = $label_array['padding'];
684 foreach($this->style_map as $s => $k) {
685 // remove the 'data_label_' part
686 $o = substr($k, 11);
687 if(isset($label_array[$o]))
688 $style[$s] = $label_array[$o];
689 }
690 }
691
692 /**
693 * Returns TRUE if the label should be shown
694 */
695 protected function filter($filter, $dataset, &$label, $index)
696 {
697 // non-numeric datasets are for additional labels,
698 // empty filter does nothing
699 if(!is_numeric($dataset) || empty($filter))
700 return true;
701
702 $item =& $label['item'];
703
704 // if the item has a show_label member, use it
705 $struct_show = $item->show_label;
706 if($struct_show !== null)
707 return $struct_show;
708
709 // if 'all' is in the list, others don't matter
710 $filters = explode(' ', $filter);
711 if(in_array('all', $filters, true))
712 return true;
713
714 // an array of closures for filter tests
715 $tests = [
716 'start' => function() use ($index, $dataset) {
717 return $index == $this->start_indices[$dataset]; },
718 'end' => function() use ($index, $dataset) {
719 return $index == $this->end_indices[$dataset]; },
720 'max' => function() use (&$item, $dataset) {
721 return $item->value == $this->max_values[$dataset]; },
722 'min' => function() use (&$item, $dataset) {
723 return $item->value == $this->min_values[$dataset]; },
724 'peaks' => function() use ($index, $dataset) {
725 return in_array($index, $this->peak_indices[$dataset], true); },
726 'troughs' => function() use ($index, $dataset) {
727 return in_array($index, $this->trough_indices[$dataset], true); },
728 'nonzero' => function() use (&$item) { return $item->value != 0; },
729 'none' => function() use (&$item) {
730 return $item->label !== null && $item->label !== ''; },
731 ];
732
733 foreach($filters as $f) {
734
735 if(isset($tests[$f]) && $tests[$f]())
736 return true;
737
738 // integer step
739 if(is_numeric($f) && $index % (int)$f == 0) {
740 return true;
741 } else {
742 // step with offset
743 $parts = explode('+', $f);
744 if(count($parts) == 2 &&
745 is_numeric($parts[0]) && is_numeric($parts[1]) &&
746 $parts[0] > 1 && $parts[1] < $parts[0] &&
747 $index % (int)$parts[0] == $parts[1])
748 return true;
749 }
750 }
751
752 // default is to show nothing
753 return false;
754 }
755
756
757 /**
758 * Straight line label style
759 */
760 protected function lineLabel($x, $y, $w, $h, &$style, &$surround)
761 {
762 $w2 = $w / 2;
763 $h2 = $h / 2;
764 $a = $style['tail_direction'] * M_PI / 180;
765
766 if($style['round']) {
767 $bbradius = sqrt($w2 * $w2 + $h2 * $h2);
768 $w2a = $bbradius * cos($a);
769 $h2a = $bbradius * sin($a);
770 } else {
771 // start at edge of text bounding box
772 $w2a = $w2;
773 $h2a = $w2 * tan($a);
774 if(abs($h2a) > $h2) {
775 $h2a = $h2;
776 $w2a = $h2 / tan($a);
777 }
778 }
779 if(($a < M_PI && $h2a < 0) || ($a > M_PI && $h2a > 0)) {
780 $h2a = -$h2a;
781 $w2a = -$w2a;
782 }
783
784 $x1 = $x + $w2 + $w2a;
785 $y1 = $y + $h2 + $h2a;
786 if($style['tail_length'] == 'auto') {
787 list($x2, $y2) = $style['target'];
788 // check line is outside bbox
789 if($style['round']) {
790 $llen = sqrt(pow($x2 - $x - $w2, 2) + pow($y2 - $y - $h2, 2));
791 if($llen < $bbradius)
792 return '';
793 } else {
794 if($x2 > $x && $x2 < $x + $w && $y2 > $y && $y2 < $y + $h)
795 return '';
796 }
797 } else {
798 // make sure line is long enough to not look like part of text
799 list($font_size) = $this->getFontSize($style);
800 $llen = max($font_size, $style['tail_length']);
801 $x2 = $x1 + ($llen * cos($a));
802 $y2 = $y1 + ($llen * sin($a));
803 }
804 $surround['d'] = new PathData('M', $x1, $y1, 'L', $x2, $y2);
805 return 'path';
806 }
807
808 /**
809 * Simple box label style
810 */
811 protected function boxLabel($x, $y, $w, $h, &$style, &$surround)
812 {
813 $surround['x'] = $x;
814 $surround['y'] = $y;
815 $surround['width'] = $w;
816 $surround['height'] = $h;
817 if($style['round'])
818 $surround['rx'] = $surround['ry'] = min((float)$style['round'],
819 $h / 2, $w / 2);
820 $surround['fill'] = new Colour($this->graph, $style['fill']);
821 return 'rect';
822 }
823
824 /**
825 * Speech bubble label style
826 */
827 protected function bubbleLabel($x, $y, $w, $h, &$style, &$surround)
828 {
829 // can't be more round than this!
830 $round = min((float)$style['round'], $h / 3, $w / 3);
831 $drop = max(2, (float)$style['tail_length']);
832 $spread = min(max(2, (float)$style['tail_width']), $w - $round * 2);
833
834 $vert = $h - $round * 2;
835 $horz = $w - $round * 2;
836 $start = new PathData('M', ($x + $w - $round), $y);
837 $t = new PathData('z');
838 $r = new PathData('v', $vert);
839 $b = new PathData('h', -$horz);
840 $l = new PathData('v', -$vert);
841 $tr = new PathData;
842 $br = new PathData;
843 $bl = new PathData;
844 $tl = new PathData;
845 if($round) {
846 $tr->add('a', $round, $round, 90, 0, 1, $round, $round);
847 $br->add('a', $round, $round, 90, 0, 1, -$round, $round);
848 $bl->add('a', $round, $round, 90, 0, 1, -$round, -$round);
849 $tl->add('a', $round, $round, 90, 0, 1, $round, -$round);
850 }
851
852 $direction = floor(($style['tail_direction'] + 22.5) * 8 / 360) % 8;
853 $ddrop = 0.707 * $drop; // cos 45
854 $p1 = $ddrop + $spread * 0.707;
855 $s2 = $spread / 2;
856 $vcropped = $vert - $spread * 0.707 + $round;
857 $hcropped = $horz - $spread * 0.707 + $round;
858 switch($direction) {
859 case 0 :
860 $bside = $h / 2 - $s2 - $round;
861 $r = new PathData('v', $bside, 'l', $drop, $s2, 'l', -$drop, $s2, 'v', $bside);
862 break;
863 case 1 :
864 $r = new PathData('v', $vcropped);
865 $br = new PathData('l', $ddrop, $p1, 'l', -$p1, -$ddrop);
866 $b = new PathData('h', -$hcropped);
867 break;
868 case 2 :
869 $bside = $w / 2 - $s2 - $round;
870 $b = new PathData('h', -$bside, 'l', -$s2, $drop, 'l', -$s2, -$drop, 'h', -$bside);
871 break;
872 case 3 :
873 $l = new PathData('v', -$vcropped);
874 $bl = new PathData('l', -$p1, $ddrop, 'l', $ddrop, -$p1);
875 $b = new PathData('h', -$hcropped);
876 break;
877 case 4 :
878 $bside = $h / 2 - $s2 - $round;
879 $l = new PathData('v', -$bside, 'l', -$drop, -$s2, 'l', $drop, -$s2, 'v', -$bside);
880 break;
881 case 5 :
882 $l = new PathData('v', -$vcropped);
883 $tl = new PathData('l', -$ddrop, -$p1, 'l', $p1, $ddrop);
884 break;
885 case 6 :
886 $bside = $w / 2 - $s2 - $round;
887 $t = new PathData('h', $bside, 'l', $s2, -$drop, 'l', $s2, $drop, 'z');
888 break;
889 case 7 :
890 $start = new PathData('M', ($x + $hcropped + $round), $y);
891 $r = new PathData('v', $vcropped);
892 $tr = new PathData('l', $p1, -$ddrop, 'l', -$ddrop, $p1);
893 break;
894 }
895 $start->add($tr);
896 $start->add($r);
897 $start->add($br);
898 $start->add($b);
899 $start->add($bl);
900 $start->add($l);
901 $start->add($tl);
902 $start->add($t);
903 $surround['d'] = $start;
904 $surround['fill'] = new Colour($this->graph, $style['fill']);
905 return 'path';
906 }
907
908 /**
909 * Returns the cx, cy and radius for a round label
910 */
911 protected function calcRoundLabel($x, $y, $w, $h)
912 {
913 $w2 = $w / 2;
914 $h2 = $h / 2;
915 $r = sqrt($w2 * $w2 + $h2 * $h2);
916 return [$x + $w2, $y + $h2, $r];
917 }
918
919 /**
920 * Circular label style
921 */
922 protected function circleLabel($x, $y, $w, $h, &$style, &$surround)
923 {
924 $params = $this->calcRoundLabel($x, $y, $w, $h);
925 $surround['cx'] = $params[0];
926 $surround['cy'] = $params[1];
927 $surround['r'] = $params[2];
928 $surround['fill'] = new Colour($this->graph, $style['fill']);
929 return 'circle';
930 }
931
932 /**
933 * Returns the tail target coordinates, angle and length for a box label,
934 * or NULL if the tail would end inside the label.
935 * Return value is array(array($x, $y), $angle, $length)
936 */
937 protected function getBoxTailTarget(&$style, $x1, $y1, $x2, $y2)
938 {
939 $target = null;
940 $length = $style['tail_length'];
941 $cx = ($x1 + $x2) / 2;
942 $cy = ($y1 + $y2) / 2;
943 $w2 = $cx - $x1;
944 $h2 = $cy - $y1;
945 if($length == 'auto') {
946 // just use the defined target
947 $target = $style['target'];
948
949 // check that target is outside the label
950 $tx = $target[0]; $ty = $target[1];
951 if($tx >= $x1 && $tx <= $x2 && $ty >= $y1 && $ty <= $y2)
952 return null;
953
954 if($tx > $x2) {
955 $dx = $tx - $x2;
956 } elseif($tx < $x1) {
957 $dx = $x1 - $tx;
958 } else {
959 $dx = abs($tx - $cx);
960 }
961 if($ty > $y2) {
962 $dy = $ty - $y2;
963 } elseif($ty < $y1) {
964 $dy = $y1 - $ty;
965 } else {
966 $dy = abs($ty - $cy);
967 }
968 $length = sqrt($dx * $dx + $dy * $dy);
969 $angle = atan2($ty - $cy, $tx - $cx);
970 } else {
971 // target is radius + tail length away
972 if($length <= 0)
973 return null;
974 $angle = $style['tail_direction'] * M_PI / 180;
975
976 $lx = $length * cos($angle);
977 $ly = $length * sin($angle);
978 // compare tangent with box ratio
979 if(abs(tan($angle)) > $h2 / $w2) {
980 // out top or bottom
981 $target = [
982 $cx + $lx,
983 $ly > 0 ? $y2 + $ly : $y1 + $ly
984 ];
985 } else {
986 // out left or right
987 $target = [
988 $lx > 0 ? $x2 + $lx : $x1 + $lx,
989 $cy + $ly
990 ];
991 }
992 }
993 return [$target, $angle, $length];
994 }
995
996 /**
997 * Returns the tail target coordinates, angle and length for a round label,
998 * or NULL if the tail would end inside the label.
999 * Return value is array(array($x, $y), $angle, $length)
1000 */
1001 protected function getRoundTailTarget(&$style, $cx, $cy, $radius)
1002 {
1003 $target = null;
1004 $length = $style['tail_length'];
1005 if($length == 'auto') {
1006 // just use the defined target
1007 $target = $style['target'];
1008
1009 // check that target is outside the label radius
1010 $tx = $target[0] - $cx;
1011 $ty = $target[1] - $cy;
1012 $rt = sqrt($tx * $tx + $ty * $ty);
1013 if($rt <= $radius)
1014 return null;
1015 $angle = atan2($ty, $tx);
1016 $length = $rt - $radius;
1017 } else {
1018 // target is radius + tail length away
1019 if($length <= 0)
1020 return null;
1021 $len = $radius + $length;
1022 $angle = $style['tail_direction'] * M_PI / 180;
1023 $target = [$cx + ($len * cos($angle)), $cy + ($len * sin($angle))];
1024 }
1025 return [$target, $angle, $length];
1026 }
1027
1028 /**
1029 * Rotate and translate $x and $y, returning Point
1030 */
1031 private static function xForm($x, $y, $a, $tx, $ty)
1032 {
1033 if($x == 0 && $y == 0)
1034 return new Point($tx, $ty);
1035 $sa = sin($a);
1036 $ca = cos($a);
1037 $x1 = $x * $ca - $y * $sa;
1038 $y1 = $x * $sa + $y * $ca;
1039 return new Point($tx + $x1, $ty + $y1);
1040 }
1041
1042 /**
1043 * Returns the tail ending path fragment
1044 */
1045 protected function getTailEnding($x, $y, $langle, $lwidth, $dist, &$style)
1046 {
1047 $a = max(5, min(80, $style['tail_end_angle']));
1048 $ewidth = max($lwidth, $style['tail_end_width']);
1049 $eangle = M_PI * $a / 180;
1050
1051 // first fallback is a tapering line
1052 $fallback = new PathData('L', $x, $y);
1053 $lw = $lwidth * 0.5;
1054 $ew = $ewidth * 0.5;
1055 $ll = $lw / tan($eangle);
1056 $el = $ew / tan($eangle);
1057
1058 // ends are defined pointing upwards
1059 $langle -= M_PI * 0.5;
1060 $type = $style['tail_end'];
1061
1062 // 'point' style by default
1063 $points = [ [-$lw, -$ll], [0, 0], [$lw, -$ll] ];
1064
1065 switch($type)
1066 {
1067 default:
1068 case 'flat' :
1069 $points = [ [-$lw, 0], [$lw, 0] ];
1070 break;
1071 case 'taper' :
1072 return $fallback;
1073 case 'point' :
1074 if($dist <= $ll)
1075 return $fallback;
1076 break;
1077
1078 case 'filled' :
1079 if($dist <= $el)
1080 return $fallback;
1081 if($ew > $lw) {
1082 $points = [
1083 [-$lw, -$el], [-$ew, -$el], [0, 0], [$ew, -$el], [$lw, -$el]
1084 ];
1085 }
1086 break;
1087 case 'arrow' :
1088 $tip_w = $lwidth * sin($eangle);
1089 $w1 = $ew - $tip_w;
1090 $l2 = $el + $lwidth * cos($eangle);
1091 $l1 = $l2 - ($ew - $tip_w - $lw) / tan($eangle);
1092 if($dist < $l2)
1093 return $fallback;
1094 if($w1 > $lw) {
1095 $points = [
1096 [-$lw, -$l1], [-$w1, -$l2], [-$ew, -$el],
1097 [0, 0],
1098 [$ew, -$el], [$w1, -$l2], [$lw, -$l1]
1099 ];
1100 break;
1101 }
1102 // fall through to diamond shape if not wide enough
1103
1104 case 'diamond' :
1105 $blen = 2 * $el - $ll;
1106 if($dist <= $blen)
1107 return $fallback;
1108 if($ew > $lw) {
1109 $points = [
1110 [-$lw, -$blen], [-$ew, -$el], [0,0], [$ew, -$el], [$lw, -$blen]
1111 ];
1112 }
1113 break;
1114 case 'tee' :
1115 if($dist <= $lwidth)
1116 return $fallback;
1117 $points = [
1118 [-$lw, -$lwidth], [-$ew, -$lwidth], [-$ew, 0],
1119 [$ew, 0], [$ew, -$lwidth], [$lw, -$lwidth]
1120 ];
1121 break;
1122 case 'round' :
1123 if($dist < $lw)
1124 return $fallback;
1125 $cradius = min($ew, max($lw, $dist / 2));
1126 $rlen = sqrt(($cradius * $cradius) - ($lw * $lw));
1127 $rdist = $cradius + $rlen;
1128 $p1 = $this->xForm(-$lw, -$rdist, $langle, $x, $y);
1129 $p2 = $this->xForm($lw, -$rdist, $langle, $x, $y);
1130 return new PathData('L', $p1, 'A', $cradius, $cradius, 0, 1, 0, $p2);
1131 }
1132
1133 $path = new PathData;
1134 foreach($points as $pair) {
1135 $pt = $this->xForm($pair[0], $pair[1], $langle, $x, $y);
1136 $path->add('L', $pt);
1137 }
1138 return $path;
1139 }
1140
1141 /**
1142 * Returns the point where a line crosses an arc
1143 */
1144 private function lineCrossArc($x, $y, $angle, $cx, $cy, $radius, $corner)
1145 {
1146 $h = $cx;
1147 $k = $cy;
1148 $r = $radius;
1149
1150 // 90-degree angles are simpler
1151 $cos = abs(cos($angle));
1152 $sin = abs(sin($angle));
1153 if($cos == 1 || $sin == 1) {
1154 if($cos == 1) {
1155 $rt = sqrt(-($y * $y) + (2 * $y * $k) - ($k * $k) + ($r * $r));
1156 $y1 = $y2 = $y;
1157 $x1 = $h - $rt;
1158 $x2 = $h + $rt;
1159 } else {
1160 $rt = sqrt(-($x * $x) + (2 * $x * $h) - ($h * $h) + ($r * $r));
1161 $x1 = $x2 = $x;
1162 $y1 = $k - $rt;
1163 $y2 = $k + $rt;
1164 }
1165 } else {
1166 // y = mx + c
1167 $m = tan($angle);
1168 $c = $y - ($x * $m);
1169
1170 // using quadratic formula
1171 $disc = -($c * $c) - (2 * $c * $h * $m) + (2 * $c * $k)
1172 - ($h * $h * $m * $m) + (2 * $h * $k * $m) - ($k * $k)
1173 + ($m * $m * $r * $r) + ($r * $r);
1174 $rt = sqrt($disc);
1175 $b = (-$c * $m) + $h + ($k * $m);
1176 $d = ($m * $m) + 1;
1177
1178 $x1 = (-$rt + $b) / $d;
1179 $x2 = ($rt + $b) / $d;
1180
1181 // y = mx + c again, using original x and y
1182 $y1 = $m * $x1 + $c;
1183 $y2 = $m * $x2 + $c;
1184 }
1185
1186 $use_first = false;
1187 switch($corner) {
1188 case 'tr' :
1189 if($x1 > $cx && $y1 < $cy)
1190 $use_first = true;
1191 break;
1192 case 'tl' :
1193 if($x1 < $cx && $y1 < $cy)
1194 $use_first = true;
1195 break;
1196 case 'br' :
1197 if($x1 > $cx && $y1 > $cy)
1198 $use_first = true;
1199 break;
1200 case 'bl' :
1201 if($x1 < $cx && $y1 > $cy)
1202 $use_first = true;
1203 }
1204 if($use_first)
1205 return new Point($x1, $y1);
1206 return new Point($x2, $y2);
1207 }
1208
1209 /**
1210 * Filled line label
1211 */
1212 protected function lineLabel2($x, $y, $w, $h, &$style, &$surround)
1213 {
1214 if($style['round']) {
1215 list($cx, $cy, $bbradius) = $this->calcRoundLabel($x, $y, $w, $h);
1216 list($target, $angle, $len) = $this->getRoundTailTarget($style, $cx, $cy,
1217 $bbradius);
1218 } else {
1219 list($target, $angle, $len) = $this->getBoxTailTarget($style, $x, $y, $x + $w,
1220 $y + $h);
1221 }
1222 if($target === null)
1223 return null;
1224
1225 if($style['round']) {
1226 $t_width = max(1, min($style['tail_width'], $bbradius));
1227 $l_angle = asin($t_width * 0.5 / $bbradius);
1228 $p1 = new Point($cx + $bbradius * cos($angle - $l_angle),
1229 $cy + $bbradius * sin($angle - $l_angle));
1230 $p2 = new Point($cx + $bbradius * cos($angle + $l_angle),
1231 $cy + $bbradius * sin($angle + $l_angle));
1232 } else {
1233
1234 $h2 = $h * 0.5; $w2 = $w * 0.5;
1235 $cx = $x + $w2; $cy = $y + $h2;
1236 $t_width = max(1, min((float)$style['tail_width'], $w - 1, $h - 1));
1237 $sin = sin($angle);
1238 $cos = cos($angle);
1239 $xo = $yo = 0;
1240 if(abs($sin) == 1) {
1241 $py = $cy + $h2 * $sin;
1242 $px = $cx;
1243 $xo = $t_width * 0.5;
1244 } elseif(abs($cos) == 1) {
1245 $px = $cx + $w2 * $cos;
1246 $py = $cy;
1247 $yo = $t_width * 0.5;
1248 } else {
1249 $h1 = abs($w2 * tan($angle));
1250 if($h1 >= $h2) {
1251 $h1 = ($sin < 0 ? -$h2 : $h2);
1252 $w1 = $h1 * tan(M_PI * 0.5 - $angle);
1253 } else {
1254 $w1 = ($cos < 0 ? -$w2 : $w2);
1255 $h1 = $w1 / tan(M_PI * 0.5 - $angle);
1256 }
1257 $px = $cx + $w1;
1258 $py = $cy + $h1;
1259 $xo = $t_width * 0.5 * $sin;
1260 $yo = $t_width * -0.5 * $cos;
1261 }
1262
1263 $p1 = new Point($px + $xo, $py + $yo);
1264 $p2 = new Point($px - $xo, $py - $yo);
1265 $l1 = $px - $target[0];
1266 $l2 = $py - $target[1];
1267 $len = sqrt($l1 * $l1 + $l2 * $l2);
1268 }
1269
1270 $surround['fill'] = new Colour($this->graph, $style['fill']);
1271 $path = new PathData('M', $p1, 'L', $p2);
1272 $path->add($this->getTailEnding($target[0], $target[1], $angle, $t_width,
1273 $len, $style));
1274 $path->add('z');
1275 $surround['d'] = $path;
1276 return 'path';
1277 }
1278
1279 /**
1280 * Line and box label style
1281 */
1282 protected function boxLineLabel($x, $y, $w, $h, &$style, &$surround)
1283 {
1284 $x1 = $x; $y1 = $y;
1285 $x2 = $x + $w; $y2 = $y + $h;
1286 list($target, $angle, $len) = $this->getBoxTailTarget($style, $x1, $y1,
1287 $x2, $y2);
1288 if($target === null)
1289 return $this->boxLabel($x, $y, $w, $h, $style, $surround);
1290
1291 $round = min((float)$style['round'], $h / 3, $w / 3);
1292 $t_width = max(1, min((float)$style['tail_width'], $w - 1, $h - 1));
1293 $cx = ($x1 + $x2) / 2;
1294 $cy = ($y1 + $y2) / 2;
1295
1296 while($angle < 0)
1297 $angle += M_PI * 2.0;
1298
1299 // centre points of corner arcs
1300 $x1c = $x1 + $round;
1301 $x2c = $x2 - $round;
1302 $y1c = $y1 + $round;
1303 $y2c = $y2 - $round;
1304
1305 // box corners
1306 if($round) {
1307 $arc = new PathData('a', $round, $round, 0, 0, 0);
1308 $c_tl = new PathData('L', $x1c, $y1);
1309 $c_tl->add($arc);
1310 $c_tl->add(-$round, $round);
1311 $c_tr = new PathData('L', $x2, $y1c);
1312 $c_tr->add($arc);
1313 $c_tr->add(-$round, -$round);
1314 $c_bl = new PathData('L', $x1, $y2c);
1315 $c_bl->add($arc);
1316 $c_bl->add($round, $round);
1317 $c_br = new PathData('L', $x2c, $y2);
1318 $c_br->add($arc);
1319 $c_br->add($round, -$round);
1320 // this gets repeated a lot
1321 $arc = new PathData('A', $round, $round, 0, 0, 0);
1322 } else {
1323 $c_tl = new PathData('L', $x1, $y1);
1324 $c_tr = new PathData('L', $x2, $y1);
1325 $c_bl = new PathData('L', $x1, $y2);
1326 $c_br = new PathData('L', $x2, $y2);
1327 }
1328 $points = [];
1329 $rangle = M_PI * 0.5 - $angle;
1330 if(abs(tan($angle)) > $h / $w) {
1331 // top or bottom
1332 // $hoff = horizontal offset from centre of edge
1333 // $wa = width at angle
1334 $hoff = $h * 0.5 * tan($rangle);
1335 $wa = $t_width * 0.5 / cos($rangle);
1336 if($angle > M_PI) {
1337 // top
1338 $p1 = new Point($cx - $hoff + $wa, $y1);
1339 if($p1->x < $x1) {
1340 $p1->y = $y1 + ($x1 - $p1->x) * tan($angle);
1341 $p1->x = $x1;
1342 }
1343 $p2 = new Point($cx - $hoff - $wa, $y1);
1344 if($p2->x > $x2) {
1345 $p2->y = $y1 - ($p2->x - $x2) * tan($angle);
1346 $p2->x = $x2;
1347 }
1348 $start = new PathData('M', $p1);
1349 $end = new PathData('L', $p2);
1350 // if the line meets the side past the corner radius, there is no corner
1351 if($p1->y > $y1c)
1352 $c_tl->clear();
1353 if($p2->y > $y1c)
1354 $c_tr->clear();
1355 if($round) {
1356 if(!$c_tl->isEmpty() && $p1->x < $x1c) {
1357 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x1c, $y1c, $round, 'tl');
1358 $start = new PathData('M');
1359 $start->add($cross);
1360 $c_tl = new PathData($arc);
1361 $c_tl->add($x1, $y1c);
1362 if($p2->x < $x1c) {
1363 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x1c, $y1c, $round, 'tl');
1364 $end = new PathData('L', $x1c, $y1);
1365 $end->add($arc);
1366 $end->add($cross);
1367 }
1368 }
1369 if(!$c_tr->isEmpty() && $x2c < $p2->x) {
1370 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x2c, $y1c, $round, 'tr');
1371 $end = new PathData($arc);
1372 $end->add($cross);
1373 $c_tr = new PathData('L', $x2, $y1c);
1374 if($x2c < $p1->x) {
1375 $cross = $this->lineCrossArc($p1->x, $p2->y, $angle, $x2c, $y1c, $round, 'tr');
1376 $start = new PathData('M');
1377 $start->add($cross);
1378 $start->add($arc);
1379 $start->add($x2c, $y1);
1380 }
1381 }
1382 }
1383 $points = [$start, $c_tl, $c_bl, $c_br, $c_tr, $end];
1384 $distance = $y1 - $target[1];
1385 } else {
1386 // bottom
1387 $p1 = new Point($cx + $hoff + $wa, $y2);
1388 if($p1->x > $x2) {
1389 $p1->y = $y2 - ($p1->x - $x2) * tan($angle);
1390 $p1->x = $x2;
1391 }
1392 $p2 = new Point($cx + $hoff - $wa, $y2);
1393 if($p2->x < $x1) {
1394 $p2->y = $y2 + ($x1 - $p2->x) * tan($angle);
1395 $p2->x = $x1;
1396 }
1397 $start = new PathData('M', $p1);
1398 $end = new PathData('L', $p2);
1399 if($p1->y < $y2c)
1400 $c_br->clear();
1401 if($p2->y < $y2c)
1402 $c_bl->clear();
1403 if($round) {
1404 if(!$c_bl->isEmpty() && $p2->x < $x1c) {
1405 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x1c, $y2c, $round, 'bl');
1406 $end = new PathData($arc);
1407 $end->add($cross);
1408 $c_bl = new PathData('L', $x1, $y2c);
1409 if($p1->x < $x1c) {
1410 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x1c, $y2c, $round, 'bl');
1411 $start = new PathData('M');
1412 $start->add($cross);
1413 $start->add($arc);
1414 $start->add($x1c, $y2);
1415 }
1416 }
1417 if(!$c_br->isEmpty() && $x2c < $p1->x) {
1418 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x2c, $y2c, $round, 'br');
1419 $start = new PathData('M');
1420 $start->add($cross);
1421 $c_br = new PathData($arc);
1422 $c_br->add($x2, $y2c);
1423 if($x2c < $p2->x) {
1424 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x2c, $y2c, $round, 'br');
1425 $end = new PathData('L', $x2c, $y2);
1426 $end->add($arc);
1427 $end->add($cross);
1428 }
1429 }
1430 }
1431 $points = [$start, $c_br, $c_tr, $c_tl, $c_bl, $end];
1432 $distance = $target[1] - $y2;
1433 }
1434 } else {
1435 // either side
1436 // $voff = vertical offset from centre of side
1437 // $wa = width at angle
1438 $voff = $w * 0.5 * tan($angle);
1439 $wa = $t_width * 0.5 / cos($angle);
1440 if($angle < M_PI * 0.5 || $angle > M_PI * 1.5) {
1441 // right
1442 $p1 = new Point($x2, $cy + $voff - $wa);
1443 if($p1->y < $y1) {
1444 $p1->x = $x2 + ($y1 - $p1->y) * tan($rangle);
1445 $p1->y = $y1;
1446 }
1447 $p2 = new Point($x2, $cy + $voff + $wa);
1448 if($p2->y > $y2) {
1449 $p2->x = $x2 - ($p2->y - $y2) * tan($rangle);
1450 $p2->y = $y2;
1451 }
1452 $start = new PathData('M', $p1);
1453 $end = new PathData('L', $p2);
1454 if($p1->x < $x2c)
1455 $c_tr->clear();
1456 if($p2->x < $x2c)
1457 $c_br->clear();
1458 if($round) {
1459 if(!$c_br->isEmpty() && $y2c < $p2->y) {
1460 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x2c, $y2c, $round, 'br');
1461 $end = new PathData($arc);
1462 $end->add($cross);
1463 $c_br = new PathData('L', $x2c, $y2);
1464 if($y2c < $p1->y) {
1465 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x2c, $y2c, $round, 'br');
1466 $start = new PathData('M');
1467 $start->add($cross);
1468 $start->add($arc);
1469 $start->add($x2, $y2c);
1470 }
1471 }
1472 if(!$c_tr->isEmpty() && $p1->y < $y1c) {
1473 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x2c, $y1c, $round, 'tr');
1474 $start = new PathData('M');
1475 $start->add($cross);
1476 $c_tr = new PathData($arc);
1477 $c_tr->add($x2c, $y1);
1478 if($p2->y < $y1c) {
1479 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x2c, $y1c, $round, 'tr');
1480 $end = new PathData('L', $x2, $y1c);
1481 $end->add($arc);
1482 $end->add($cross);
1483 }
1484 }
1485 }
1486 $points = [$start, $c_tr, $c_tl, $c_bl, $c_br, $end];
1487 $distance = $target[0] - $x2;
1488 } else {
1489 // left
1490 $p1 = new Point($x1, $cy - $voff - $wa);
1491 if($y2 < $p1->y) {
1492 $p1->x = $x1 + ($y2 - $p1->y) * tan($rangle);
1493 $p1->y = $y2;
1494 }
1495 $p2 = new Point($x1, $cy - $voff + $wa);
1496 if($p2->y < $y1) {
1497 $p2->x = $x1 - ($p2->y - $y1) * tan($rangle);
1498 $p2->y = $y1;
1499 }
1500 $start = new PathData('M', $p1);
1501 $end = new PathData('L', $p2);
1502 if($p1->x > $x1c)
1503 $c_bl->clear();
1504 if($p2->x > $x1c)
1505 $c_tl->clear();
1506 if($round) {
1507 if(!$c_bl->isEmpty() && $y2c < $p1->y) {
1508 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x1c, $y2c, $round, 'bl');
1509 $start = new PathData('M');
1510 $start->add($cross);
1511 $c_bl = new PathData($arc);
1512 $c_bl->add($x1c, $y2);
1513 if($y2c < $p2->y) {
1514 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x1c, $y2c, $round, 'bl');
1515 $end = new PathData('L', $x1, $y2c);
1516 $end->add($arc);
1517 $end->add($cross);
1518 }
1519 }
1520 if(!$c_tl->isEmpty() && $p2->y < $y1c) {
1521 $cross = $this->lineCrossArc($p2->x, $p2->y, $angle, $x1c, $y1c, $round, 'tl');
1522 $end = new PathData($arc);
1523 $end->add($cross);
1524 $c_tl = new PathData('L', $x1c, $y1);
1525 if($p1->y < $y1c) {
1526 $cross = $this->lineCrossArc($p1->x, $p1->y, $angle, $x1c, $y1c, $round, 'tl');
1527 $start = new PathData('M');
1528 $start->add($cross);
1529 $start->add($arc);
1530 $start->add($x1, $y1c);
1531 }
1532 }
1533 }
1534 $points = [$start, $c_bl, $c_br, $c_tr, $c_tl, $end];
1535 $distance = $x1 - $target[0];
1536 }
1537 }
1538 $box_path = new PathData;
1539 foreach($points as $pt)
1540 $box_path->add($pt);
1541 $box_path->add($this->getTailEnding($target[0], $target[1], $angle,
1542 $t_width, $distance, $style));
1543 $box_path->add('z');
1544
1545 $surround['fill'] = new Colour($this->graph, $style['fill']);
1546 $surround['d'] = $box_path;
1547 return 'path';
1548 }
1549
1550 /**
1551 * Line and circle label style
1552 */
1553 protected function circleLineLabel($x, $y, $w, $h, &$style, &$surround)
1554 {
1555 list($cx, $cy, $bbradius) = $this->calcRoundLabel($x, $y, $w, $h);
1556 list($target, $angle, $len) = $this->getRoundTailTarget($style, $cx, $cy,
1557 $bbradius);
1558 if($target === null)
1559 return $this->circleLabel($x, $y, $w, $h, $style, $surround);
1560
1561 $t_width = max(1, min($style['tail_width'], $bbradius));
1562 $l_angle = asin($t_width * 0.5 / $bbradius);
1563 $p1x = $cx + $bbradius * cos($angle - $l_angle);
1564 $p1y = $cy + $bbradius * sin($angle - $l_angle);
1565 $p2x = $cx + $bbradius * cos($angle + $l_angle);
1566 $p2y = $cy + $bbradius * sin($angle + $l_angle);
1567
1568 $surround['fill'] = new Colour($this->graph, $style['fill']);
1569 $path = new PathData('M', $p1x, $p1y, 'A', $bbradius, $bbradius, 0, 1, 0,
1570 $p2x, $p2y);
1571 $path->add($this->getTailEnding($target[0], $target[1], $angle, $t_width, $len, $style));
1572 $path->add('z');
1573 $surround['d'] = $path;
1574 return 'path';
1575 }
1576
1577 /**
1578 * Converts a rectangle to a square with same centre point
1579 */
1580 private static function makeSquare(&$x, &$y, &$w, &$h)
1581 {
1582 if($w == $h)
1583 return;
1584 $w2 = $w / 2;
1585 $h2 = $h / 2;
1586 if($w > $h) {
1587 $y -= ($w2 - $h2);
1588 $h += ($w - $h);
1589 return;
1590 }
1591 $x -= ($h2 - $w2);
1592 $w += ($h - $w);
1593 }
1594
1595 /**
1596 * Square label style
1597 */
1598 protected function squareLabel($x, $y, $w, $h, &$style, &$surround)
1599 {
1600 $this->makeSquare($x, $y, $w, $h);
1601 return $this->boxLabel($x, $y, $w, $h, $style, $surround);
1602 }
1603
1604 /**
1605 * Line and square label style
1606 */
1607 protected function squareLineLabel($x, $y, $w, $h, &$style, &$surround)
1608 {
1609 $this->makeSquare($x, $y, $w, $h);
1610 return $this->boxLineLabel($x, $y, $w, $h, $style, $surround);
1611 }
1612 }
1613
1614