/* 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}
`;
});
}