// Radar chart generation for IP stats // Used by map popups and IP detail partials // Extracted from dashboard_template.py (lines ~2092-2181) /** * Generate an SVG radar chart for category scores. * This is a reusable function that can be called from: * - Map popup panels (generateMapPanelRadarChart in map.js) * - IP detail partials (server-side or client-side rendering) * * @param {Object} categoryScores - Object with keys: attacker, good_crawler, bad_crawler, regular_user, unknown * @param {number} [size=200] - Width/height of the SVG in pixels * @param {boolean} [showLegend=true] - Whether to show the legend below the chart * @param {string} [legendPosition='below'] - 'below' or 'side' (side = legend to the right of the chart) * @returns {string} HTML string containing the SVG radar chart */ function generateRadarChart(categoryScores, size, showLegend, legendPosition) { size = size || 200; if (showLegend === undefined) showLegend = true; legendPosition = legendPosition || 'below'; if (!categoryScores || Object.keys(categoryScores).length === 0) { return '
No category data available
'; } const scores = { attacker: categoryScores.attacker || 0, good_crawler: categoryScores.good_crawler || 0, bad_crawler: categoryScores.bad_crawler || 0, regular_user: categoryScores.regular_user || 0, unknown: categoryScores.unknown || 0 }; const maxScore = Math.max(...Object.values(scores), 1); const minVisibleRadius = 0.15; const normalizedScores = {}; Object.keys(scores).forEach(key => { normalizedScores[key] = minVisibleRadius + (scores[key] / maxScore) * (1 - minVisibleRadius); }); const colors = { attacker: '#f85149', good_crawler: '#3fb950', bad_crawler: '#f0883e', regular_user: '#58a6ff', unknown: '#8b949e' }; const labels = { attacker: 'Attacker', good_crawler: 'Good Bot', bad_crawler: 'Bad Bot', regular_user: 'User', unknown: 'Unknown' }; const cx = 100, cy = 100, maxRadius = 75; const flexDir = legendPosition === 'side' ? 'row' : 'column'; let html = `
`; html += ``; // Draw concentric circles (grid) for (let i = 1; i <= 5; i++) { const r = (maxRadius / 5) * i; html += ``; } const angles = [0, 72, 144, 216, 288]; const keys = ['good_crawler', 'regular_user', 'unknown', 'bad_crawler', 'attacker']; // Draw axis lines and labels angles.forEach((angle, i) => { const rad = (angle - 90) * Math.PI / 180; const x2 = cx + maxRadius * Math.cos(rad); const y2 = cy + maxRadius * Math.sin(rad); html += ``; const labelDist = maxRadius + 35; const lx = cx + labelDist * Math.cos(rad); const ly = cy + labelDist * Math.sin(rad); html += `${labels[keys[i]]}`; }); // Calculate polygon points let points = []; angles.forEach((angle, i) => { const normalizedScore = normalizedScores[keys[i]]; const rad = (angle - 90) * Math.PI / 180; const r = normalizedScore * maxRadius; const x = cx + r * Math.cos(rad); const y = cy + r * Math.sin(rad); points.push(`${x},${y}`); }); // Determine dominant category for color const dominantKey = Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b); const dominantColor = colors[dominantKey]; // Draw filled polygon html += ``; // Draw data point dots angles.forEach((angle, i) => { const normalizedScore = normalizedScores[keys[i]]; const rad = (angle - 90) * Math.PI / 180; const r = normalizedScore * maxRadius; const x = cx + r * Math.cos(rad); const y = cy + r * Math.sin(rad); html += ``; }); html += ''; // Optional legend if (showLegend) { html += '
'; keys.forEach(key => { html += '
'; html += `
`; html += `${labels[key]}: ${scores[key]} pt`; html += '
'; }); html += '
'; } html += '
'; return html; }