zigbee device graph visualization

This commit is contained in:
2025-08-17 22:11:47 +02:00
parent c94a2bf5d9
commit 6fa1beac99
3 changed files with 8867 additions and 0 deletions

19
src/lib/utils/color.ts Normal file
View File

@@ -0,0 +1,19 @@
export function hexToRgba(hex: string, alpha = 1) {
// Remove leading # if present
hex = hex.replace(/^#/, '');
// Handle shorthand (#fff → #ffffff)
if (hex.length === 3) {
hex = hex.split('').map(c => c + c).join('');
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
<script lang="ts">
import { onMount } from 'svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { hexToRgba } from '$lib/utils/color.ts';
import { data } from '$lib/utils/zigbee_devices';
let width = 650;
let element;
let hovering;
let Graph;
export const ssr = false;
function nodeCanvasObject(node, ctx, globalScale) {
const label = node.name;
let baseFontSize = 7;
let exponent = -0.6; // Adjust this value based on testing to find the right feel.
let fontSize = baseFontSize * Math.pow(globalScale, exponent);
ctx.font = `${fontSize}px sans-serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map((n) => n + fontSize * 0.2); // some padding
ctx.beginPath();
let color = groupToColor(node.group);
if (hovering) {
const hoveredNeighbor = hovering?.neighbors?.findIndex((n) => n.ieee === node.ieee);
color = hoveredNeighbor > -1 ? hexToRgba(color, 0.2) : color;
}
ctx.fillStyle = color || 'blue';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, node.x + 5 + 5 * fontSize, node.y); // Adjust position as needed
ctx.arc(node.x, node.y, node.radius || 5, 0, 2 * Math.PI, false);
ctx.fillStyle = color || 'blue';
ctx.fill();
ctx.fill();
}
function groupToColor(group) {
switch (group) {
case 0:
return '#01A9F4';
case 1:
return '#00BCD4';
case 2:
return '#009688';
case 3:
return '#DB4537';
default:
return 'blue'
}
}
function createNodeLabel(node) {
const el = document.createElement('div');
el.style.display = 'flex';
el.style.flexDirection = 'column';
const ob = {
IEEE: node.ieee,
'Device type': node.type,
NWK: '0x3cf1',
Device: node.device,
Area: node.area
};
el.innerHTML = Object.entries(ob)
.map(([title, text]) => {
return `<span><b>${title}:</b> ${text}</span>`;
})
.toString()
.replaceAll(',', '');
return el;
}
onMount(async function () {
// window.addEventListener('mousemove', trackMouse, false);
console.log(data);
const ForceGraph = (await import('force-graph')).default;
Graph = new ForceGraph(element)
.width(800)
.height(550)
.graphData(data)
.linkDirectionalParticles(2)
.linkDirectionalParticleWidth(1.4)
.linkCurvature(0)
.nodeCanvasObject(nodeCanvasObject)
.nodeLabel(createNodeLabel)
.onNodeClick((node) => {
// Center/zoom on node
Graph.centerAt(node.x, node.y, 1000);
Graph.zoom(8, 2000);
})
.onNodeHover((node) => {
hovering = node || null;
console.log(node);
});
setTimeout(() => Graph.zoomToFit(0, 140), 2);
});
</script>
<PageHeader>Zigbee</PageHeader>
<div class="container">
<div class="header">
<span>Coordinator</span>
<span>Router</span>
<span>End device</span>
<span>Offline</span>
</div>
<div id="graph" bind:this={element}></div>
</div>
<style lang="scss">
.container {
display: inline-block;
border: 3px solid black;
border-radius: var(--border-radius);
position: relative;
.header {
position: absolute;
height: 2rem;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 1rem;
&:nth-of-type(1)::before {
content: '';
position: absolute;
margin-top: 1px;
margin-left: -1.6rem;
height: 1rem;
width: 1.4rem;
border-radius: 3px;
background-color: #01a9f4;
}
&:nth-of-type(2)::before {
content: '';
position: absolute;
margin-top: 1px;
margin-left: -1.2rem;
height: 1rem;
width: 1rem;
border-radius: 50%;
background-color: #00bcd4;
}
&:nth-of-type(3)::before {
content: '';
position: absolute;
margin-top: 1px;
margin-left: -1.2rem;
height: 1rem;
width: 1rem;
border-radius: 50%;
background-color: #009688;
}
&:nth-of-type(4)::before {
content: '';
position: absolute;
margin-top: 1px;
margin-left: -1.2rem;
height: 1rem;
width: 1rem;
border-radius: 50%;
background-color: #db4537;
}
}
}
}
</style>