. */ /** * For more information, please contact */ namespace Goat1000\SVGGraph; /** * Converts an abstract axis into SVG markup */ class DisplayAxis { protected $graph; protected $axis; protected $axis_no; protected $orientation; protected $type; protected $main; protected $styles; protected $show_axis; protected $show_divisions; protected $show_subdivisions; protected $show_text; protected $show_label; protected $label = ''; protected $block_label; protected $boxed_text; protected $minimum_subdivision; protected $minimum_units; protected $subdivisions_fixed; protected $offset = [0, 0]; /** * $orientation = 'h' or 'v' * $type = 'x' or 'y' * $main = TRUE for the main axis * $label_centre = TRUE for graphs with labels between divisions */ public function __construct(&$graph, &$axis, $axis_no, $orientation, $type, $main, $label_centre) { $this->axis_no = $axis_no; $this->orientation = $orientation; $this->type = $type; $this->main = $main; $this->block_label = ($type == 'x' && ($label_centre || $graph->getOption('force_block_label_x'))); $this->boxed_text = false; $styles = []; // set up options, styles $o = $orientation; $this->show_axis = false; $this->show_text = false; if($graph->getOption('show_axes')) { $this->show_axis = $graph->getOption(['show_axis_' . $o, $axis_no]); $this->show_text = $graph->getOption('show_axis_text_' . $o); } // offset caused by padding between axis and grid if($o == 'v') { switch($axis_no) { case 0: $this->offset[0] = -$graph->getOption('axis_pad_left'); break; case 1: $this->offset[0] = $graph->getOption('axis_pad_right'); break; } } else { switch($axis_no) { case 0: $this->offset[1] = $graph->getOption('axis_pad_bottom'); break; case 1: $this->offset[1] = -$graph->getOption('axis_pad_top'); break; } } // lambda to make retrieving options simpler $get_axis_option = function($option) use ($graph, $o, $axis_no) { return $graph->getOption([$option . '_' . $o, $axis_no], $option); }; // gridgraph moves label_[xy] into label_[hv] $o_labels = $graph->getOption('label_' . $o); if(is_array($o_labels)) { // use array entry if one exists for this axis if(isset($o_labels[$axis_no])) $this->label = $o_labels[$axis_no]; } elseif($axis_no == 0 && !empty($o_labels)) { // not an array, so only valid for axis 0 $this->label = $o_labels; } $this->show_label = ($this->label != ''); // axis and text both need colour $styles['colour'] = new Colour($graph, $get_axis_option('axis_colour')); if($this->show_axis) { $styles['overlap'] = $graph->getOption('axis_overlap'); $styles['stroke_width'] = $get_axis_option('axis_stroke_width'); if($o == 'v') { $styles['extend'] = [ $graph->getOption('axis_extend_top', 'axis_pad_top'), $graph->getOption('axis_extend_bottom', 'axis_pad_bottom'), ]; } else { $styles['extend'] = [ $graph->getOption('axis_extend_left', 'axis_pad_left'), $graph->getOption('axis_extend_right', 'axis_pad_right'), ]; } if($graph->getOption('show_divisions')) { $this->show_divisions = true; $styles['d_style'] = $get_axis_option('division_style'); $styles['d_size'] = $get_axis_option('division_size'); $styles['d_colour'] = new Colour($graph, $graph->getOption( ['division_colour_' . $o, $axis_no], 'division_colour', ['@', $styles['colour']])); if($graph->getOption('show_subdivisions')) { $this->show_subdivisions = true; $styles['s_style'] = $get_axis_option('subdivision_style'); $styles['s_size'] = $get_axis_option('subdivision_size'); $styles['s_colour'] = new Colour($graph, $graph->getOption( ['subdivision_colour_' . $o, $axis_no], 'subdivision_colour', ['division_colour_' . $o, $axis_no], 'division_colour', ['@', $styles['colour']])); $this->minimum_subdivision = $graph->getOption('minimum_subdivision'); $this->minimum_units = ($type == 'x' ? 1 : $graph->getOption(['minimum_units_y', $axis_no])); $this->subdivisions_fixed = $graph->getOption( ['subdivision_' . $o, $axis_no]); } } } if($this->show_text) { $styles['t_angle'] = $graph->getOption( ['axis_text_angle_' . $o, $axis_no], 0); if($graph->getOption('limit_text_angle')) { $angle = $styles['t_angle'] % 360; if($angle > 180) $angle = -360 + $angle; if($angle < -180) $angle = 360 + $angle; $angle = min(90, max(-90, $angle)); $styles['t_angle'] = $angle; } $styles['t_position'] = $get_axis_option('axis_text_position'); $styles['t_location'] = $get_axis_option('axis_text_location'); $styles['t_font'] = $get_axis_option('axis_font'); $styles['t_font_size'] = Number::units($get_axis_option('axis_font_size')); $styles['t_font_adjust'] = $get_axis_option('axis_font_adjust'); $styles['t_font_weight'] = $get_axis_option('axis_font_weight'); $styles['t_space'] = $get_axis_option('axis_text_space'); $styles['t_offset_x'] = $get_axis_option('axis_text_offset_x'); $styles['t_offset_y'] = $get_axis_option('axis_text_offset_y'); $styles['t_colour'] = new Colour($graph, $graph->getOption( ['axis_text_colour_' . $o, $axis_no], 'axis_text_colour', ['@', $styles['colour']])); $styles['t_line_spacing'] = Number::units($get_axis_option('axis_text_line_spacing')); if($styles['t_line_spacing'] === null || $styles['t_line_spacing'] < 1) $styles['t_line_spacing'] = $styles['t_font_size']; $styles['t_back_colour'] = null; $styles['t_align'] = $get_axis_option('axis_text_align'); // fill in background colour array, if required $back_colour = $get_axis_option('axis_text_back_colour'); if(!empty($back_colour)) { $styles['t_back_colour'] = [ 'stroke-width' => '3px', 'stroke' => new Colour($graph, $back_colour), 'stroke-linejoin' => 'round', ]; } // text is boxed only if it is outside and block labelling if($this->block_label && $this->show_divisions && ($styles['d_style'] == 'box' || $styles['d_style'] == 'extend') && $styles['t_position'] != 'inside') { $this->boxed_text = true; // text must be attached to axis, not grid $styles['t_location'] = 'axis'; } } if($this->show_label) { $styles['l_font'] = $graph->getOption( ['label_font_' . $o, $axis_no], 'label_font', ['axis_font_' . $o, $axis_no], 'axis_font'); $styles['l_font_size'] = Number::units($graph->getOption( ['label_font_size_' . $o, $axis_no], 'label_font_size', ['axis_font_size_' . $o, $axis_no], 'axis_font_size')); $styles['l_font_weight'] = $graph->getOption( ['label_font_weight_' . $o, $axis_no], 'label_font_weight'); $styles['l_colour'] = new Colour($graph, $graph->getOption( ['label_colour_' . $o, $axis_no], 'label_colour', ['axis_text_colour_' . $o, $axis_no], 'axis_text_colour', ['@', $styles['colour']])); $styles['l_space'] = $graph->getOption('label_space'); $styles['l_pos'] = $get_axis_option('axis_label_position'); $styles['l_line_spacing'] = Number::units($get_axis_option('label_line_spacing')); if($styles['l_line_spacing'] === null || $styles['l_line_spacing'] < 1) $styles['l_line_spacing'] = $styles['l_font_size']; } $this->styles = $styles; $this->axis =& $axis; $this->graph =& $graph; } /** * Returns the array of style information for the axis */ public function getStyling() { return $this->styles; } /** * Returns the extents of the axis, relative to where it will be drawn from * returns BoundingBox */ public function measure($with_label = true) { $bbox = new BoundingBox(0, 0, 0, 0); if($this->show_axis) { $dbox = $this->getDivisionsBBox(0); if($this->orientation == 'h') $dbox->flipY(); $bbox->growBox($dbox); } if($this->show_text) { $tbox = $this->getTextBBox(0); $bbox->growBox($tbox); } if($with_label && $this->show_label) { $lpos = $this->getLabelPosition(); $bbox->grow($lpos['x'], $lpos['y'], $lpos['x'] + $lpos['width'], $lpos['y'] + $lpos['height']); } $bbox = $this->addOffset($bbox); return $bbox; } /** * Adds the padding offset to the bounding box */ protected function addOffset(BoundingBox $bbox) { if($this->axis_no > 0) { $bbox->x2 += $this->offset[0]; $bbox->y1 += $this->offset[1]; } else { $bbox->x1 += $this->offset[0]; $bbox->y2 += $this->offset[1]; } return $bbox; } /** * Measures the space taken up by axis and divisions * returns BoundingBox */ protected function getDivisionsBBox($level) { $bbox = new BoundingBox(0, 0, 0, 0); if(!$this->show_axis) return $bbox; // orientation more important than type $x = 'x'; $y = 'y'; if($this->orientation == 'h') { $x = 'y'; $y = 'x'; } $min = $max = ['x' => 0, 'y' => 0]; $length = $this->axis->getLength(); $d_info = $s_info = ['pos' => 0, 'sz' => 0]; if($this->show_divisions) { $di = $this->getDivisionPathInfo(false, 100, 100, $level); if($di !== null) $d_info = $di; if($this->show_subdivisions) { $si = $this->getDivisionPathInfo(true, 100, 100, $level); if($si !== null) $s_info = $si; } } $points = $this->axis->getGridPoints(0); $p1 = end($points); if($p1->position < 0) { $min[$y] = $p1->position; $max[$y] = 0; } else { $min[$y] = 0; $max[$y] = $p1->position; } $min[$x] = min($d_info['pos'], $s_info['pos']); $max[$x] = max($d_info['pos'] + $d_info['sz'], $s_info['pos'] + $s_info['sz']); $bbox->grow($min['x'], $min['y'], $max['x'], $max['y']); return $bbox; } /** * Returns the bounding box of the text */ protected function getTextBBox($level) { $bbox = new BoundingBox(0, 0, 0, 0); list($x_off, $y_off, $opp) = $this->getTextOffset(0, 0, 0, 0, 0, 0, $level); $t_offset = ($this->orientation == 'h' ? $x_off : $y_off); if($this->axis->reversed()) $t_offset = -$t_offset; $length = $this->axis->getLength(); $points = $this->axis->getGridPoints(0); $positions = $this->getTextPositions(0, 0, $x_off, $y_off, $points, null); $count = count($positions); for($p = 0; $p < $count; ++$p) { if(!$this->pointTextVisible($points[$p], $length, $t_offset)) continue; $pos = $positions[$p]; $lbl = $this->measureText($pos['x'], $pos['y'], $points[$p], $opp, $level); $bbox->grow($lbl['x'], $lbl['y'], $lbl['x'] + $lbl['width'], $lbl['y'] + $lbl['height']); } return $bbox; } /** * Returns the overlap between axis text labels */ public function getTextOverlap() { // start with obviously good overlap $overlap = -1000; $prev_x = -10; $level = 0; // no overlap if there is no text if(!$this->show_text) return null; list($x_off, $y_off, $opp) = $this->getTextOffset(0, 0, 0, 0, 0, 0, $level); $t_offset = ($this->orientation == 'h' ? $x_off : $y_off); if($this->axis->reversed()) $t_offset = -$t_offset; $length = $this->axis->getLength(); $points = $this->axis->getGridPoints(0); $positions = $this->getTextPositions(0, 0, $x_off, $y_off, $points, null); $count = count($positions); for($p = 0; $p < $count; ++$p) { if(!$this->pointTextVisible($points[$p], $length, $t_offset)) continue; $pos = $positions[$p]; $lbl = $this->measureText($pos['x'], $pos['y'], $points[$p], $opp, $level); $o = $prev_x - $lbl['x']; // bail out now if the overlap is positive if($o > 0) return $o; if($o > $overlap) $overlap = $o; $prev_x = $lbl['x'] + $lbl['width']; } return $overlap; } /** * Returns true if the text exists and its location is within the axis */ protected function pointTextVisible($point, $axis_len, $offset) { if($point->blank()) return false; // amount of space to allow for rounding errors $leeway = 0.5; $position = abs($point->position) + $offset; return $position >= -$leeway && $position <= $axis_len + $leeway; } /** * Returns the positions (actually offsets) of all the text blocks */ protected function getTextPositions($x, $y, $xoff, $yoff, $points, $anchor) { $positions = []; // vertical axis a bit simpler if($this->orientation == 'v') { foreach($points as $k => $p) { if($this->boxed_text && isset($points[$k + 1])) { $pnext = $points[$k + 1]; $yoff = ($pnext->position - $p->position) / 2; } $positions[] = ['x' => $x + $xoff + $this->styles['t_offset_x'], 'y' => $y + $yoff + $this->styles['t_offset_y']]; } return $positions; } foreach($points as $k => $p) { if($anchor == 'start') { $xoff = $this->styles['t_space']; } elseif($anchor == 'end') { $pnext = $points[$k + 1]; $xoff = $pnext->position - $p->position - $this->styles['t_space']; } elseif($this->boxed_text && isset($points[$k + 1])) { $pnext = $points[$k + 1]; $xoff = ($pnext->position - $p->position) / 2; } $positions[] = ['x' => $x + $xoff + $this->styles['t_offset_x'], 'y' => $y + $yoff + $this->styles['t_offset_y']]; } return $positions; } /** * Draws the axis at ($x,$y) * $gx, $gy, $g_width and $g_height are the grid dimensions */ public function draw($x, $y, $gx, $gy, $g_width, $g_height) { $content = ''; $x += $this->offset[0]; $y += $this->offset[1]; $gx += $this->offset[0]; $gy += $this->offset[1]; if($this->show_axis) { if($this->show_divisions) { $content .= $this->drawDivisions($x, $y, $g_width, $g_height); if($this->show_subdivisions) { $content .= $this->drawSubDivisions($x, $y, $g_width, $g_height); } } $length = $this->axis->getLength(); $content .= $this->drawAxisLine($x, $y, $length); } if($this->show_text || $this->show_label) $content .= $this->drawText($x, $y, $gx, $gy, $g_width, $g_height); return $content; } /** * Draws the axis line */ public function drawAxisLine($x, $y, $len) { $overlap = $this->styles['overlap']; $length = $len + $overlap * 2; $line = $this->orientation; // 'h' and 'v' are SVG path lines $reversed = $this->axis->reversed(); if($this->orientation == 'h') { $x = $reversed ? $x - $overlap - $len : $x - $overlap; $x -= $this->styles['extend'][0]; $length += $this->styles['extend'][0] + $this->styles['extend'][1]; } else { $y = $reversed ? $y - $overlap - $len : $y - $overlap; $y -= $this->styles['extend'][0]; $length += $this->styles['extend'][0] + $this->styles['extend'][1]; } $colour = $this->styles['colour']; if($colour->isGradient()) { // gradients don't work on stroked horizontal or vertical lines $sw = $this->styles['stroke_width']; $attr = [ 'fill' => $colour, 'x' => $x, 'y' => $y, ]; if($this->orientation == 'h') { $attr['y'] -= $sw / 2; $attr['width'] = $length; $attr['height'] = $sw; } else { $attr['x'] -= $sw / 2; $attr['width'] = $sw; $attr['height'] = $length; } return $this->graph->element('rect', $attr); } $attr = [ 'stroke' => $colour, 'd' => new PathData('M', $x, $y, $line, $length), ]; if($this->styles['stroke_width'] != 1) $attr['stroke-width'] = $this->styles['stroke_width']; return $this->graph->element('path', $attr); } /** * Draws the axis divisions */ public function drawDivisions($x, $y, $g_width, $g_height) { $path_info = $this->getDivisionPathInfo(false, $g_width, $g_height, 0); if($path_info === null) return ''; $points = $this->axis->getGridPoints(0); $d = $this->getDivisionPath($x, $y, $points, $path_info, 0); $attr = [ 'd' => $d, 'stroke' => $this->styles['d_colour'], 'fill' => 'none', ]; return $this->graph->element('path', $attr); } /** * Draws the axis subdivisions */ public function drawSubDivisions($x, $y, $g_width, $g_height) { $path_info = $this->getDivisionPathInfo(true, $g_width, $g_height, 0); if($path_info === null) return ''; $points = $this->axis->getGridSubdivisions($this->minimum_subdivision, $this->minimum_units, 0, $this->subdivisions_fixed); $d = $this->getDivisionPath($x, $y, $points, $path_info, 0); $attr = [ 'd' => $d, 'stroke' => $this->styles['s_colour'], 'fill' => 'none', ]; return $this->graph->element('path', $attr); } /** * Draws the axis text labels */ public function drawText($x, $y, $gx, $gy, $g_width, $g_height) { $labels = ''; if($this->show_text) { list($x_offset, $y_offset, $opposite) = $this->getTextOffset($x, $y, $gx, $gy, $g_width, $g_height, 0); $t_offset = ($this->orientation == 'h' ? $x_offset : $y_offset); if($this->axis->reversed()) $t_offset = -$t_offset; $length = $this->axis->getLength(); $points = $this->axis->getGridPoints(0); $anchor = null; $space = $this->styles['t_space']; if($this->styles['t_align']) { $bbox = $this->measure(false); switch($this->styles['t_align']) { case 'left': if($this->orientation == 'v') { if(!$opposite) { $anchor = 'start'; $x_offset = $bbox->x1 + $space; $x_offset -= $this->offset[0]; } } else { $anchor = 'start'; $x_offset = $space; } break; case 'right': if($this->orientation == 'v') { if($opposite) { $anchor = 'end'; $x_offset = $bbox->x2 - $space; } } else { $anchor = 'end'; $x_offset = -$space; } break; case 'centre': if($this->orientation == 'v') { $x_offset = ($x_offset + ($opposite ? $bbox->x2 : $bbox->x1)) / 2; $anchor = 'middle'; } } } $positions = $this->getTextPositions($x, $y, $x_offset, $y_offset, $points, $anchor); $count = count($positions); for($p = 0; $p < $count; ++$p) { if(!$this->pointTextVisible($points[$p], $length, $t_offset)) continue; $pos = $positions[$p]; $labels .= $this->getText($pos['x'], $pos['y'], $points[$p], $opposite, 0, $anchor); } if($labels != '') { $group = [ 'font-family' => $this->styles['t_font'], 'font-size' => $this->styles['t_font_size'], 'fill' => $this->styles['t_colour'], ]; $weight = $this->styles['t_font_weight']; if($weight != 'normal' && $weight !== null) $group['font-weight'] = $weight; $labels = $this->graph->element('g', $group, null, $labels); } } if($this->show_label) $labels .= $this->getLabel($x, $y, $gx, $gy, $g_width, $g_height); return $labels; } /** * Returns the label */ protected function getLabel($x, $y, $gx, $gy, $g_width, $g_height) { $pos = $this->getLabelPosition(); $offset = $this->getLabelOffset($x, $y, $gx, $gy, $g_width, $g_height); $tx = $offset['x'] + $pos['tx']; $ty = $offset['y'] + $pos['ty']; $label = [ 'text-anchor' => 'middle', 'font-family' => $this->styles['l_font'], 'font-size' => $this->styles['l_font_size'], 'fill' => $this->styles['l_colour'], 'x' => $tx, 'y' => $ty, ]; if(!empty($this->styles['l_font_weight']) && $this->styles['l_font_weight'] != 'normal') $label['font-weight'] = $this->styles['l_font_weight']; if($pos['angle']) { $xform = new Transform; $xform->rotate($pos['angle'], $tx, $ty); $label['transform'] = $xform; } $svg_text = new Text($this->graph, $this->styles['l_font']); return $svg_text->text($this->label, $this->styles['l_line_spacing'], $label); } /** * Returns the corrected offset for the label */ protected function getLabelOffset($x, $y, $gx, $gy, $g_width, $g_height) { if($this->orientation == 'h') { // label at bottom of grid? if($this->axis_no == 0) $y = $gy + $g_height; } else { // leftmost axis label must be outside grid if($this->axis_no == 0) $x = $gx; } return ['x' => $x, 'y' => $y]; } /** * Returns the dimensions of the label * x, y, width, height = position and size * tx, tx = text anchor point * angle = text angle */ protected function getLabelPosition() { $bbox = $this->measure(false); $font_size = $this->styles['l_font_size']; $line_spacing = $this->styles['l_line_spacing']; $svg_text = new Text($this->graph, $this->styles['l_font']); $tsize = $svg_text->measure($this->label, $font_size, 0, $line_spacing); $baseline = $svg_text->baseline($font_size); $a_length = $this->axis->getLength(); $space = $this->styles['l_space']; $align = $this->styles['l_pos']; if($this->orientation == 'h') { $width = $tsize[0]; $height = $tsize[1]; if($this->axis_no > 0) { $y = $bbox->y1 - $space - $height; } else { $y = $bbox->y2 + $space; } $y -= $this->offset[1]; // handle padding offset if(is_numeric($align)) { $tx = $a_length * $align; } else { switch($align) { case 'left' : $tx = $width / 2; break; case 'right': $tx = $a_length - ($width / 2); break; default: $tx = $a_length / 2; break; } } $ty = $y + $baseline; $x = $tx - $width / 2; $height += $space; $angle = 0; } else { $width = $tsize[1]; $height = $tsize[0]; $reversed = $this->axis->reversed(); if($this->axis_no > 0) { $x = $bbox->x2 + $space; $x -= $this->offset[0]; // handle padding offset $tx = $x + $width - $baseline; } else { $x = $bbox->x1 - $space - $width; $x -= $this->offset[0]; // handle padding offset $tx = $x + $baseline; $x -= $space; } if(is_numeric($align)) { $ty = $reversed ? -$a_length * $align : $a_length * $align; } else { switch($align) { case 'top' : $ty = $reversed ? -$a_length + $height / 2 : $height / 2; break; case 'bottom': $ty = $reversed ? -$height / 2 : $a_length - $height / 2; break; default: $ty = $reversed ? -$a_length / 2 : $a_length / 2; break; } } $y = $ty - $height / 2; $width += $space; $angle = $this->axis_no > 0 ? 90 : 270; } return compact('x', 'y', 'width', 'height', 'tx', 'ty', 'angle'); } /** * Returns the distance from the axis to draw the text */ protected function getTextOffset($ax, $ay, $gx, $gy, $g_width, $g_height, $level) { $d1 = $d2 = 0; if($this->show_divisions && $level == 0) { if(!$this->boxed_text) { $d_info = $this->getDivisionPathInfo(false, 100, 100, 0); if($d_info !== null) { $d1 = $d_info['pos']; $d2 = $d1 + $d_info['sz']; } } if($this->show_subdivisions) { $s_info = $this->getDivisionPathInfo(true, 100, 100, 0); if($s_info !== null) { $s1 = $s_info['pos']; $s2 = $s1 + $s_info['sz']; $d1 = min($d1, $s1); $d2 = max($d2, $s2); } } } $space = $this->styles['t_space']; $d1 -= $space; $d2 += $space; $opposite = ($this->styles['t_position'] == 'inside'); $reversed = $this->axis->reversed(); $block_offset = $this->axis->unit() * ($reversed ? -0.5 : 0.5); $grid = ($this->styles['t_location'] == 'grid'); if($this->axis_no > 0) $opposite = !$opposite; if($this->orientation == 'h') { $y = ($opposite ? -$d2 : -$d1); $x = ($this->block_label ? $block_offset : 0); if($grid) { $y += $gy + $g_height - $ay; if($this->axis_no == 1) $y -= $g_height; } } else { $x = ($opposite ? $d2 : $d1); $y = ($this->block_label ? $block_offset : 0); if($grid && $this->axis_no < 2) { $x += $gx - $ax; if($this->axis_no == 1) $x += $g_width; } } return [$x, $y, $opposite]; } /** * Returns the bounding box for a single axis label */ protected function measureText($x, $y, &$point, $opposite, $level) { list($svg_text, $font_size, $attr, $anchor, $rcx, $rcy, $angle, $line_spacing) = $this->getTextInfo($x, $y, $point, $opposite, $level); // find (rotated) size and position now list($x, $y, $w, $h) = $svg_text->measurePosition($point->getText($level), $font_size, $line_spacing, $attr['x'], $attr['y'], $anchor, $angle, $rcx, $rcy); // per-item text indent last if($point->item && $point->axis_text_indent) { if($opposite) $w += $point->axis_text_indent; else $x -= $point->axis_text_indent; } return ['x' => $x, 'y' => $y, 'width' => $w, 'height' => $h]; } /** * Returns the SVG fragment for a single axis label */ protected function getText($x, $y, &$point, $opposite, $level, $anchor) { // skip 0 on axis when it would sit on top of other axis if(!$this->block_label && $point->value == 0) { if($this->axis_no == 0 && $opposite) return ''; if($this->axis_no == 1 && !$opposite) return ''; } // see if the text needs shifting if($point->item && $point->axis_text_indent) { switch($this->styles['t_align']) { case 'left' : $x += $point->axis_text_indent; break; case 'centre': // centred things can't be indented break; case 'right': default: $x -= $point->axis_text_indent; break; } } $string = $point->getText($level); $text_out = ''; $text_info = $this->getTextInfo($x, $y, $point, $opposite, $level); $svg_text = $text_info[0]; $attr = $text_info[2]; $line_spacing = $text_info[7]; $back_colour = $this->styles['t_back_colour']; // $anchor overrides the one based on axis text location if($anchor) $attr['text-anchor'] = $anchor; // structured data attributes? if($point->item) { if($point->axis_text_font) $attr['font-family'] = $point->axis_text_font; if($point->axis_text_font_size) $attr['font-size'] = $point->axis_text_font_size; if($point->axis_text_font_weight) $attr['font-weight'] = $point->axis_text_font_weight; if($point->axis_text_colour) $attr['fill'] = new Colour($this->graph, $point->axis_text_colour); if($point->axis_text_back_colour) $back_colour = [ 'stroke-width' => '3px', 'stroke' => new Colour($this->graph, $point->axis_text_back_colour), 'stroke-linejoin' => 'round', ]; } if(!empty($back_colour)) { $b_attr = array_merge($back_colour, $attr); $text_out .= $svg_text->text($string, $line_spacing, $b_attr); } $text_out .= $svg_text->text($string, $line_spacing, $attr); return $text_out; } /** * Returns text information: * [Text, $font_size, $attr, $anchor, $rcx, $rcy, $angle, $line_spacing] */ protected function getTextInfo($x, $y, &$point, $opposite, $level) { $font = $this->styles['t_font']; $font_size = $this->styles['t_font_size']; $font_adjust = $this->styles['t_font_adjust']; if($point->item) { if($point->axis_text_font) $font = $point->axis_text_font; if($point->axis_text_font_size) $font_size = $point->axis_text_font_size; if($point->axis_text_font_adjust) $font_adjust = $point->axis_text_font_adjust; } $line_spacing = $this->styles['t_line_spacing']; $svg_text = new Text($this->graph, $font, $font_adjust); $baseline = $svg_text->baseline($font_size); list($w, $h) = $svg_text->measure($point->getText($level), $font_size, 0, $line_spacing); if($this->orientation == 'h') { $attr['x'] = $x + $point->position; $attr['y'] = $y + $baseline - ($opposite ? $h : 0); $anchor = 'middle'; } else { $attr['x'] = $x; $attr['y'] = $y + $baseline + $point->position - $h / 2; $anchor = $opposite ? 'start' : 'end'; } $angle = $this->styles['t_angle']; $rcx = $rcy = null; if($angle) { if($this->orientation == 'h') { $rcx = $x + $point->position; $rcy = $y + $h * ($opposite ? -0.5 : 0.5); if($angle < 0) { $anchor = 'end'; $attr['x'] += $h * 0.5; } else { $anchor = 'start'; $attr['x'] -= $h * 0.5; } if($opposite) $angle = -$angle; } else { $rcx = $attr['x'] + $h * ($opposite ? 0.5 : -0.5); $rcy = $y + $point->position; } $xform = new Transform; $xform->rotate($angle, $rcx, $rcy); $attr['transform'] = $xform; } $attr['text-anchor'] = $anchor; return [$svg_text, $font_size, $attr, $anchor, $rcx, $rcy, $angle, $line_spacing]; } /** * Returns the path for divisions or subdivisions */ protected function getDivisionPath($x, $y, $points, $path_info, $level) { $y0 = $y; $x0 = $x; $yinc = $xinc = 0; if($this->orientation == 'v') { $x0 = $x + $path_info['pos']; $len = $path_info['sz']; $yinc = 1; } else { $y0 = $y - $path_info['pos']; $len = -$path_info['sz']; $xinc = 1; } $line = $path_info['line']; $path = new PathData; foreach($points as $point) { $x = $x0 + $point->position * $xinc; $y = $y0 + $point->position * $yinc; $path->add('M', $x, $y, $line, $len); } if(!$path->isEmpty() && $path_info['box']) { $x = $x0 + $path_info['box_pos'] * $yinc; $y = $y0 + $path_info['box_pos'] * $xinc; $path->add('M', $x, $y, $path_info['box_line'], $path_info['box_len']); } return $path; } /** * Returns the details of the path segment */ protected function getDivisionPathInfo($subdiv, $g_width, $g_height, $level) { if($this->orientation == 'h') { $line = 'v'; $full = $g_height; $box_len = $g_width; $box_line = 'h'; } else { $line = 'h'; $full = $g_width; $box_len = $this->axis->reversed() ? -$g_height : $g_height; $box_line = 'v'; } if($subdiv) { $style = $this->styles['s_style']; $sz = $size = $this->styles['s_size']; } else { $style = $this->styles['d_style']; $sz = $size = $this->styles['d_size']; if($this->boxed_text) { $bbox = $this->getTextBBox($level); $sx = $bbox->width(); $sy = $bbox->height(); $sz = $size = ($this->orientation == 'h' ? $sy : $sx) + $this->styles['t_space']; } } if(!$this->main) $style = str_replace('full', '', $style); $pos = 0; $box_pos = 0; $box = false; switch($style) { case 'none' : return null; // no pos or sz case 'infull' : $sz = $full; break; case 'over' : $pos = -$size; $sz = $size * 2; break; case 'overfull' : if($this->axis_no == 0) $pos = -$size; $sz = $full + $size; break; case 'in' : if($this->axis_no != 0) $pos = -$size; break; case 'box' : $box = true; if($this->axis_no > 0) $box_pos += ($this->orientation == 'h' ? -$size : $size); if($this->axis_no == 0) $pos -= $size; break; case 'extend' : if($this->axis_no > 0) $box_pos += ($this->orientation == 'h' ? -$size : $size); // fall through case 'out' : default : if($this->axis_no == 0) $pos -= $size; } return compact('sz', 'pos', 'line', 'full', 'box', 'box_len', 'box_line', 'box_pos'); } }