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

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

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