mirror of
https://github.com/Roslund/sthlm-mesh.git
synced 2026-05-08 06:14:57 +02:00
160 lines
6.5 KiB
JavaScript
160 lines
6.5 KiB
JavaScript
async function hardwareStatsGraph() {
|
|
const canvas = document.getElementById('hardwareChart');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Show "Loading..." message
|
|
ctx.font = '16px Arial';
|
|
ctx.fillStyle = 'gray';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
|
|
|
// Manufacturer colors (fixed palette)
|
|
const MANUFACTURER_COLORS = {
|
|
Lilygo: '#3B82F6', // blue-500
|
|
Heltec: '#10B981', // emerald-500
|
|
RakWireless: '#A855F7', // purple-500
|
|
Seeedstudio: '#EAB308', // yellow-500
|
|
Other: '#9CA3AF' // gray-400
|
|
};
|
|
|
|
// Single source of truth: models listed under each manufacturer
|
|
const HARDWARE_MODELS = {
|
|
Heltec: [
|
|
'HELTEC_V3',
|
|
'HELTEC_V4',
|
|
'HELTEC_V2_1',
|
|
'HELTEC_V2_0',
|
|
'HELTEC_WSL_V3',
|
|
'HELTEC_MESH_NODE_T114',
|
|
'HELTEC_MESH_POCKET',
|
|
'HELTEC_WIRELESS_PAPER',
|
|
'HELTEC_WIRELESS_TRACKER'
|
|
],
|
|
RakWireless: [
|
|
'RAK4631',
|
|
'RAK2560',
|
|
'WISMESH_TAG'
|
|
],
|
|
Lilygo: [
|
|
'LILYGO_TBEAM_S3_CORE',
|
|
'TBEAM',
|
|
'TLORA_T3_S3',
|
|
'T_DECK',
|
|
'T_ECHO',
|
|
'T_WATCH_S3'
|
|
],
|
|
Seeedstudio: [
|
|
'SEEED_XIAO_S3',
|
|
'TRACKER_T1000_E',
|
|
'XIAO_NRF52_KIT',
|
|
'SENSECAP_INDICATOR',
|
|
'SEEED_WIO_TRACKER_L1',
|
|
]
|
|
};
|
|
|
|
// Simple lookup without a prebuilt map
|
|
function getManufacturerForModel(modelName) {
|
|
const key = modelName.toUpperCase();
|
|
for (const [manufacturer, models] of Object.entries(HARDWARE_MODELS)) {
|
|
if (models.some(m => m.toUpperCase() === key)) return manufacturer;
|
|
}
|
|
return 'Other';
|
|
}
|
|
|
|
|
|
try {
|
|
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/hardware-models');
|
|
const data = await response.json();
|
|
|
|
const labels = data.hardware_model_stats.map(item => item.hardware_model_name);
|
|
const counts = data.hardware_model_stats.map(item => item.count);
|
|
|
|
// Manufacturers present and interactive filter state
|
|
const presentManufacturers = Array.from(new Set(labels.map(getManufacturerForModel)));
|
|
const enabledManufacturers = new Set(presentManufacturers);
|
|
|
|
// Color bars per model based on manufacturer
|
|
const backgroundColors = labels.map(model => {
|
|
const manu = getManufacturerForModel(model);
|
|
return MANUFACTURER_COLORS[manu] || MANUFACTURER_COLORS.Other;
|
|
});
|
|
|
|
// Adjust chart height based on number of items
|
|
const chartContainer = document.getElementById('hardwareChartContainer');
|
|
chartContainer.style.height = `${labels.length * 25 + 50}px`;
|
|
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Hardware Models',
|
|
data: counts,
|
|
backgroundColor: backgroundColors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
barPercentage: 0.8,
|
|
indexAxis: 'y',
|
|
y: { ticks: { autoSkip: false, font: { size: 12 } } },
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
onClick: (_evt, legendItem) => {
|
|
const manu = legendItem.text;
|
|
if (enabledManufacturers.has(manu)) {
|
|
enabledManufacturers.delete(manu);
|
|
} else {
|
|
enabledManufacturers.add(manu);
|
|
}
|
|
// Rebuild labels, data and colors to remove hidden manufacturers entirely
|
|
const mask = labels.map((model) => enabledManufacturers.has(getManufacturerForModel(model)));
|
|
const newLabels = labels.filter((_, i) => mask[i]);
|
|
const newData = counts.filter((_, i) => mask[i]);
|
|
const newColors = newLabels.map(model => {
|
|
const m = getManufacturerForModel(model);
|
|
return MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other;
|
|
});
|
|
// Adjust chart height to the filtered number of items
|
|
chartContainer.style.height = `${newLabels.length * 25 + 70}px`;
|
|
chart.data.labels = newLabels;
|
|
chart.data.datasets[0].data = newData;
|
|
chart.data.datasets[0].backgroundColor = newColors;
|
|
chart.update();
|
|
},
|
|
labels: {
|
|
generateLabels: (chart) => {
|
|
// Use the full set of manufacturers present in the original data
|
|
const present = presentManufacturers;
|
|
const preferredOrder = ['Heltec', 'RakWireless', 'Lilygo', 'Seeedstudio', 'Other'];
|
|
const ordered = preferredOrder.filter(m => present.includes(m)).concat(
|
|
present.filter(m => !preferredOrder.includes(m))
|
|
);
|
|
return ordered.map((m) => ({
|
|
text: m,
|
|
fillStyle: MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other,
|
|
strokeStyle: MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other,
|
|
lineWidth: 0,
|
|
hidden: !enabledManufacturers.has(m),
|
|
// Needed by Chart.js internals; keep pointing to first dataset
|
|
datasetIndex: 0
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} 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);
|
|
}
|
|
|
|
}
|
|
hardwareStatsGraph(); |