let currentMetric = 'cpu'; let clusterData = null; const formatBytes = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatCpu = (millicores) => { if (millicores === 0) return '0 m'; return `${millicores} m`; }; document.getElementById('btn-cpu').addEventListener('click', (e) => { document.getElementById('btn-cpu').classList.add('active'); document.getElementById('btn-mem').classList.remove('active'); currentMetric = 'cpu'; if (clusterData) renderChart(clusterData); }); document.getElementById('btn-mem').addEventListener('click', (e) => { document.getElementById('btn-mem').classList.add('active'); document.getElementById('btn-cpu').classList.remove('active'); currentMetric = 'memory'; if (clusterData) renderChart(clusterData); }); async function fetchData() { try { const response = await fetch('/api/cluster-data'); if (!response.ok) throw new Error('Failed to fetch data'); clusterData = await response.json(); document.getElementById('loading').classList.add('hidden'); renderChart(clusterData); } catch (error) { console.error(error); document.getElementById('loading').innerHTML = `

Error loading data: ${error.message}

`; } } function renderChart(data) { const container = document.getElementById('chart-container'); const width = container.clientWidth; const height = container.clientHeight; // Clear previous d3.select('#chart-container').selectAll('*').remove(); const svg = d3.select('#chart-container') .append('svg') .attr('width', width) .attr('height', height) .style('font-family', 'Inter, sans-serif'); // Create hierarchy const root = d3.hierarchy(data) .sum(d => { if (d.type === 'node') return 0; return d[currentMetric] || 0; }) .sort((a, b) => b.value - a.value); // Create Treemap d3.treemap() .size([width, height]) .paddingTop(28) .paddingRight(8) .paddingInner(4) .paddingBottom(8) .paddingLeft(8) .round(true)(root); // Get nodes and pods const nodes = root.descendants().filter(d => d.depth === 1); // Nodes const pods = root.leaves(); // Pods and Unused // Color scale for pods const getColor = (d) => { if (d.data.type === 'unused') return 'var(--color-unused)'; if (d.data.status === 'Running') return 'var(--color-running)'; if (d.data.status === 'Pending') return 'var(--color-pending)'; return 'var(--color-failed)'; }; // Draw Node Groups const nodeGroups = svg.selectAll('.node-group') .data(nodes) .enter() .append('g') .attr('class', 'node-group') .attr('transform', d => `translate(${d.x0},${d.y0})`); // Node Background nodeGroups.append('rect') .attr('class', 'node-rect') .attr('width', d => Math.max(0, d.x1 - d.x0)) .attr('height', d => Math.max(0, d.y1 - d.y0)); // Node Label nodeGroups.append('text') .attr('class', 'node-label') .attr('x', 8) .attr('y', 20) .text(d => d.data.name); // Draw Pods const podGroups = svg.selectAll('.pod-group') .data(pods) .enter() .append('g') .attr('class', 'pod-group') .attr('transform', d => `translate(${d.x0},${d.y0})`); // Pod Rect podGroups.append('rect') .attr('class', 'pod-rect') .attr('width', d => Math.max(0, d.x1 - d.x0)) .attr('height', d => Math.max(0, d.y1 - d.y0)) .attr('fill', d => getColor(d)) .on('mousemove', function(event, d) { const tooltip = document.getElementById('tooltip'); tooltip.classList.remove('hidden'); // Boundary detection for tooltip const tWidth = 240; let left = event.pageX + 15; let top = event.pageY + 15; if (left + tWidth > window.innerWidth) { left = event.pageX - tWidth - 15; } tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; let val = currentMetric === 'cpu' ? formatCpu(d.data[`${currentMetric}_raw`]) : formatBytes(d.data[`${currentMetric}_raw`]); if (d.data.type === 'unused') { tooltip.innerHTML = `
Available Capacity
${currentMetric.toUpperCase()}: ${val}
`; } else { tooltip.innerHTML = `
${d.data.name}
Namespace: ${d.data.namespace}
Status: ${d.data.status}
${currentMetric.toUpperCase()}: ${val}
`; } }) .on('mouseout', function() { document.getElementById('tooltip').classList.add('hidden'); }); // Pod Label (only if there is space) podGroups.append('text') .attr('class', 'pod-label') .attr('x', 4) .attr('y', 14) .text(d => { if (d.data.type === 'unused') return ''; const width = d.x1 - d.x0; const height = d.y1 - d.y0; if (width > 60 && height > 20) { let txt = d.data.name; if (txt.length * 5 > width) { return txt.substring(0, Math.floor(width/5) - 2) + '..'; } return txt; } return ''; }); } // Handle resize window.addEventListener('resize', () => { if (clusterData) { renderChart(clusterData); } }); // Initial fetch fetchData(); // Poll every 30 seconds setInterval(fetchData, 30000);