PluginProbe
E2Pdf – Export Pdf Tool for WordPress / 1.32.53
E2Pdf – Export Pdf Tool for WordPress v1.32.53
1.32.53 1.32.51 1.32.49 1.32.48 1.32.43 1.32.40 1.32.34 1.32.32 1.32.31 1.32.26 1.32.22 1.32.23 1.32.18 1.32.17 1.32.15 trunk 1.00.00 1.00.13 1.01.01 1.02.02 1.03.07 1.04.07 1.05.03 1.06.02 1.07.11 All 74 releases
e2pdf / vendors / svggraph / DisplayAxis.php

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

1,104 lines 33.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Copyright (C) 2018-2023 Graham Breach
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 /**
19 * For more information, please contact <[email protected]>
20 */
21
22 namespace Goat1000\SVGGraph;
23
24 /**
25 * Converts an abstract axis into SVG markup
26 */
27 class DisplayAxis {
28
29 protected $graph;
30 protected $axis;
31 protected $axis_no;
32 protected $orientation;
33 protected $type;
34 protected $main;
35 protected $styles;
36 protected $show_axis;
37 protected $show_divisions;
38 protected $show_subdivisions;
39 protected $show_text;
40 protected $show_label;
41 protected $label = '';
42 protected $block_label;
43 protected $boxed_text;
44 protected $minimum_subdivision;
45 protected $minimum_units;
46 protected $subdivisions_fixed;
47 protected $offset = [0, 0];
48
49 /**
50 * $orientation = 'h' or 'v'
51 * $type = 'x' or 'y'
52 * $main = TRUE for the main axis
53 * $label_centre = TRUE for graphs with labels between divisions
54 */
55 public function __construct(&$graph, &$axis, $axis_no, $orientation, $type,
56 $main, $label_centre)
57 {
58 $this->axis_no = $axis_no;
59 $this->orientation = $orientation;
60 $this->type = $type;
61 $this->main = $main;
62 $this->block_label = ($type == 'x' && ($label_centre ||
63 $graph->getOption('force_block_label_x')));
64 $this->boxed_text = false;
65 $styles = [];
66
67 // set up options, styles
68 $o = $orientation;
69 $this->show_axis = false;
70 $this->show_text = false;
71 if($graph->getOption('show_axes')) {
72 $this->show_axis = $graph->getOption(['show_axis_' . $o, $axis_no]);
73 $this->show_text = $graph->getOption('show_axis_text_' . $o);
74 }
75
76 // offset caused by padding between axis and grid
77 if($o == 'v') {
78 switch($axis_no) {
79 case 0: $this->offset[0] = -$graph->getOption('axis_pad_left');
80 break;
81 case 1: $this->offset[0] = $graph->getOption('axis_pad_right');
82 break;
83 }
84 } else {
85 switch($axis_no) {
86 case 0: $this->offset[1] = $graph->getOption('axis_pad_bottom');
87 break;
88 case 1: $this->offset[1] = -$graph->getOption('axis_pad_top');
89 break;
90 }
91 }
92
93 // lambda to make retrieving options simpler
94 $get_axis_option = function($option) use ($graph, $o, $axis_no) {
95 return $graph->getOption([$option . '_' . $o, $axis_no], $option);
96 };
97
98 // gridgraph moves label_[xy] into label_[hv]
99 $o_labels = $graph->getOption('label_' . $o);
100 if(is_array($o_labels)) {
101 // use array entry if one exists for this axis
102 if(isset($o_labels[$axis_no]))
103 $this->label = $o_labels[$axis_no];
104 } elseif($axis_no == 0 && !empty($o_labels)) {
105 // not an array, so only valid for axis 0
106 $this->label = $o_labels;
107 }
108 $this->show_label = ($this->label != '');
109
110 // axis and text both need colour
111 $styles['colour'] = new Colour($graph, $get_axis_option('axis_colour'));
112 if($this->show_axis) {
113 $styles['overlap'] = $graph->getOption('axis_overlap');
114 $styles['stroke_width'] = $get_axis_option('axis_stroke_width');
115 if($o == 'v') {
116 $styles['extend'] = [
117 $graph->getOption('axis_extend_top', 'axis_pad_top'),
118 $graph->getOption('axis_extend_bottom', 'axis_pad_bottom'),
119 ];
120 } else {
121 $styles['extend'] = [
122 $graph->getOption('axis_extend_left', 'axis_pad_left'),
123 $graph->getOption('axis_extend_right', 'axis_pad_right'),
124 ];
125 }
126
127 if($graph->getOption('show_divisions')) {
128 $this->show_divisions = true;
129 $styles['d_style'] = $get_axis_option('division_style');
130 $styles['d_size'] = $get_axis_option('division_size');
131 $styles['d_colour'] = new Colour($graph, $graph->getOption(
132 ['division_colour_' . $o, $axis_no], 'division_colour',
133 ['@', $styles['colour']]));
134
135 if($graph->getOption('show_subdivisions')) {
136 $this->show_subdivisions = true;
137 $styles['s_style'] = $get_axis_option('subdivision_style');
138 $styles['s_size'] = $get_axis_option('subdivision_size');
139 $styles['s_colour'] = new Colour($graph, $graph->getOption(
140 ['subdivision_colour_' . $o, $axis_no], 'subdivision_colour',
141 ['division_colour_' . $o, $axis_no], 'division_colour',
142 ['@', $styles['colour']]));
143 $this->minimum_subdivision = $graph->getOption('minimum_subdivision');
144 $this->minimum_units = ($type == 'x' ? 1 :
145 $graph->getOption(['minimum_units_y', $axis_no]));
146 $this->subdivisions_fixed = $graph->getOption(
147 ['subdivision_' . $o, $axis_no]);
148 }
149 }
150 }
151
152 if($this->show_text) {
153 $styles['t_angle'] = $graph->getOption(
154 ['axis_text_angle_' . $o, $axis_no], 0);
155 if($graph->getOption('limit_text_angle')) {
156 $angle = $styles['t_angle'] % 360;
157 if($angle > 180)
158 $angle = -360 + $angle;
159 if($angle < -180)
160 $angle = 360 + $angle;
161 $angle = min(90, max(-90, $angle));
162 $styles['t_angle'] = $angle;
163 }
164 $styles['t_position'] = $get_axis_option('axis_text_position');
165 $styles['t_location'] = $get_axis_option('axis_text_location');
166 $styles['t_font'] = $get_axis_option('axis_font');
167 $styles['t_font_size'] = Number::units($get_axis_option('axis_font_size'));
168 $styles['t_font_adjust'] = $get_axis_option('axis_font_adjust');
169 $styles['t_font_weight'] = $get_axis_option('axis_font_weight');
170 $styles['t_space'] = $get_axis_option('axis_text_space');
171 // BEGIN e2pdf
172 $styles['t_offset_x'] = $get_axis_option('axis_text_offset_x');
173 $styles['t_offset_y'] = $get_axis_option('axis_text_offset_y');
174 // END e2pdf
175 $styles['t_colour'] = new Colour($graph, $graph->getOption(
176 ['axis_text_colour_' . $o, $axis_no], 'axis_text_colour',
177 ['@', $styles['colour']]));
178 $styles['t_line_spacing'] = Number::units($get_axis_option('axis_text_line_spacing'));
179 if($styles['t_line_spacing'] === null || $styles['t_line_spacing'] < 1)
180 $styles['t_line_spacing'] = $styles['t_font_size'];
181 $styles['t_back_colour'] = null;
182 $styles['t_align'] = $get_axis_option('axis_text_align');
183
184 // fill in background colour array, if required
185 $back_colour = $get_axis_option('axis_text_back_colour');
186 if(!empty($back_colour)) {
187 $styles['t_back_colour'] = [
188 'stroke-width' => '3px',
189 'stroke' => new Colour($graph, $back_colour),
190 'stroke-linejoin' => 'round',
191 ];
192 }
193
194 // text is boxed only if it is outside and block labelling
195 if($this->block_label && $this->show_divisions &&
196 ($styles['d_style'] == 'box' || $styles['d_style'] == 'extend') &&
197 $styles['t_position'] != 'inside') {
198 $this->boxed_text = true;
199
200 // text must be attached to axis, not grid
201 $styles['t_location'] = 'axis';
202 }
203 }
204
205 if($this->show_label) {
206 $styles['l_font'] = $graph->getOption(
207 ['label_font_' . $o, $axis_no], 'label_font',
208 ['axis_font_' . $o, $axis_no], 'axis_font');
209 $styles['l_font_size'] = Number::units($graph->getOption(
210 ['label_font_size_' . $o, $axis_no], 'label_font_size',
211 ['axis_font_size_' . $o, $axis_no], 'axis_font_size'));
212 $styles['l_font_weight'] = $graph->getOption(
213 ['label_font_weight_' . $o, $axis_no], 'label_font_weight');
214 $styles['l_colour'] = new Colour($graph, $graph->getOption(
215 ['label_colour_' . $o, $axis_no], 'label_colour',
216 ['axis_text_colour_' . $o, $axis_no], 'axis_text_colour',
217 ['@', $styles['colour']]));
218 $styles['l_space'] = $graph->getOption('label_space');
219 $styles['l_pos'] = $get_axis_option('axis_label_position');
220 $styles['l_line_spacing'] = Number::units($get_axis_option('label_line_spacing'));
221 if($styles['l_line_spacing'] === null || $styles['l_line_spacing'] < 1)
222 $styles['l_line_spacing'] = $styles['l_font_size'];
223 }
224
225 $this->styles = $styles;
226 $this->axis =& $axis;
227 $this->graph =& $graph;
228 }
229
230 /**
231 * Returns the array of style information for the axis
232 */
233 public function getStyling()
234 {
235 return $this->styles;
236 }
237
238 /**
239 * Returns the extents of the axis, relative to where it will be drawn from
240 * returns BoundingBox
241 */
242 public function measure($with_label = true)
243 {
244 $bbox = new BoundingBox(0, 0, 0, 0);
245 if($this->show_axis) {
246 $dbox = $this->getDivisionsBBox(0);
247 if($this->orientation == 'h')
248 $dbox->flipY();
249 $bbox->growBox($dbox);
250 }
251
252 if($this->show_text) {
253 $tbox = $this->getTextBBox(0);
254 $bbox->growBox($tbox);
255 }
256
257 if($with_label && $this->show_label) {
258 $lpos = $this->getLabelPosition();
259 $bbox->grow($lpos['x'], $lpos['y'], $lpos['x'] + $lpos['width'],
260 $lpos['y'] + $lpos['height']);
261 }
262 $bbox = $this->addOffset($bbox);
263
264 return $bbox;
265 }
266
267 /**
268 * Adds the padding offset to the bounding box
269 */
270 protected function addOffset(BoundingBox $bbox)
271 {
272 if($this->axis_no > 0) {
273 $bbox->x2 += $this->offset[0];
274 $bbox->y1 += $this->offset[1];
275 } else {
276 $bbox->x1 += $this->offset[0];
277 $bbox->y2 += $this->offset[1];
278 }
279 return $bbox;
280 }
281
282 /**
283 * Measures the space taken up by axis and divisions
284 * returns BoundingBox
285 */
286 protected function getDivisionsBBox($level)
287 {
288 $bbox = new BoundingBox(0, 0, 0, 0);
289 if(!$this->show_axis)
290 return $bbox;
291
292 // orientation more important than type
293 $x = 'x';
294 $y = 'y';
295 if($this->orientation == 'h') {
296 $x = 'y';
297 $y = 'x';
298 }
299 $min = $max = ['x' => 0, 'y' => 0];
300
301 $length = $this->axis->getLength();
302 $d_info = $s_info = ['pos' => 0, 'sz' => 0];
303 if($this->show_divisions) {
304 $di = $this->getDivisionPathInfo(false, 100, 100, $level);
305 if($di !== null)
306 $d_info = $di;
307 if($this->show_subdivisions) {
308 $si = $this->getDivisionPathInfo(true, 100, 100, $level);
309 if($si !== null)
310 $s_info = $si;
311 }
312 }
313
314 $points = $this->axis->getGridPoints(0);
315 $p1 = end($points);
316 if($p1->position < 0) {
317 $min[$y] = $p1->position;
318 $max[$y] = 0;
319 } else {
320 $min[$y] = 0;
321 $max[$y] = $p1->position;
322 }
323
324 $min[$x] = min($d_info['pos'], $s_info['pos']);
325 $max[$x] = max($d_info['pos'] + $d_info['sz'], $s_info['pos'] + $s_info['sz']);
326
327 $bbox->grow($min['x'], $min['y'], $max['x'], $max['y']);
328 return $bbox;
329 }
330
331 /**
332 * Returns the bounding box of the text
333 */
334 protected function getTextBBox($level)
335 {
336 $bbox = new BoundingBox(0, 0, 0, 0);
337 list($x_off, $y_off, $opp) = $this->getTextOffset(0, 0, 0, 0, 0, 0, $level);
338
339 $t_offset = ($this->orientation == 'h' ? $x_off : $y_off);
340 if($this->axis->reversed())
341 $t_offset = -$t_offset;
342
343 $length = $this->axis->getLength();
344 $points = $this->axis->getGridPoints(0);
345 $positions = $this->getTextPositions(0, 0, $x_off, $y_off, $points, null);
346 $count = count($positions);
347 for($p = 0; $p < $count; ++$p) {
348
349 if(!$this->pointTextVisible($points[$p], $length, $t_offset))
350 continue;
351
352 $pos = $positions[$p];
353 $lbl = $this->measureText($pos['x'], $pos['y'], $points[$p], $opp, $level);
354 $bbox->grow($lbl['x'], $lbl['y'], $lbl['x'] + $lbl['width'],
355 $lbl['y'] + $lbl['height']);
356 }
357 return $bbox;
358 }
359
360 /**
361 * Returns the overlap between axis text labels
362 */
363 public function getTextOverlap()
364 {
365 // start with obviously good overlap
366 $overlap = -1000;
367 $prev_x = -10;
368 $level = 0;
369
370 // no overlap if there is no text
371 if(!$this->show_text)
372 return null;
373
374 list($x_off, $y_off, $opp) = $this->getTextOffset(0, 0, 0, 0, 0, 0, $level);
375 $t_offset = ($this->orientation == 'h' ? $x_off : $y_off);
376 if($this->axis->reversed())
377 $t_offset = -$t_offset;
378
379 $length = $this->axis->getLength();
380 $points = $this->axis->getGridPoints(0);
381 $positions = $this->getTextPositions(0, 0, $x_off, $y_off, $points, null);
382 $count = count($positions);
383
384 for($p = 0; $p < $count; ++$p) {
385 if(!$this->pointTextVisible($points[$p], $length, $t_offset))
386 continue;
387
388 $pos = $positions[$p];
389 $lbl = $this->measureText($pos['x'], $pos['y'], $points[$p], $opp, $level);
390 $o = $prev_x - $lbl['x'];
391
392 // bail out now if the overlap is positive
393 if($o > 0)
394 return $o;
395 if($o > $overlap)
396 $overlap = $o;
397 $prev_x = $lbl['x'] + $lbl['width'];
398 }
399 return $overlap;
400 }
401
402 /**
403 * Returns true if the text exists and its location is within the axis
404 */
405 protected function pointTextVisible($point, $axis_len, $offset)
406 {
407 if($point->blank())
408 return false;
409
410 // amount of space to allow for rounding errors
411 $leeway = 0.5;
412 $position = abs($point->position) + $offset;
413 return $position >= -$leeway && $position <= $axis_len + $leeway;
414 }
415
416 /**
417 * Returns the positions (actually offsets) of all the text blocks
418 */
419 protected function getTextPositions($x, $y, $xoff, $yoff, $points, $anchor)
420 {
421 $positions = [];
422 // vertical axis a bit simpler
423 if($this->orientation == 'v') {
424 foreach($points as $k => $p) {
425 if($this->boxed_text && isset($points[$k + 1])) {
426 $pnext = $points[$k + 1];
427 $yoff = ($pnext->position - $p->position) / 2;
428 }
429 // e2pdf
430 $positions[] = ['x' => $x + $xoff + $this->styles['t_offset_x'], 'y' => $y + $yoff + $this->styles['t_offset_y']];
431 }
432 return $positions;
433 }
434
435 foreach($points as $k => $p) {
436 if($anchor == 'start') {
437 $xoff = $this->styles['t_space'];
438 } elseif($anchor == 'end') {
439 $pnext = $points[$k + 1];
440 $xoff = $pnext->position - $p->position - $this->styles['t_space'];
441 } elseif($this->boxed_text && isset($points[$k + 1])) {
442 $pnext = $points[$k + 1];
443 $xoff = ($pnext->position - $p->position) / 2;
444 }
445 // e2pdf
446 $positions[] = ['x' => $x + $xoff + $this->styles['t_offset_x'], 'y' => $y + $yoff + $this->styles['t_offset_y']];
447 }
448 return $positions;
449 }
450
451 /**
452 * Draws the axis at ($x,$y)
453 * $gx, $gy, $g_width and $g_height are the grid dimensions
454 */
455 public function draw($x, $y, $gx, $gy, $g_width, $g_height)
456 {
457 $content = '';
458 $x += $this->offset[0];
459 $y += $this->offset[1];
460 $gx += $this->offset[0];
461 $gy += $this->offset[1];
462 if($this->show_axis) {
463 if($this->show_divisions) {
464 $content .= $this->drawDivisions($x, $y, $g_width, $g_height);
465 if($this->show_subdivisions) {
466 $content .= $this->drawSubDivisions($x, $y, $g_width, $g_height);
467 }
468 }
469
470 $length = $this->axis->getLength();
471 $content .= $this->drawAxisLine($x, $y, $length);
472 }
473
474 if($this->show_text || $this->show_label)
475 $content .= $this->drawText($x, $y, $gx, $gy, $g_width, $g_height);
476
477 return $content;
478 }
479
480 /**
481 * Draws the axis line
482 */
483 public function drawAxisLine($x, $y, $len)
484 {
485 $overlap = $this->styles['overlap'];
486 $length = $len + $overlap * 2;
487 $line = $this->orientation; // 'h' and 'v' are SVG path lines
488 $reversed = $this->axis->reversed();
489 if($this->orientation == 'h') {
490 $x = $reversed ? $x - $overlap - $len : $x - $overlap;
491 $x -= $this->styles['extend'][0];
492 $length += $this->styles['extend'][0] + $this->styles['extend'][1];
493 } else {
494 $y = $reversed ? $y - $overlap - $len : $y - $overlap;
495 $y -= $this->styles['extend'][0];
496 $length += $this->styles['extend'][0] + $this->styles['extend'][1];
497 }
498
499 $colour = $this->styles['colour'];
500 if($colour->isGradient()) {
501 // gradients don't work on stroked horizontal or vertical lines
502 $sw = $this->styles['stroke_width'];
503 $attr = [
504 'fill' => $colour,
505 'x' => $x,
506 'y' => $y,
507 ];
508 if($this->orientation == 'h') {
509 $attr['y'] -= $sw / 2;
510 $attr['width'] = $length;
511 $attr['height'] = $sw;
512 } else {
513 $attr['x'] -= $sw / 2;
514 $attr['width'] = $sw;
515 $attr['height'] = $length;
516 }
517 return $this->graph->element('rect', $attr);
518 }
519
520 $attr = [
521 'stroke' => $colour,
522 'd' => new PathData('M', $x, $y, $line, $length),
523 ];
524
525 if($this->styles['stroke_width'] != 1)
526 $attr['stroke-width'] = $this->styles['stroke_width'];
527 return $this->graph->element('path', $attr);
528 }
529
530 /**
531 * Draws the axis divisions
532 */
533 public function drawDivisions($x, $y, $g_width, $g_height)
534 {
535 $path_info = $this->getDivisionPathInfo(false, $g_width, $g_height, 0);
536 if($path_info === null)
537 return '';
538
539 $points = $this->axis->getGridPoints(0);
540 $d = $this->getDivisionPath($x, $y, $points, $path_info, 0);
541
542 $attr = [
543 'd' => $d,
544 'stroke' => $this->styles['d_colour'],
545 'fill' => 'none',
546 ];
547 return $this->graph->element('path', $attr);
548 }
549
550 /**
551 * Draws the axis subdivisions
552 */
553 public function drawSubDivisions($x, $y, $g_width, $g_height)
554 {
555 $path_info = $this->getDivisionPathInfo(true, $g_width, $g_height, 0);
556 if($path_info === null)
557 return '';
558
559 $points = $this->axis->getGridSubdivisions($this->minimum_subdivision,
560 $this->minimum_units, 0, $this->subdivisions_fixed);
561 $d = $this->getDivisionPath($x, $y, $points, $path_info, 0);
562 $attr = [
563 'd' => $d,
564 'stroke' => $this->styles['s_colour'],
565 'fill' => 'none',
566 ];
567 return $this->graph->element('path', $attr);
568 }
569
570 /**
571 * Draws the axis text labels
572 */
573 public function drawText($x, $y, $gx, $gy, $g_width, $g_height)
574 {
575 $labels = '';
576 if($this->show_text) {
577 list($x_offset, $y_offset, $opposite) = $this->getTextOffset($x, $y,
578 $gx, $gy, $g_width, $g_height, 0);
579
580 $t_offset = ($this->orientation == 'h' ? $x_offset : $y_offset);
581 if($this->axis->reversed())
582 $t_offset = -$t_offset;
583
584 $length = $this->axis->getLength();
585 $points = $this->axis->getGridPoints(0);
586 $anchor = null;
587 $space = $this->styles['t_space'];
588 if($this->styles['t_align']) {
589 $bbox = $this->measure(false);
590 switch($this->styles['t_align']) {
591 case 'left':
592 if($this->orientation == 'v') {
593 if(!$opposite) {
594 $anchor = 'start';
595 $x_offset = $bbox->x1 + $space;
596 $x_offset -= $this->offset[0];
597 }
598 } else {
599 $anchor = 'start';
600 $x_offset = $space;
601 }
602 break;
603 case 'right':
604 if($this->orientation == 'v') {
605 if($opposite) {
606 $anchor = 'end';
607 $x_offset = $bbox->x2 - $space;
608 }
609 } else {
610 $anchor = 'end';
611 $x_offset = -$space;
612 }
613 break;
614 case 'centre':
615 if($this->orientation == 'v') {
616 $x_offset = ($x_offset + ($opposite ? $bbox->x2 : $bbox->x1)) / 2;
617 $anchor = 'middle';
618 }
619 }
620 }
621
622 $positions = $this->getTextPositions($x, $y, $x_offset, $y_offset, $points, $anchor);
623 $count = count($positions);
624 for($p = 0; $p < $count; ++$p) {
625
626 if(!$this->pointTextVisible($points[$p], $length, $t_offset))
627 continue;
628
629 $pos = $positions[$p];
630 $labels .= $this->getText($pos['x'], $pos['y'], $points[$p], $opposite, 0, $anchor);
631 }
632 if($labels != '') {
633 $group = [
634 'font-family' => $this->styles['t_font'],
635 'font-size' => $this->styles['t_font_size'],
636 'fill' => $this->styles['t_colour'],
637 ];
638 $weight = $this->styles['t_font_weight'];
639 if($weight != 'normal' && $weight !== null)
640 $group['font-weight'] = $weight;
641
642 $labels = $this->graph->element('g', $group, null, $labels);
643 }
644 }
645 if($this->show_label)
646 $labels .= $this->getLabel($x, $y, $gx, $gy, $g_width, $g_height);
647
648 return $labels;
649 }
650
651 /**
652 * Returns the label
653 */
654 protected function getLabel($x, $y, $gx, $gy, $g_width, $g_height)
655 {
656 $pos = $this->getLabelPosition();
657 $offset = $this->getLabelOffset($x, $y, $gx, $gy, $g_width, $g_height);
658 $tx = $offset['x'] + $pos['tx'];
659 $ty = $offset['y'] + $pos['ty'];
660 $label = [
661 'text-anchor' => 'middle',
662 'font-family' => $this->styles['l_font'],
663 'font-size' => $this->styles['l_font_size'],
664 'fill' => $this->styles['l_colour'],
665 'x' => $tx,
666 'y' => $ty,
667 ];
668 if(!empty($this->styles['l_font_weight']) &&
669 $this->styles['l_font_weight'] != 'normal')
670 $label['font-weight'] = $this->styles['l_font_weight'];
671 if($pos['angle']) {
672 $xform = new Transform;
673 $xform->rotate($pos['angle'], $tx, $ty);
674 $label['transform'] = $xform;
675 }
676
677 $svg_text = new Text($this->graph, $this->styles['l_font']);
678 return $svg_text->text($this->label, $this->styles['l_line_spacing'], $label);
679 }
680
681 /**
682 * Returns the corrected offset for the label
683 */
684 protected function getLabelOffset($x, $y, $gx, $gy, $g_width, $g_height)
685 {
686 if($this->orientation == 'h') {
687
688 // label at bottom of grid?
689 if($this->axis_no == 0)
690 $y = $gy + $g_height;
691
692 } else {
693
694 // leftmost axis label must be outside grid
695 if($this->axis_no == 0)
696 $x = $gx;
697 }
698 return ['x' => $x, 'y' => $y];
699 }
700
701 /**
702 * Returns the dimensions of the label
703 * x, y, width, height = position and size
704 * tx, tx = text anchor point
705 * angle = text angle
706 */
707 protected function getLabelPosition()
708 {
709 $bbox = $this->measure(false);
710 $font_size = $this->styles['l_font_size'];
711 $line_spacing = $this->styles['l_line_spacing'];
712 $svg_text = new Text($this->graph, $this->styles['l_font']);
713 $tsize = $svg_text->measure($this->label, $font_size, 0, $line_spacing);
714 $baseline = $svg_text->baseline($font_size);
715 $a_length = $this->axis->getLength();
716 $space = $this->styles['l_space'];
717 $align = $this->styles['l_pos'];
718
719 if($this->orientation == 'h') {
720 $width = $tsize[0];
721 $height = $tsize[1];
722 if($this->axis_no > 0) {
723 $y = $bbox->y1 - $space - $height;
724 } else {
725 $y = $bbox->y2 + $space;
726 }
727 $y -= $this->offset[1]; // handle padding offset
728 if(is_numeric($align)) {
729 $tx = $a_length * $align;
730 } else {
731 switch($align) {
732 case 'left' :
733 $tx = $width / 2;
734 break;
735 case 'right':
736 $tx = $a_length - ($width / 2);
737 break;
738 default:
739 $tx = $a_length / 2;
740 break;
741 }
742 }
743 $ty = $y + $baseline;
744 $x = $tx - $width / 2;
745
746 $height += $space;
747 $angle = 0;
748 } else {
749 $width = $tsize[1];
750 $height = $tsize[0];
751 $reversed = $this->axis->reversed();
752 if($this->axis_no > 0) {
753 $x = $bbox->x2 + $space;
754 $x -= $this->offset[0]; // handle padding offset
755 $tx = $x + $width - $baseline;
756 } else {
757 $x = $bbox->x1 - $space - $width;
758 $x -= $this->offset[0]; // handle padding offset
759 $tx = $x + $baseline;
760 $x -= $space;
761 }
762 if(is_numeric($align)) {
763 $ty = $reversed ? -$a_length * $align : $a_length * $align;
764 } else {
765 switch($align) {
766 case 'top' :
767 $ty = $reversed ? -$a_length + $height / 2 : $height / 2;
768 break;
769 case 'bottom':
770 $ty = $reversed ? -$height / 2 : $a_length - $height / 2;
771 break;
772 default:
773 $ty = $reversed ? -$a_length / 2 : $a_length / 2;
774 break;
775 }
776 }
777 $y = $ty - $height / 2;
778 $width += $space;
779 $angle = $this->axis_no > 0 ? 90 : 270;
780 }
781
782 return compact('x', 'y', 'width', 'height', 'tx', 'ty', 'angle');
783 }
784
785 /**
786 * Returns the distance from the axis to draw the text
787 */
788 protected function getTextOffset($ax, $ay, $gx, $gy, $g_width, $g_height,
789 $level)
790 {
791 $d1 = $d2 = 0;
792 if($this->show_divisions && $level == 0) {
793 if(!$this->boxed_text) {
794 $d_info = $this->getDivisionPathInfo(false, 100, 100, 0);
795 if($d_info !== null) {
796 $d1 = $d_info['pos'];
797 $d2 = $d1 + $d_info['sz'];
798 }
799 }
800 if($this->show_subdivisions) {
801 $s_info = $this->getDivisionPathInfo(true, 100, 100, 0);
802 if($s_info !== null) {
803 $s1 = $s_info['pos'];
804 $s2 = $s1 + $s_info['sz'];
805 $d1 = min($d1, $s1);
806 $d2 = max($d2, $s2);
807 }
808 }
809 }
810 $space = $this->styles['t_space'];
811 $d1 -= $space;
812 $d2 += $space;
813 $opposite = ($this->styles['t_position'] == 'inside');
814 $reversed = $this->axis->reversed();
815 $block_offset = $this->axis->unit() * ($reversed ? -0.5 : 0.5);
816 $grid = ($this->styles['t_location'] == 'grid');
817 if($this->axis_no > 0)
818 $opposite = !$opposite;
819 if($this->orientation == 'h') {
820 $y = ($opposite ? -$d2 : -$d1);
821 $x = ($this->block_label ? $block_offset : 0);
822 if($grid) {
823 $y += $gy + $g_height - $ay;
824 if($this->axis_no == 1)
825 $y -= $g_height;
826 }
827 } else {
828 $x = ($opposite ? $d2 : $d1);
829 $y = ($this->block_label ? $block_offset : 0);
830 if($grid && $this->axis_no < 2) {
831 $x += $gx - $ax;
832 if($this->axis_no == 1)
833 $x += $g_width;
834 }
835 }
836 return [$x, $y, $opposite];
837 }
838
839 /**
840 * Returns the bounding box for a single axis label
841 */
842 protected function measureText($x, $y, &$point, $opposite, $level)
843 {
844 list($svg_text, $font_size, $attr, $anchor, $rcx, $rcy, $angle,
845 $line_spacing) = $this->getTextInfo($x, $y, $point, $opposite, $level);
846
847 // find (rotated) size and position now
848 list($x, $y, $w, $h) = $svg_text->measurePosition($point->getText($level),
849 $font_size, $line_spacing, $attr['x'], $attr['y'], $anchor, $angle,
850 $rcx, $rcy);
851
852 // per-item text indent last
853 if($point->item && $point->axis_text_indent) {
854 if($opposite)
855 $w += $point->axis_text_indent;
856 else
857 $x -= $point->axis_text_indent;
858 }
859 return ['x' => $x, 'y' => $y, 'width' => $w, 'height' => $h];
860 }
861
862 /**
863 * Returns the SVG fragment for a single axis label
864 */
865 protected function getText($x, $y, &$point, $opposite, $level, $anchor)
866 {
867 // skip 0 on axis when it would sit on top of other axis
868 if(!$this->block_label && $point->value == 0) {
869 if($this->axis_no == 0 && $opposite)
870 return '';
871 if($this->axis_no == 1 && !$opposite)
872 return '';
873 }
874
875 // see if the text needs shifting
876 if($point->item && $point->axis_text_indent) {
877 switch($this->styles['t_align']) {
878 case 'left' :
879 $x += $point->axis_text_indent;
880 break;
881 case 'centre':
882 // centred things can't be indented
883 break;
884 case 'right':
885 default:
886 $x -= $point->axis_text_indent;
887 break;
888 }
889 }
890
891 $string = $point->getText($level);
892 $text_out = '';
893 $text_info = $this->getTextInfo($x, $y, $point, $opposite, $level);
894 $svg_text = $text_info[0];
895 $attr = $text_info[2];
896 $line_spacing = $text_info[7];
897 $back_colour = $this->styles['t_back_colour'];
898
899 // $anchor overrides the one based on axis text location
900 if($anchor)
901 $attr['text-anchor'] = $anchor;
902
903 // structured data attributes?
904 if($point->item) {
905 if($point->axis_text_font)
906 $attr['font-family'] = $point->axis_text_font;
907 if($point->axis_text_font_size)
908 $attr['font-size'] = $point->axis_text_font_size;
909 if($point->axis_text_font_weight)
910 $attr['font-weight'] = $point->axis_text_font_weight;
911 if($point->axis_text_colour)
912 $attr['fill'] = new Colour($this->graph, $point->axis_text_colour);
913 if($point->axis_text_back_colour)
914 $back_colour = [
915 'stroke-width' => '3px',
916 'stroke' => new Colour($this->graph, $point->axis_text_back_colour),
917 'stroke-linejoin' => 'round',
918 ];
919 }
920
921 if(!empty($back_colour)) {
922 $b_attr = array_merge($back_colour, $attr);
923 $text_out .= $svg_text->text($string, $line_spacing, $b_attr);
924 }
925 $text_out .= $svg_text->text($string, $line_spacing, $attr);
926 return $text_out;
927 }
928
929 /**
930 * Returns text information:
931 * [Text, $font_size, $attr, $anchor, $rcx, $rcy, $angle, $line_spacing]
932 */
933 protected function getTextInfo($x, $y, &$point, $opposite, $level)
934 {
935 $font = $this->styles['t_font'];
936 $font_size = $this->styles['t_font_size'];
937 $font_adjust = $this->styles['t_font_adjust'];
938 if($point->item) {
939 if($point->axis_text_font)
940 $font = $point->axis_text_font;
941 if($point->axis_text_font_size)
942 $font_size = $point->axis_text_font_size;
943 if($point->axis_text_font_adjust)
944 $font_adjust = $point->axis_text_font_adjust;
945 }
946 $line_spacing = $this->styles['t_line_spacing'];
947 $svg_text = new Text($this->graph, $font, $font_adjust);
948 $baseline = $svg_text->baseline($font_size);
949 list($w, $h) = $svg_text->measure($point->getText($level), $font_size, 0,
950 $line_spacing);
951 if($this->orientation == 'h') {
952 $attr['x'] = $x + $point->position;
953 $attr['y'] = $y + $baseline - ($opposite ? $h : 0);
954 $anchor = 'middle';
955 } else {
956 $attr['x'] = $x;
957 $attr['y'] = $y + $baseline + $point->position - $h / 2;
958 $anchor = $opposite ? 'start' : 'end';
959 }
960
961 $angle = $this->styles['t_angle'];
962 $rcx = $rcy = null;
963 if($angle) {
964 if($this->orientation == 'h') {
965 $rcx = $x + $point->position;
966 $rcy = $y + $h * ($opposite ? -0.5 : 0.5);
967 if($angle < 0) {
968 $anchor = 'end';
969 $attr['x'] += $h * 0.5;
970 } else {
971 $anchor = 'start';
972 $attr['x'] -= $h * 0.5;
973 }
974 if($opposite)
975 $angle = -$angle;
976 } else {
977 $rcx = $attr['x'] + $h * ($opposite ? 0.5 : -0.5);
978 $rcy = $y + $point->position;
979 }
980 $xform = new Transform;
981 $xform->rotate($angle, $rcx, $rcy);
982 $attr['transform'] = $xform;
983 }
984 $attr['text-anchor'] = $anchor;
985
986 return [$svg_text, $font_size, $attr, $anchor, $rcx, $rcy, $angle,
987 $line_spacing];
988 }
989
990 /**
991 * Returns the path for divisions or subdivisions
992 */
993 protected function getDivisionPath($x, $y, $points, $path_info, $level)
994 {
995 $y0 = $y;
996 $x0 = $x;
997 $yinc = $xinc = 0;
998 if($this->orientation == 'v')
999 {
1000 $x0 = $x + $path_info['pos'];
1001 $len = $path_info['sz'];
1002 $yinc = 1;
1003 }
1004 else
1005 {
1006 $y0 = $y - $path_info['pos'];
1007 $len = -$path_info['sz'];
1008 $xinc = 1;
1009 }
1010
1011 $line = $path_info['line'];
1012 $path = new PathData;
1013 foreach($points as $point) {
1014 $x = $x0 + $point->position * $xinc;
1015 $y = $y0 + $point->position * $yinc;
1016 $path->add('M', $x, $y, $line, $len);
1017 }
1018
1019 if(!$path->isEmpty() && $path_info['box']) {
1020 $x = $x0 + $path_info['box_pos'] * $yinc;
1021 $y = $y0 + $path_info['box_pos'] * $xinc;
1022 $path->add('M', $x, $y, $path_info['box_line'], $path_info['box_len']);
1023 }
1024 return $path;
1025 }
1026
1027 /**
1028 * Returns the details of the path segment
1029 */
1030 protected function getDivisionPathInfo($subdiv, $g_width, $g_height, $level)
1031 {
1032 if($this->orientation == 'h') {
1033 $line = 'v';
1034 $full = $g_height;
1035 $box_len = $g_width;
1036 $box_line = 'h';
1037 } else {
1038 $line = 'h';
1039 $full = $g_width;
1040 $box_len = $this->axis->reversed() ? -$g_height : $g_height;
1041 $box_line = 'v';
1042 }
1043
1044 if($subdiv) {
1045 $style = $this->styles['s_style'];
1046 $sz = $size = $this->styles['s_size'];
1047 } else {
1048 $style = $this->styles['d_style'];
1049 $sz = $size = $this->styles['d_size'];
1050 if($this->boxed_text) {
1051 $bbox = $this->getTextBBox($level);
1052 $sx = $bbox->width();
1053 $sy = $bbox->height();
1054 $sz = $size = ($this->orientation == 'h' ? $sy : $sx) +
1055 $this->styles['t_space'];
1056 }
1057 }
1058 if(!$this->main)
1059 $style = str_replace('full', '', $style);
1060 $pos = 0;
1061 $box_pos = 0;
1062 $box = false;
1063
1064 switch($style) {
1065 case 'none' :
1066 return null; // no pos or sz
1067 case 'infull' :
1068 $sz = $full;
1069 break;
1070 case 'over' :
1071 $pos = -$size;
1072 $sz = $size * 2;
1073 break;
1074 case 'overfull' :
1075 if($this->axis_no == 0)
1076 $pos = -$size;
1077 $sz = $full + $size;
1078 break;
1079 case 'in' :
1080 if($this->axis_no != 0)
1081 $pos = -$size;
1082 break;
1083 case 'box' :
1084 $box = true;
1085 if($this->axis_no > 0)
1086 $box_pos += ($this->orientation == 'h' ? -$size : $size);
1087 if($this->axis_no == 0)
1088 $pos -= $size;
1089 break;
1090 case 'extend' :
1091 if($this->axis_no > 0)
1092 $box_pos += ($this->orientation == 'h' ? -$size : $size);
1093 // fall through
1094 case 'out' :
1095 default :
1096 if($this->axis_no == 0)
1097 $pos -= $size;
1098 }
1099
1100 return compact('sz', 'pos', 'line', 'full', 'box', 'box_len', 'box_line',
1101 'box_pos');
1102 }
1103 }
1104