mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-06-27 05:21:19 +02:00
264 lines
8.3 KiB
HTML
264 lines
8.3 KiB
HTML
{% macro graph(name) %}
|
|
<div id="{{name}}Chart" style="width: 100%; height: 100%;"></div>
|
|
{% endmacro %}
|
|
|
|
<!-- Download and Expand buttons -->
|
|
<div class="d-flex justify-content-end mb-2">
|
|
<button class="btn btn-sm btn-outline-light me-2" id="downloadCsvBtn">Download CSV</button>
|
|
<button class="btn btn-sm btn-outline-light" data-bs-toggle="modal" data-bs-target="#fullChartModal">Expand</button>
|
|
</div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
{% for name in [
|
|
"power", "utilization", "temperature", "humidity", "pressure",
|
|
"iaq", "wind_speed", "wind_direction", "power_metrics", "neighbors"
|
|
] %}
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link {% if loop.first %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#{{name}}Tab" type="button" role="tab">{{ name | capitalize }}</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content mt-3" style="height: 40vh;">
|
|
{% for name in [
|
|
"power", "utilization", "temperature", "humidity", "pressure",
|
|
"iaq", "wind_speed", "wind_direction", "power_metrics", "neighbors"
|
|
] %}
|
|
<div class="tab-pane fade {% if loop.first %}show active{% endif %}" id="{{name}}Tab" role="tabpanel" style="height: 100%;">
|
|
{{ graph(name) | safe }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Fullscreen Modal -->
|
|
<div class="modal fade" id="fullChartModal" tabindex="-1" aria-labelledby="fullChartModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content bg-dark text-white">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="fullChartModalLabel">Full Graph</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" style="height: 100vh;">
|
|
<div id="fullChartContainer" style="width: 100%; height: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ECharts Library -->
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
let currentChart = null;
|
|
let currentChartName = null;
|
|
let currentChartData = null;
|
|
let fullChart = null;
|
|
|
|
async function loadChart(name, targetDiv) {
|
|
currentChartName = name;
|
|
const chartDiv = document.getElementById(targetDiv);
|
|
if (!chartDiv) return;
|
|
|
|
try {
|
|
const resp = await fetch(`/graph/${name}_json/{{ node_id }}`);
|
|
if (!resp.ok) throw new Error(`Failed to load data for ${name}`);
|
|
const data = await resp.json();
|
|
|
|
// Reverse for chronological order
|
|
data.timestamps.reverse();
|
|
data.series.forEach(s => s.data.reverse());
|
|
|
|
const formattedDates = data.timestamps.map(t => {
|
|
const d = new Date(t);
|
|
return `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}-${d.getFullYear().toString().slice(-2)}`;
|
|
});
|
|
|
|
currentChartData = {
|
|
...data,
|
|
timestamps: formattedDates
|
|
};
|
|
|
|
const chart = echarts.init(chartDiv);
|
|
|
|
const isDualAxis = name === 'power';
|
|
|
|
chart.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
formatter: function (params) {
|
|
return params.map(p => {
|
|
const label = p.seriesName.toLowerCase();
|
|
const unit = label.includes('volt') ? 'V' : label.includes('battery') ? '%' : '';
|
|
return `${p.marker} ${p.seriesName}: ${p.data}${unit}`;
|
|
}).join('<br>');
|
|
}
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: formattedDates,
|
|
axisLabel: { color: '#fff', rotate: 45 },
|
|
},
|
|
yAxis: isDualAxis ? [
|
|
{
|
|
type: 'value',
|
|
name: 'Battery (%)',
|
|
min: 0,
|
|
max: 120,
|
|
position: 'left',
|
|
axisLabel: { color: '#fff' },
|
|
nameTextStyle: { color: '#fff' }
|
|
},
|
|
{
|
|
type: 'value',
|
|
name: 'Voltage (V)',
|
|
min: 0,
|
|
max: 6,
|
|
position: 'right',
|
|
axisLabel: { color: '#fff' },
|
|
nameTextStyle: { color: '#fff' }
|
|
}
|
|
] : {
|
|
type: 'value',
|
|
axisLabel: { color: '#fff' },
|
|
},
|
|
series: data.series.map(s => ({
|
|
name: s.name,
|
|
type: 'line',
|
|
data: s.data,
|
|
smooth: true,
|
|
connectNulls: true,
|
|
showSymbol: false,
|
|
yAxisIndex: isDualAxis && s.name.toLowerCase().includes('volt') ? 1 : 0,
|
|
})),
|
|
legend: { textStyle: { color: '#fff' } }
|
|
});
|
|
|
|
return chart;
|
|
} catch (err) {
|
|
console.error(err);
|
|
currentChartData = null;
|
|
currentChartName = null;
|
|
chartDiv.innerHTML = `<div class="text-white text-center mt-5">Error loading ${name} data.</div>`;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Load first chart
|
|
const firstTabBtn = document.querySelector('.nav-tabs button.nav-link.active');
|
|
if (firstTabBtn) {
|
|
const name = firstTabBtn.textContent.toLowerCase();
|
|
const chartId = `${name}Chart`;
|
|
loadChart(name, chartId).then(chart => currentChart = chart);
|
|
}
|
|
|
|
// On tab switch
|
|
document.querySelectorAll('.nav-tabs button.nav-link').forEach(button => {
|
|
button.addEventListener('shown.bs.tab', event => {
|
|
const name = event.target.textContent.toLowerCase();
|
|
const chartId = `${name}Chart`;
|
|
loadChart(name, chartId).then(chart => currentChart = chart);
|
|
});
|
|
});
|
|
|
|
// CSV Download
|
|
document.getElementById('downloadCsvBtn').addEventListener('click', () => {
|
|
if (!currentChartData || !currentChartName) {
|
|
alert("Chart data not loaded yet.");
|
|
return;
|
|
}
|
|
|
|
const { timestamps, series } = currentChartData;
|
|
let csv = 'Date,' + series.map(s => s.name).join(',') + '\n';
|
|
|
|
for (let i = 0; i < timestamps.length; i++) {
|
|
const row = [timestamps[i]];
|
|
for (const s of series) {
|
|
row.push(s.data[i] != null ? s.data[i] : '');
|
|
}
|
|
csv += row.join(',') + '\n';
|
|
}
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${currentChartName}_{{ node_id }}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
// Fullscreen modal chart
|
|
document.getElementById('fullChartModal').addEventListener('shown.bs.modal', () => {
|
|
if (!currentChartData || !currentChartName) return;
|
|
|
|
if (!fullChart) {
|
|
fullChart = echarts.init(document.getElementById('fullChartContainer'));
|
|
}
|
|
|
|
const isDualAxis = currentChartName === 'power';
|
|
|
|
fullChart.setOption({
|
|
title: { text: currentChartName.charAt(0).toUpperCase() + currentChartName.slice(1), textStyle: { color: '#fff' } },
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
formatter: function (params) {
|
|
return params.map(p => {
|
|
const label = p.seriesName.toLowerCase();
|
|
const unit = label.includes('volt') ? 'V' : label.includes('battery') ? '%' : '';
|
|
return `${p.marker} ${p.seriesName}: ${p.data}${unit}`;
|
|
}).join('<br>');
|
|
}
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: currentChartData.timestamps,
|
|
axisLabel: { color: '#fff', rotate: 45 },
|
|
},
|
|
yAxis: isDualAxis ? [
|
|
{
|
|
type: 'value',
|
|
name: 'Battery (%)',
|
|
min: 0,
|
|
max: 120,
|
|
position: 'left',
|
|
axisLabel: { color: '#fff' },
|
|
nameTextStyle: { color: '#fff' }
|
|
},
|
|
{
|
|
type: 'value',
|
|
name: 'Voltage (V)',
|
|
min: 0,
|
|
max: 6,
|
|
position: 'right',
|
|
axisLabel: { color: '#fff' },
|
|
nameTextStyle: { color: '#fff' }
|
|
}
|
|
] : {
|
|
type: 'value',
|
|
axisLabel: { color: '#fff' },
|
|
},
|
|
series: currentChartData.series.map(s => ({
|
|
name: s.name,
|
|
type: 'line',
|
|
data: s.data,
|
|
smooth: true,
|
|
connectNulls: true,
|
|
showSymbol: false,
|
|
yAxisIndex: isDualAxis && s.name.toLowerCase().includes('volt') ? 1 : 0,
|
|
})),
|
|
legend: { textStyle: { color: '#fff' } }
|
|
});
|
|
|
|
fullChart.resize();
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (fullChart) fullChart.resize();
|
|
if (currentChart) currentChart.resize();
|
|
});
|
|
});
|
|
</script>
|