1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4 5/** 6 * Google Chart Plugin: Embeds Charts into DokuWiki 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Andreas Gohr <andi@splitbrain.org> 10 */ 11class syntax_plugin_gchart extends SyntaxPlugin 12{ 13 private $supported_charts = [ 14 'qr' => 'qr', 15 'pie' => 'p3', 16 'pie3d' => 'p3', 17 'pie2d' => 'p', 18 'line' => 'lc', 19 'spark' => 'ls', 20 'sparkline' => 'ls', 21 'bar' => 'bvs', 22 'hbar' => 'bhs', 23 'vbar' => 'bvs' 24 ]; 25 26 /** 27 * What kind of syntax are we? 28 */ 29 public function getType() 30 { 31 return 'substition'; 32 } 33 34 public function getPType() 35 { 36 return 'block'; 37 } 38 39 /** 40 * Where to sort in? 41 */ 42 public function getSort() 43 { 44 return 160; 45 } 46 47 /** 48 * Connect pattern to lexer 49 */ 50 public function connectTo($mode) 51 { 52 $this->Lexer->addSpecialPattern('<gchart.*?>\n.*?\n</gchart>', $mode, 'plugin_gchart'); 53 } 54 55 /** 56 * Handle the match 57 */ 58 public function handle($match, $state, $pos, Doku_Handler $handler) 59 { 60 61 // prepare default data 62 $return = [ 63 'type' => 'p3', 64 'data' => [], 65 'width' => 320, 66 'height' => 140, 67 'align' => 'right', 68 'legend' => false, 69 'value' => false, 70 'title' => '', 71 'fg' => ltrim($this->getConf('fg'), '#'), 72 'bg' => ltrim($this->getConf('bg'), '#') 73 ]; 74 75 // prepare input 76 $lines = explode("\n", $match); 77 $conf = array_shift($lines); 78 array_pop($lines); 79 80 // parse adhoc configs 81 if (preg_match('/"([^"]+)"/', $conf, $match)) { 82 $return['title'] = $match[1]; 83 $conf = preg_replace('/"([^"]+)"/', '', $conf); 84 } 85 if (preg_match('/\b(left|center|right)\b/i', $conf, $match)) { 86 $return['align'] = strtolower($match[1]); 87 } 88 if (preg_match('/\b(legend)\b/i', $conf, $match)) { 89 $return['legend'] = true; 90 } 91 if (preg_match('/\b(values?)\b/i', $conf, $match)) { 92 $return['value'] = true; 93 } 94 if (preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) { 95 $return['width'] = $match[1]; 96 $return['height'] = $match[2]; 97 } 98 99 $type_regex = '/\b(' . implode('|', array_keys($this->supported_charts)) . ')\b/i'; 100 if (preg_match($type_regex, $conf, $match)) { 101 $return['type'] = $this->supported_charts[strtolower($match[1])]; 102 } 103 if (preg_match_all('/#([0-9a-f]{6}([0-9a-f][0-9a-f])?)\b/i', $conf, $match)) { 104 if (isset($match[1][0])) { 105 $return['fg'] = $match[1][0]; 106 } 107 if (isset($match[1][1])) { 108 $return['bg'] = $match[1][1]; 109 } 110 } 111 112 // parse chart data 113 $data = []; 114 foreach ($lines as $line) { 115 //ignore comments (except escaped ones) 116 $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line); 117 $line = str_replace('\\#', '#', $line); 118 $line = trim($line); 119 if (empty($line)) { 120 continue; 121 } 122 $line = preg_split('/(?<!\\\\)=/', $line, 2); //split on unescaped equal sign 123 $line[0] = str_replace('\\=', '=', $line[0]); 124 $line[1] = str_replace('\\=', '=', $line[1]); 125 $data[trim($line[0])] = trim($line[1]); 126 } 127 $return['data'] = $data; 128 129 return $return; 130 } 131 132 /** 133 * Create output 134 */ 135 public function render($mode, Doku_Renderer $R, $data) 136 { 137 if ($mode != 'xhtml') { 138 return false; 139 } 140 141 $val = array_map('floatval', array_values($data['data'])); 142 $max = max(0, ceil(max($val))); 143 $min = min(0, floor(min($val))); 144 $key = array_keys($data['data']); 145 146 $parameters = []; 147 148 $parameters['cht'] = $data['type']; 149 if ($data['bg']) { 150 $parameters['chf'] = 'bg,s,' . $data['bg']; 151 } 152 if ($data['fg']) { 153 $parameters['chco'] = implode('|', $this->createColorPalette($data['fg'], count($val))); 154 } 155 $parameters['chs'] = $data['width'] . 'x' . $data['height']; # size 156 $parameters['chd'] = 't:' . implode(',', $val); 157 $parameters['chds'] = $min . ',' . $max; 158 $parameters['choe'] = 'UTF-8'; 159 if ($data['title']) { 160 $parameters['chtt'] = $data['title']; 161 } 162 163 switch ($data['type']) { 164 case 'bhs': # horizontal bar 165 $parameters['chxt'] = 'y'; 166 $parameters['chxl'] = '0:|' . implode('|', array_reverse($key)); 167 $parameters['chbh'] = 'a'; 168 if ($data['value']) { 169 $parameters['chm'] = 'N*f*,333333,0,-1,11'; 170 } 171 break; 172 case 'bvs': # vertical bar 173 $parameters['chxt'] = 'y,x'; 174 $parameters['chxr'] = '0,' . $min . ',' . $max; 175 $parameters['chxl'] = '1:|' . implode('|', $key); 176 $parameters['chbh'] = 'a'; 177 if ($data['value']) { 178 $parameters['chm'] = 'N*f*,333333,0,-1,11'; 179 } 180 break; 181 case 'lc': # line graph 182 $parameters['chxt'] = 'y,x'; 183 $parameters['chxr'] = '0,' . floor(min($min, 0)) . ',' . ceil($max); 184 $parameters['chxl'] = '1:|' . implode('|', $key); 185 if ($data['value']) { 186 $parameters['chm'] = 'N*f*,333333,0,-1,11'; 187 } 188 break; 189 case 'ls': # spark line 190 if ($data['value']) { 191 $parameters['chm'] = 'N*f*,333333,0,-1,11'; 192 } 193 break; 194 case 'p3': # pie graphs 195 case 'p': 196 if ($data['value']) { 197 $cnt = count($key); 198 for ($i = 0; $i < $cnt; $i++) { 199 $key[$i] .= ' (' . $val[$i] . ')'; 200 } 201 } 202 $parameters['chl'] = implode('|', $key); 203 break; 204 case 'qr': 205 $rawval = array_keys($data['data']); 206 if (in_array($rawval[0], ['L', 'M', 'Q', 'H'])) { 207 $parameters['chld'] = array_shift($rawval); 208 } 209 unset($parameters['chd']); 210 unset($parameters['chds']); 211 $parameters['chl'] = implode(';', $rawval); 212 break; 213 } 214 215 $url = $this->getConf('charturl') . '?' . http_build_query($parameters, '', '&') . '&.png'; 216 217 $attr = [ 218 'class' => 'media' . $data['align'], 219 'alt' => '', 220 'width' => $data['width'], 221 'height' => $data['height'] 222 ]; 223 224 225 if ($data['align'] == 'left') { 226 $attr['align'] = 'left'; 227 } elseif ($data['align'] == 'right') { 228 $attr['align'] = 'right'; 229 } 230 231 $R->doc .= sprintf('<img src="%s" %s />', ml($url), buildAttributes($attr)); 232 233 return true; 234 } 235 236 /** 237 * Google used to creae a palette of colors based on a single given color, 238 * quickcharts won't so we do it ourselves. Crudely. Using transparancy. 239 * It does not look great but at least each element has a different shade. 240 * 241 * @param string $rgb original hex color 242 * @param int $count number of colors to generate 243 * @return array 244 */ 245 protected function createColorPalette($rgb, $count) 246 { 247 $palette = []; 248 $inc = floor(255 / $count); 249 for ($i = 0; $i < $count; $i++) { 250 $palette[] = $rgb . dechex(255 - $i * $inc); 251 } 252 return $palette; 253 } 254} 255