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