mirror of
https://github.com/Roslund/sthlm-mesh.git
synced 2026-08-12 11:53:12 +02:00
add channel filter to stats
This commit is contained in:
@@ -7,6 +7,30 @@ draft: false
|
||||
<br/>
|
||||
{{% blocks/section color="white" %}}
|
||||
|
||||
<style>
|
||||
@media (max-width: 576px) {
|
||||
#channelToolbar { top: 16px !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="channelToolbar" class="d-flex justify-content-end sticky-top" style="top: 84px;">
|
||||
<div class="btn-toolbar bg-white shadow-sm rounded-3 py-2 px-2" role="toolbar" aria-label="Channel toolbar">
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Channel filter">
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelAll" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="filterChannelAll">Alla</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Basic radio toggle button group">
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelMediumFast" data-channel="MediumFast" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="filterChannelMediumFast">MediumFast</label>
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelLongFast" data-channel="LongFast" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="filterChannelLongFast">LongFast</label>
|
||||
</div>
|
||||
<div data-bs-toggle="tooltip" data-bs-placement="left" title="Filtrera på kanal eller visa alla.">
|
||||
<span class="badge rounded-pill bg-dark">?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Antal Enheter
|
||||
<div class="container my-3 mx-0" style="max-width: 1000px;">
|
||||
<div class="row text-center px-0">
|
||||
@@ -164,4 +188,17 @@ Graferna baseras på data från [map.sthlm-mesh.se](https://map.sthlm-mesh.se),
|
||||
<script src="/js/status/channel-utilization-gauges.js"></script>
|
||||
<script src="/js/status/is-unmessagable-chart.js"></script>
|
||||
<script src="/js/status/is-ok-to-mqtt-chart.js"></script>
|
||||
<script src="/js/status/max-hops-chart.js"></script>
|
||||
<script src="/js/status/max-hops-chart.js"></script>
|
||||
<script>
|
||||
// Initialize Bootstrap tooltips (delegated) so static and dynamic elements work
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.bootstrap && typeof window.bootstrap.Tooltip === 'function') {
|
||||
new window.bootstrap.Tooltip(document.body, {
|
||||
selector: "[data-bs-toggle='tooltip']",
|
||||
container: 'body'
|
||||
});
|
||||
} else {
|
||||
console.warn("Bootstrap Tooltip not available; falling back to native titles.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,3 +1,6 @@
|
||||
let deviceRolesChartInstance = null;
|
||||
let selectedChannelIdRoles = null; // null = All
|
||||
|
||||
async function deviceRolesChart() {
|
||||
const canvas = document.getElementById('deviceRoles');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -14,7 +17,12 @@ async function deviceRolesChart() {
|
||||
|
||||
// Count role occurrences
|
||||
const roleCounts = {};
|
||||
nodes.forEach(node => {
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!selectedChannelIdRoles) return true; // All
|
||||
return n.channel_id === selectedChannelIdRoles;
|
||||
}) : [];
|
||||
|
||||
filteredNodes.forEach(node => {
|
||||
const role = node.role_name || "UNKNOWN";
|
||||
roleCounts[role] = (roleCounts[role] || 0) + 1;
|
||||
});
|
||||
@@ -36,7 +44,12 @@ async function deviceRolesChart() {
|
||||
return 'rgb(41, 156, 70)';
|
||||
});
|
||||
|
||||
new Chart(ctx, {
|
||||
if (deviceRolesChartInstance) {
|
||||
deviceRolesChartInstance.destroy();
|
||||
deviceRolesChartInstance = null;
|
||||
}
|
||||
|
||||
deviceRolesChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -71,3 +84,14 @@ async function deviceRolesChart() {
|
||||
}
|
||||
|
||||
deviceRolesChart();
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
deviceRolesChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,8 +10,11 @@ async function firmwareVersionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter out nodes without firmware_version
|
||||
const nodesWithVersion = nodes.filter(node => node.firmware_version != null);
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter out nodes without firmware_version and channel match
|
||||
const nodesWithVersion = nodes
|
||||
.filter(node => node.firmware_version != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
const countsByVersion = {};
|
||||
const validVersions = ['<2.5.0', '>2.5.0', '>2.6.8'];
|
||||
@@ -39,7 +42,12 @@ async function firmwareVersionGraph() {
|
||||
const chartContainer = document.getElementById('firmwareVersionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.firmwareVersionChartInstance) {
|
||||
window.firmwareVersionChartInstance.destroy();
|
||||
window.firmwareVersionChartInstance = null;
|
||||
}
|
||||
|
||||
window.firmwareVersionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -67,3 +75,10 @@ async function firmwareVersionGraph() {
|
||||
}
|
||||
|
||||
firmwareVersionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => firmwareVersionGraph());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,8 +11,11 @@ async function isOkToMqttGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter nodes to only include those updated within the last 30 days
|
||||
const recentNodes = nodes.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
@@ -30,7 +33,12 @@ async function isOkToMqttGraph() {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.isOkToMqttChartInstance) {
|
||||
window.isOkToMqttChartInstance.destroy();
|
||||
window.isOkToMqttChartInstance = null;
|
||||
}
|
||||
|
||||
window.isOkToMqttChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['True', 'False'],
|
||||
@@ -77,4 +85,11 @@ async function isOkToMqttGraph() {
|
||||
|
||||
isOkToMqttGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isOkToMqttGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ async function isUnmessagableGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter nodes to only include those updated within the last 30 days
|
||||
const recentNodes = nodes.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
@@ -32,7 +35,12 @@ async function isUnmessagableGraph() {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.isUnmessagableChartInstance) {
|
||||
window.isUnmessagableChartInstance.destroy();
|
||||
window.isUnmessagableChartInstance = null;
|
||||
}
|
||||
|
||||
window.isUnmessagableChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['unmessagable', 'messagable'],
|
||||
@@ -81,4 +89,11 @@ async function isUnmessagableGraph() {
|
||||
}
|
||||
}
|
||||
|
||||
isUnmessagableGraph();
|
||||
isUnmessagableGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isUnmessagableGraph());
|
||||
}
|
||||
});
|
||||
@@ -12,6 +12,8 @@ function getHopsColor(maxHops) {
|
||||
return 'rgb(200, 200, 200)';
|
||||
}
|
||||
|
||||
let maxHopsChartInstance = null;
|
||||
|
||||
async function maxHopsGraph() {
|
||||
const canvas = document.getElementById('maxHopsChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -29,7 +31,13 @@ async function maxHopsGraph() {
|
||||
const countsByHops = {};
|
||||
for (let i = 0; i <= 7; i++) countsByHops[i] = 0;
|
||||
|
||||
const nodesWithMaxHops = nodes.filter(n => n.max_hops !== null && n.max_hops !== undefined);
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!channelId) return true;
|
||||
return n.channel_id === channelId;
|
||||
}) : [];
|
||||
|
||||
const nodesWithMaxHops = filteredNodes.filter(n => n.max_hops !== null && n.max_hops !== undefined);
|
||||
|
||||
for (const node of nodesWithMaxHops) {
|
||||
const val = Number(node.max_hops);
|
||||
@@ -43,7 +51,12 @@ async function maxHopsGraph() {
|
||||
const chartContainer = document.getElementById('maxHopsContainer');
|
||||
if (chartContainer) chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (maxHopsChartInstance) {
|
||||
maxHopsChartInstance.destroy();
|
||||
maxHopsChartInstance = null;
|
||||
}
|
||||
|
||||
maxHopsChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
@@ -75,3 +88,11 @@ async function maxHopsGraph() {
|
||||
maxHopsGraph();
|
||||
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => maxHopsGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +1,94 @@
|
||||
async function mostActiveNodesGraph() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('mostActiveNodes');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
let chartInstance = null;
|
||||
|
||||
// Show "Loading..." message
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes');
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y', // horizontal bars
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 300 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
function getSelectedChannelId() {
|
||||
return (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
}
|
||||
}
|
||||
|
||||
mostActiveNodesGraph();
|
||||
async function renderMostActiveNodes() {
|
||||
// Show "Loading..." message
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
// Build URL with optional channel_id
|
||||
const baseUrl = 'https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes';
|
||||
const channelId = getSelectedChannelId();
|
||||
const url = channelId ? `${baseUrl}?channel_id=${encodeURIComponent(channelId)}` : baseUrl;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
if (chartContainer) {
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
}
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 300 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => renderMostActiveNodes());
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderMostActiveNodes();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
let selectedChannelIdNodes = null; // null = All
|
||||
|
||||
function countNodesSince(daysAgo) {
|
||||
const now = new Date();
|
||||
const threshold = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
||||
return nodes.filter(node => new Date(node.updated_at) >= threshold).length;
|
||||
return nodes
|
||||
.filter(node => new Date(node.updated_at) >= threshold)
|
||||
.filter(node => {
|
||||
if (!selectedChannelIdNodes) return true; // All
|
||||
return node.channel_id === selectedChannelIdNodes;
|
||||
})
|
||||
.length;
|
||||
}
|
||||
|
||||
async function nodesSeen() {
|
||||
@@ -13,3 +21,14 @@ async function nodesSeen() {
|
||||
}
|
||||
|
||||
nodesSeen();
|
||||
|
||||
// Listen for Antal Enheter channel changes (local only)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
nodesSeen();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let highlightedIndex = -1;
|
||||
let chartInstance;
|
||||
let selectedChannelIdPort = null; // null = All
|
||||
|
||||
|
||||
function filterSuggestions(query) {
|
||||
@@ -97,6 +98,21 @@ document.getElementById('clearFilterBtn').addEventListener('click', () => {
|
||||
portnumDistributionChart(); // Reload full chart
|
||||
});
|
||||
|
||||
// Channel filter listeners (mirrors messages/most-active-nodes design)
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdPort = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
const currentNodeName = document.getElementById('nodeSearch').value.trim();
|
||||
if (currentNodeName) {
|
||||
const match = (nodes || []).find(n => n.long_name === currentNodeName);
|
||||
const nodeId = match ? match.node_id : null;
|
||||
portnumDistributionChart(nodeId);
|
||||
} else {
|
||||
portnumDistributionChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main chart function
|
||||
async function portnumDistributionChart(nodeId = null) {
|
||||
const canvas = document.getElementById('portnumDistribution');
|
||||
@@ -110,9 +126,11 @@ async function portnumDistributionChart(nodeId = null) {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const url = nodeId
|
||||
? `https://map.sthlm-mesh.se/api/v1/stats/portnum-counts?nodeId=${nodeId}`
|
||||
: 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const base = 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const params = new URLSearchParams();
|
||||
if (nodeId) params.set('nodeId', nodeId);
|
||||
if (selectedChannelIdPort) params.set('channel_id', selectedChannelIdPort);
|
||||
const url = params.toString() ? `${base}?${params.toString()}` : base;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -77,6 +77,8 @@ function formatPositionPrecision(positionPrecision) {
|
||||
|
||||
}
|
||||
|
||||
let positionPrecisionChartInstance = null;
|
||||
|
||||
async function positionPrecisionGraph() {
|
||||
const canvas = document.getElementById('positionPrecisionChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -90,10 +92,13 @@ async function positionPrecisionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const countsByPrecision = {};
|
||||
|
||||
// Filter out nodes without position_precision
|
||||
const nodesWithPoistion = nodes.filter(node => node.position_precision != null);
|
||||
const nodesWithPoistion = nodes
|
||||
.filter(node => node.position_precision != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
// Count how many nodes fall into each precision value
|
||||
for (const node of nodesWithPoistion) {
|
||||
@@ -116,7 +121,12 @@ async function positionPrecisionGraph() {
|
||||
const chartContainer = document.getElementById('positionPrecisionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (positionPrecisionChartInstance) {
|
||||
positionPrecisionChartInstance.destroy();
|
||||
positionPrecisionChartInstance = null;
|
||||
}
|
||||
|
||||
positionPrecisionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -144,4 +154,11 @@ async function positionPrecisionGraph() {
|
||||
}
|
||||
|
||||
|
||||
positionPrecisionGraph();
|
||||
positionPrecisionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => positionPrecisionGraph());
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,20 @@
|
||||
let nodes = [];
|
||||
let fetchPromise = null;
|
||||
const StatusFilter = {
|
||||
_channelId: null,
|
||||
_subscribers: [],
|
||||
getChannelId() {
|
||||
return this._channelId;
|
||||
},
|
||||
subscribe(callback) {
|
||||
if (typeof callback === 'function') this._subscribers.push(callback);
|
||||
},
|
||||
_notify() {
|
||||
for (const cb of this._subscribers) {
|
||||
try { cb(this._channelId); } catch (_) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchNodes() {
|
||||
if (nodes.length > 0) return;
|
||||
@@ -19,3 +34,16 @@ async function fetchNodes() {
|
||||
|
||||
await fetchPromise;
|
||||
}
|
||||
|
||||
// Initialize global channel filter listeners once
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const radios = document.querySelectorAll("input[name='btnchannel-nodes']");
|
||||
if (!radios || radios.length === 0) return;
|
||||
const checked = document.querySelector("input[name='btnchannel-nodes']:checked");
|
||||
StatusFilter._channelId = checked ? (checked.getAttribute('data-channel') || null) : null;
|
||||
radios.forEach(r => r.addEventListener('change', () => {
|
||||
const channel = r.getAttribute('data-channel');
|
||||
StatusFilter._channelId = channel || null;
|
||||
StatusFilter._notify();
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user