. */ /** * For more information, please contact */ namespace Goat1000\SVGGraph; /** * Draws legend */ class Legend { protected $graph; protected $entry_details = []; protected $autohide; protected $autohide_opacity; protected $back_colour; protected $colour; protected $columns; protected $draggable; protected $entries; protected $entry_height; protected $entry_width; protected $font; protected $font_adjust; protected $font_size; protected $font_weight; protected $position; protected $round; protected $shadow_opacity; protected $show_empty; protected $stroke_colour; protected $stroke_width; protected $text_side; protected $line_spacing; protected $title; protected $title_colour; protected $title_font; protected $title_font_adjust; protected $title_font_size; protected $title_font_weight; protected $title_line_spacing; protected $title_link; protected $type; protected $unique_fields; public function __construct(&$graph) { $this->graph =& $graph; // copy options to class $opts = ['autohide', 'autohide_opacity', 'back_colour', 'colour', 'columns', 'draggable', 'entries', 'entry_height', 'entry_width', 'font', 'font_adjust', 'font_weight', 'position', 'round', 'shadow_opacity', 'show_empty', 'stroke_colour', 'stroke_width', 'text_side', 'title', 'title_link', 'title_font_weight', 'type', 'unique_fields']; foreach($opts as $opt) { $this->{$opt} = $graph->getOption('legend_' . $opt); } // slightly more complicated options $this->font_size = Number::units($graph->getOption('legend_font_size')); $this->title_colour = $graph->getOption('legend_title_colour', 'legend_colour'); $this->title_font = $graph->getOption('legend_title_font', 'legend_font'); $this->title_font_adjust = $graph->getOption('legend_title_font_adjust', 'legend_font_adjust'); $this->title_font_size = Number::units($graph->getOption('legend_title_font_size', 'legend_font_size')); $this->line_spacing = Number::units($graph->getOption('legend_line_spacing')); if($this->line_spacing === null || $this->line_spacing < 1) $this->line_spacing = $this->font_size; $this->title_line_spacing = Number::units($graph->getOption('legend_title_line_spacing')); if($this->title_line_spacing === null || $this->title_line_spacing < 1) $this->title_line_spacing = $this->title_font_size; } /** * Sets the style information for an entry */ public function setEntry($dataset, $index, $item, $style_info) { // ignore entry if empty and legend_show_empty not enabled if(!$this->show_empty && !$this->graph->isVisible($item, $dataset)) return; // find the text first $text = ''; $link = null; $entry = count($this->entry_details); $itext = $item->legend_text; if($itext !== null) $text = $itext; if($text == '') { // no text from structured data if($this->type == 'none') return; if($this->type == 'dataset') { // one entry per dataset $entry = $dataset; if(!isset($this->entry_details[$entry]) && isset($this->entries[$dataset])) $text = $this->entries[$dataset]; } else { // $this->type == 'all' // one entry per data item if(isset($this->entries[$index])) $text = $this->entries[$index]; } } // split out link if(is_array($text)) list($text, $link) = $text; if($this->unique_fields) { // prevent adding multiple entries with the same text foreach($this->entry_details as $e) { if($e->text == $text) return; } } // if there is no text, don't add the entry if($text != '') $this->entry_details[$entry] = new LegendEntry($item, $text, $link, $style_info); } /** * Draws the legend */ public function draw() { $entries = $this->getEntries(); $entry_count = count($entries); if($entry_count < 1) return ''; $encoding = $this->graph->encoding; // find the largest width / height $font_size = $this->font_size; $max_width = $max_height = 0; $svg_text = new Text($this->graph, $this->font, $this->font_adjust); $baseline = $svg_text->baseline($font_size); foreach($this->entry_details as $entry) { list($w, $h) = $svg_text->measure($entry->text, $font_size, 0, $this->line_spacing); if($w > $max_width) $max_width = $w; if($h > $max_height) $max_height = $h; $entry->width = $w; $entry->height = $h; } $title = ''; $title_width = $entries_x = 0; $padding_y = $this->graph->getOption('legend_padding_y', 'legend_padding', 1); $padding_x = $this->graph->getOption('legend_padding_x', 'legend_padding', 1); $start_y = $padding_y; $w = $this->entry_width; $x = 0; $entry_height = max($max_height, $this->entry_height); // make room for title if($this->title != '') { $title_font_size = $this->title_font_size; $svg_text_title = new Text($this->graph, $this->title_font, $this->title_font_adjust); list($tw, $th) = $svg_text_title->measure($this->title, $title_font_size, 0, $this->title_line_spacing); $title_width = $tw + $padding_x * 2; $start_y += $th + $this->graph->getOption('legend_title_spacing', 'legend_padding', 1); } $columns = max(1, min(ceil($this->columns), $entry_count)); $per_column = ceil($entry_count / $columns); $columns = ceil($entry_count / $per_column); $column = 0; $text = ['x' => 0]; $column_entry = 0; $y = $start_y; $text_columns = array_fill(0, $columns, ''); $entry_columns = array_fill(0, $columns, ''); $valid_entries = 0; $spacing = $this->graph->getOption('legend_spacing', 'legend_padding', 1); $entry_space = $this->graph->getOption('legend_entry_spacing', 'legend_padding', 1); $col_space = $this->graph->getOption('legend_column_spacing', 'legend_padding', 1); foreach($entries as $entry) { $y = $start_y + $column_entry * ($entry_height + $spacing); // position the graph element $e_y = $y + ($entry_height - $this->entry_height) / 2; $element = $this->graph->drawLegendEntry($x, $e_y, $w, $this->entry_height, $entry); if(!empty($element)) { // position the text element $text['y'] = $y + $baseline + ($entry_height - $entry->height) / 2; $text_element = $svg_text->text($entry->text, $this->line_spacing, $text); if($entry->link !== null) $text_element = $this->graph->getLink($entry, 0, $text_element); $text_columns[$column] .= $text_element; $entry_columns[$column] .= $element; ++$valid_entries; if(++$column_entry == $per_column) { $column_entry = 0; ++$column; } } } // if there's nothing to go in the legend, stop now if(!$valid_entries) return ''; if($this->text_side == 'left') { $text_x_offset = $max_width + $padding_x; $entries_x_offset = $max_width + $padding_x + $entry_space; } else { $text_x_offset = $w + $padding_x + $entry_space; $entries_x_offset = $padding_x; } $longest_width = $padding_x * 2 + ($entry_space * $columns) + $col_space * ($columns - 1) + ($this->entry_width + $max_width) * $columns; $column_width = $col_space + $this->entry_width + $entry_space + $max_width; $width = max($title_width, $longest_width); $height = $start_y + $per_column * ($entry_height + $spacing) - $spacing + $padding_y; // centre the entries if the title makes the box bigger if($width > $longest_width) { $offset = ($width - $longest_width) / 2; $entries_x_offset += $offset; $text_x_offset += $offset; } $xform = new Transform; $xform->translate($text_x_offset, 0); $text_group = ['transform' => $xform]; if($this->text_side == 'left') $text_group['text-anchor'] = 'end'; $xform = new Transform; $xform->translate($entries_x_offset, 0); $entries_group = ['transform' => $xform]; $parts = ''; foreach($entry_columns as $col) { $parts .= $this->graph->element('g', $entries_group, null, $col); $entries_x_offset += $column_width; $xform = new Transform; $xform->translate($entries_x_offset, 0); $entries_group['transform'] = $xform; } foreach($text_columns as $col) { $parts .= $this->graph->element('g', $text_group, null, $col); $text_x_offset += $column_width; $xform = new Transform; $xform->translate($text_x_offset, 0); $text_group['transform'] = $xform; } // create box and title $box = [ 'fill' => new Colour($this->graph, $this->back_colour), 'width' => $width, 'height' => $height, ]; if($this->round > 0) $box['rx'] = $box['ry'] = $this->round; if($this->stroke_width) { $box['stroke-width'] = $this->stroke_width; $box['stroke'] = new Colour($this->graph, $this->stroke_colour); } $rect = $this->graph->element('rect', $box); if($this->title != '') { $text['x'] = $width / 2; $text['y'] = $padding_y + $svg_text_title->baseline($title_font_size); $text['text-anchor'] = 'middle'; if($this->title_font != $this->font) $text['font-family'] = $this->title_font; if($title_font_size != $font_size) $text['font-size'] = $title_font_size; if($this->title_font_weight != $this->font_weight) $text['font-weight'] = $this->title_font_weight; if($this->title_colour != $this->colour) $text['fill'] = new Colour($this->graph, $this->title_colour); $title = $svg_text_title->text($this->title, $this->title_line_spacing, $text); if(!empty($this->title_link)) { $item = new \stdClass; $item->link = $this->title_link; $title = $this->graph->getLink($item, 0, $title); } } // find position for legend if(is_array($this->position)) { list($px, $py) = $this->position; $vx = Coords::parseValue($px); $vy = Coords::parseValue($py); $c = new Coords($this->graph); list($tx, $ty) = $c->transformCoords($px, $py); // handle relative positions switch($vx['value']) { case 'c' : $left = $tx - ($width / 2); break; case 'r' : $left = $tx - $width; break; default : $left = $tx; } switch($vy['value']) { case 'c' : $top = $ty - ($height / 2); break; case 'b' : $top = $ty - $height; break; default: $top = $ty; } } else { list($left, $top) = $this->graph->parsePosition($this->position, $width, $height); } // create group to contain whole legend $xform = new Transform; $xform->translate($left, $top); $group = [ 'font-family' => $this->font, 'font-size' => $font_size, 'fill' => new Colour($this->graph, $this->colour), 'transform' => $xform, ]; if($this->font_weight != 'normal') $group['font-weight'] = $this->font_weight; // add shadow if not completely transparent if($this->shadow_opacity > 0) { $box['x'] = $box['y'] = 2 + ($this->stroke_width / 2); $box['fill'] = '#000'; $box['opacity'] = $this->shadow_opacity; unset($box['stroke'], $box['stroke-width']); $rect = $this->graph->element('rect', $box) . $rect; } if($this->autohide) { $o0 = $this->autohide_opacity; $o1 = 1; if($o0 < 0) { $o1 = new Number(-$o0); $o0 = 1; $group['opacity'] = $o1; } $this->graph->getJavascript()->autoHide($group, $o0, $o1); } if($this->draggable) $this->graph->getJavascript()->setDraggable($group); return $this->graph->element('g', $group, null, $rect . $title . $parts); } /** * Filters the entry list */ protected function filterEntries($entries) { $callback = $this->graph->getOption('legend_entries_callback'); if(!is_callable($callback)) return $entries; $filtered = call_user_func($callback, $entries); $valid = false; if(is_array($filtered)) { $valid = true; foreach($filtered as $e) { if(!is_object($e) || get_class($e) !== 'Goat1000\\SVGGraph\\LegendEntry') $valid = false; } } if(!$valid) throw new \Exception('Callback did not return array of LegendEntry values.'); return $filtered; } /** * Returns the list of entries in the correct order */ protected function getEntries() { $entry_order = $this->graph->getOption('legend_order'); if($entry_order === null || $entry_order == 'auto') $entry_order = $this->graph->getLegendOrder(); if(is_array($entry_order)) { $entries = []; foreach($entry_order as $e) { if(isset($this->entry_details[$e])) $entries[] = $this->entry_details[$e]; } return $this->filterEntries($entries); } $entries = $this->entry_details; if(!empty($entry_order)) { if(strpos($entry_order, 'sort') !== false) { usort($entries, function($a, $b) { if($a->text == $b->text) return 0; return $a->text > $b->text ? 1 : -1; }); } if(strpos($entry_order, 'reverse') !== false) $entries = array_reverse($entries, true); } return $this->filterEntries($entries); } }