Skip to content

Instantly share code, notes, and snippets.

@ryanbateman
Last active September 23, 2023 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanbateman/1ce6c910b747bbe888246bf9dac4549d to your computer and use it in GitHub Desktop.
Save ryanbateman/1ce6c910b747bbe888246bf9dac4549d to your computer and use it in GitHub Desktop.
An in-browser, MQTT topic visualisation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MQTT Topic tree visualisation experiment</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<style>
.node-label {
max-width: 15px; /* You can adjust this value as needed */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</head>
<body>
<svg width="960" height="1500"></svg>
<script>
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
const g = svg.append("g").attr("transform", "translate(40,40)");
const treeLayout = d3.tree().size([height - 200, width - 160]).separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
const rootNode = {
name: "Root",
children: []
};
function drawTree(data) {
const root = d3.hierarchy(data);
treeLayout(root);
svg.selectAll('*').remove();
const g = svg.append("g").attr("transform", "translate(10,10)");
const links = g.selectAll(".link")
.data(root.links())
.enter()
.append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal().x(d => d.y).y(d => d.x))
.style("fill", "none")
.style("stroke", d => d.target.data.isFlashing ? "#F6BE00" : "#ccc")
.style("stroke-width", d => d.target.data.isFlashing ? "0.3em" : "0.1em");
links.filter(d => d.target.data.isFlashing)
.transition()
.duration(5000)
.style("stroke", "#ccc")
.style("stroke-width", "0.1em")
.on("end", d => d.target.data.isFlashing = false);
const nodes = g.selectAll(".node")
.data(root.descendants())
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", d => d.y)
.attr("cy", d => d.x)
.attr("r", 4)
.style("fill", d => d.data.isFlashing ? "#F6BE00" : (d.data.isNew ? "red" : "#666"));
nodes.filter(d => d.data.isNew)
.transition()
.duration(3000)
.style("fill", "#666")
.on("end", d => d.data.isNew = false);
nodes.filter(d => d.data.isFlashing)
.transition()
.duration(5000)
.style("fill", "#666")
.on("end", d => d.data.isFlashing = false);
g.selectAll(".node-label")
.data(root.descendants())
.enter()
.append("text")
.attr("class", "node-label")
.attr("x", d => d.y)
.attr("y", d => d.x)
.attr("dy", "0.35em")
.attr("dx", d => d.children ? "-15px" : "15px")
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name)
.style("font-family", "Arial")
.style("font-size", "8px");
}
function flashNodesForTopic(topic) {
let segments = topic.split('/');
let currentNode = rootNode;
segments.forEach(segment => {
if (currentNode.children) {
let existingNode = currentNode.children.find(n => n.name === segment);
if (existingNode) {
existingNode.isFlashing = true;
currentNode = existingNode;
}
}
});
drawTree(rootNode);
}
function updateTreeWithNewNode(topic) {
let segments = topic.split('/');
let currentNode = rootNode;
segments.forEach(segment => {
if (!currentNode.children) {
currentNode.children = [];
}
let existingNode = currentNode.children.find(n => n.name === segment);
if (!existingNode) {
let newNode = { name: segment, isNew: true };
currentNode.children.push(newNode);
existingNode = newNode;
}
currentNode = existingNode;
});
drawTree(rootNode);
}
const client = mqtt.connect("mqtt://192.168.178.17:9001");
client.on("connect", () => {
client.subscribe("#", (err) => {
if (!err) {
console.log("Connected");
}
});
});
client.on('message', function (topic, message) {
flashNodesForTopic(topic.toString());
updateTreeWithNewNode(topic.toString());
});
drawTree(rootNode);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment