/* Vendor Treemap — D3 treemap of device vendors by type */ function renderVendorTreemap(selector, apiUrl) { const container = document.querySelector(selector); const tooltip = createTooltip(); const { width, height } = getVizSize(container); fetch(apiUrl) .then(r => r.json()) .then(data => { if (!data.children || data.children.length === 0) { container.innerHTML = '
No device data available
'; return; } const color = d3.scaleOrdinal() .domain(['wifi', 'ble']) .range(['#4dabf7', '#69db7c']); const root = d3.hierarchy(data) .sum(d => d.value || 0) .sort((a, b) => b.value - a.value); d3.treemap() .size([width, height]) .padding(2) .round(true)(root); const svg = d3.select(selector) .append('svg') .attr('viewBox', `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMidYMid meet'); const leaves = svg.selectAll('g') .data(root.leaves()) .join('g') .attr('transform', d => `translate(${d.x0},${d.y0})`); leaves.append('rect') .attr('class', 'treemap-cell') .attr('width', d => Math.max(0, d.x1 - d.x0)) .attr('height', d => Math.max(0, d.y1 - d.y0)) .attr('fill', d => { const type = d.parent ? d.parent.data.name : 'wifi'; return color(type); }) .attr('fill-opacity', 0.8) .on('mouseover', (event, d) => { const type = d.parent ? d.parent.data.name : ''; tooltip.show( `${d.data.name}
` + `Type: ${type}
` + `Devices: ${d.value}`, event ); d3.select(event.target).attr('fill-opacity', 1); }) .on('mousemove', (event) => { tooltip.move(event); }) .on('mouseout', (event) => { tooltip.hide(); d3.select(event.target).attr('fill-opacity', 0.8); }); // Labels only for cells big enough leaves.append('text') .attr('class', 'treemap-label') .attr('x', 4) .attr('y', 14) .text(d => { const w = d.x1 - d.x0; const h = d.y1 - d.y0; if (w < 40 || h < 18) return ''; const name = d.data.name; const maxChars = Math.floor(w / 7); return name.length > maxChars ? name.slice(0, maxChars - 1) + '\u2026' : name; }); leaves.append('text') .attr('class', 'treemap-label') .attr('x', 4) .attr('y', 28) .style('font-size', '10px') .style('opacity', 0.8) .text(d => { const w = d.x1 - d.x0; const h = d.y1 - d.y0; if (w < 30 || h < 32) return ''; return d.value; }); }) .catch(err => { container.innerHTML = `
Failed to load data: ${err.message}
`; }); }