1<?php 2/** 3 * DokuWiki Plugin RadarChart (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Heinrich Krupp 7 */ 8 9use dokuwiki\Extension\SyntaxPlugin; 10 11class syntax_plugin_radarchart extends SyntaxPlugin { 12 /** @var array */ 13 protected array $colorSchemes; 14 15 /** @var array */ 16 protected array $defaultConfig; 17 18 /** 19 * Constructor. Initializes properties. 20 */ 21 public function __construct() { 22 $this->colorSchemes = array( 23 'default' => array( 24 array('background' => 'rgba(54, 162, 235, 0.2)', 'border' => 'rgb(54, 162, 235)'), 25 array('background' => 'rgba(255, 99, 132, 0.2)', 'border' => 'rgb(255, 99, 132)'), 26 array('background' => 'rgba(75, 192, 192, 0.2)', 'border' => 'rgb(75, 192, 192)'), 27 array('background' => 'rgba(255, 159, 64, 0.2)', 'border' => 'rgb(255, 159, 64)') 28 ), 29 'pastel' => array( 30 array('background' => 'rgba(190, 227, 219, 0.2)', 'border' => 'rgb(137, 207, 191)'), 31 array('background' => 'rgba(255, 214, 214, 0.2)', 'border' => 'rgb(255, 169, 169)'), 32 array('background' => 'rgba(214, 229, 250, 0.2)', 'border' => 'rgb(159, 190, 237)'), 33 array('background' => 'rgba(255, 234, 214, 0.2)', 'border' => 'rgb(255, 202, 149)') 34 ) 35 ); 36 37 $this->defaultConfig = array( 38 'width' => 400, 39 'height' => 400, 40 'colorScheme' => 'default', 41 'minScale' => 0, 42 'maxScale' => 100, 43 'legendPosition' => 'top', 44 'borderWidth' => 2, 45 'pointRadius' => 3, 46 'fillOpacity' => 0.2, 47 'containerBg' => 'default', 48 'chartBg' => 'transparent' 49 ); 50 } 51 52 public function getType(): string { 53 return 'protected'; 54 } 55 56 public function getPType(): string { 57 return 'block'; 58 } 59 60 public function getSort(): int { 61 return 155; 62 } 63 64 public function connectTo($mode) { 65 $this->Lexer->addSpecialPattern('<radar.*?>.*?</radar>', $mode, 'plugin_radarchart'); 66 } 67 68 protected function parseConfig($configString): array { 69 $config = $this->defaultConfig; 70 71 try { 72 if (preg_match('/<radar\s+([^>]+)>/', $configString, $matches)) { 73 $attrs = $matches[1]; 74 if (preg_match_all('/(\w+)="([^"]*)"/', $attrs, $pairs)) { 75 for ($i = 0; $i < count($pairs[1]); $i++) { 76 $key = $pairs[1][$i]; 77 $value = $pairs[2][$i]; 78 if (array_key_exists($key, $config)) { 79 $config[$key] = $value; 80 } 81 } 82 } 83 } 84 } catch (Exception $e) { 85 msg('RadarChart config parsing error: ' . hsc($e->getMessage()), -1); 86 } 87 88 return $config; 89 } 90 91 public function handle($match, $state, $pos, $handler): array { 92 try { 93 $config = $this->parseConfig($match); 94 95 if (!preg_match('/<radar.*?>(.*?)<\/radar>/s', $match, $matches)) { 96 return array(array(), array(), $config); 97 } 98 99 $content = trim($matches[1]); 100 if (empty($content)) { 101 return array(array(), array(), $config); 102 } 103 104 $lines = explode("\n", $content); 105 $datasets = array(); 106 $labels = array(); 107 $currentDataset = null; 108 109 foreach ($lines as $line) { 110 $line = trim($line); 111 if (empty($line)) continue; 112 113 if (preg_match('/^@dataset\s+([^|]+)(?:\|([^|]+))?(?:\|([^|]+))?$/', $line, $matches)) { 114 if ($currentDataset !== null) { 115 $datasets[] = $currentDataset; 116 } 117 $currentDataset = array( 118 'label' => trim($matches[1]), 119 'data' => array() 120 ); 121 continue; 122 } 123 124 $parts = array_map('trim', explode('|', $line)); 125 if (count($parts) >= 2) { 126 if (!in_array($parts[0], $labels)) { 127 $labels[] = $parts[0]; 128 } 129 if ($currentDataset === null) { 130 $currentDataset = array( 131 'label' => 'Dataset 1', 132 'data' => array() 133 ); 134 } 135 $currentDataset['data'][] = floatval($parts[1]); 136 } 137 } 138 139 if ($currentDataset !== null) { 140 $datasets[] = $currentDataset; 141 } 142 143 return array($labels, $datasets, $config); 144 145 } catch (Exception $e) { 146 msg('RadarChart error: ' . hsc($e->getMessage()), -1); 147 return array(array(), array(), $this->defaultConfig); 148 } 149 } 150 151 public function render($mode, $renderer, $data): bool { 152 if ($mode !== 'xhtml') return false; 153 154 try { 155 list($labels, $datasets, $config) = $data; 156 157 if (empty($labels) || empty($datasets)) { 158 return false; 159 } 160 161 $chartId = 'radar_' . md5(uniqid('', true)); 162 163 // Container style with specific dimensions 164 $containerStyle = sprintf('width: %dpx; height: %dpx;', 165 intval($config['width']), 166 intval($config['height']) 167 ); 168 169 $renderer->doc .= sprintf( 170 '<div class="radar-chart-container" style="%s">', 171 $containerStyle 172 ); 173 $renderer->doc .= sprintf( 174 '<canvas id="%s" width="%d" height="%d"></canvas>', 175 $chartId, 176 intval($config['width']), 177 intval($config['height']) 178 ); 179 $renderer->doc .= '</div>'; 180 181 $chartData = array( 182 'labels' => $labels, 183 'datasets' => array() 184 ); 185 186 $colors = $this->colorSchemes[$config['colorScheme']] ?? $this->colorSchemes['default']; 187 foreach ($datasets as $index => $dataset) { 188 $colorIndex = $index % count($colors); 189 $chartData['datasets'][] = array( 190 'label' => $dataset['label'], 191 'data' => $dataset['data'], 192 'backgroundColor' => $colors[$colorIndex]['background'], 193 'borderColor' => $colors[$colorIndex]['border'], 194 'borderWidth' => intval($config['borderWidth']), 195 'fill' => true 196 ); 197 } 198 199 $renderer->doc .= '<script>'; 200 $renderer->doc .= 'window.addEventListener("load", function() {'; 201 $renderer->doc .= ' if (typeof Chart === "undefined") {'; 202 $renderer->doc .= ' const script = document.createElement("script");'; 203 $renderer->doc .= ' script.src = "https://cdn.jsdelivr.net/npm/chart.js";'; 204 $renderer->doc .= ' script.onload = function() { createChart(); };'; 205 $renderer->doc .= ' document.head.appendChild(script);'; 206 $renderer->doc .= ' } else {'; 207 $renderer->doc .= ' createChart();'; 208 $renderer->doc .= ' }'; 209 $renderer->doc .= ' function createChart() {'; 210 $renderer->doc .= ' new Chart(document.getElementById("' . $chartId . '"), {'; 211 $renderer->doc .= ' type: "radar",'; 212 $renderer->doc .= ' data: ' . json_encode($chartData) . ','; 213 $renderer->doc .= ' options: {'; 214 $renderer->doc .= ' responsive: true,'; 215 $renderer->doc .= ' maintainAspectRatio: false,'; 216 $renderer->doc .= ' scales: {r: {suggestedMin: ' . intval($config['minScale']) . ','; 217 $renderer->doc .= ' suggestedMax: ' . intval($config['maxScale']) . '}},'; 218 $renderer->doc .= ' plugins: {legend: {position: "' . $config['legendPosition'] . '"}}'; 219 $renderer->doc .= ' }'; 220 $renderer->doc .= ' });'; 221 $renderer->doc .= ' }'; 222 $renderer->doc .= '});'; 223 $renderer->doc .= '</script>'; 224 225 return true; 226 } catch (Exception $e) { 227 msg('RadarChart render error: ' . hsc($e->getMessage()), -1); 228 return false; 229 } 230 } 231}