mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Modified all graphs to eliminate the errors and use eCharts
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
<!-- Macro -->
|
||||
{% macro graph(name) %}
|
||||
<div id="{{name}}Chart" style="width: 100%; height: 100%;"></div>
|
||||
{% endmacro %}
|
||||
@@ -55,31 +56,40 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
let currentChart = null;
|
||||
let currentChartName = null;
|
||||
let currentChartData = null;
|
||||
let fullChart = null;
|
||||
|
||||
async function loadChart(name) {
|
||||
async function loadChart(name, targetDiv) {
|
||||
currentChartName = name;
|
||||
const chartDiv = document.getElementById(`${name}Chart`);
|
||||
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();
|
||||
currentChartData = data;
|
||||
|
||||
if (!currentChart) {
|
||||
currentChart = echarts.init(chartDiv);
|
||||
} else if (currentChart.getDom() !== chartDiv) {
|
||||
currentChart.dispose();
|
||||
currentChart = echarts.init(chartDiv);
|
||||
}
|
||||
// Reverse to go left-to-right
|
||||
data.timestamps.reverse();
|
||||
data.series.forEach(s => s.data.reverse());
|
||||
|
||||
currentChart.setOption({
|
||||
// Format timestamps as MM-DD-YY
|
||||
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);
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.timestamps,
|
||||
axisLabel: { color: '#fff' },
|
||||
data: formattedDates,
|
||||
axisLabel: { color: '#fff', rotate: 45 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -91,38 +101,46 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
data: s.data,
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
showSymbol: false,
|
||||
})),
|
||||
legend: { textStyle: { color: '#fff' } }
|
||||
});
|
||||
|
||||
return chart;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
currentChartData = null;
|
||||
currentChartName = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Load first tab chart initially
|
||||
// Load the first chart
|
||||
const firstTabBtn = document.querySelector('.nav-tabs button.nav-link.active');
|
||||
if (firstTabBtn) {
|
||||
loadChart(firstTabBtn.textContent.toLowerCase());
|
||||
const name = firstTabBtn.textContent.toLowerCase();
|
||||
const chartId = `${name}Chart`;
|
||||
loadChart(name, chartId).then(chart => currentChart = chart);
|
||||
}
|
||||
|
||||
// Listen for tab changes to load charts
|
||||
// Tab change event
|
||||
document.querySelectorAll('.nav-tabs button.nav-link').forEach(button => {
|
||||
button.addEventListener('shown.bs.tab', event => {
|
||||
const tabName = event.target.textContent.toLowerCase();
|
||||
loadChart(tabName);
|
||||
const name = event.target.textContent.toLowerCase();
|
||||
const chartId = `${name}Chart`;
|
||||
loadChart(name, chartId).then(chart => currentChart = chart);
|
||||
});
|
||||
});
|
||||
|
||||
// Download CSV button handler for current chart
|
||||
// Download CSV button
|
||||
document.getElementById('downloadCsvBtn').addEventListener('click', () => {
|
||||
if (!currentChartData || !currentChartName) {
|
||||
alert("Chart data not loaded yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { timestamps, series } = currentChartData;
|
||||
let csv = 'Time,' + series.map(s => s.name).join(',') + '\n';
|
||||
let csv = 'Date,' + series.map(s => s.name).join(',') + '\n';
|
||||
|
||||
for (let i = 0; i < timestamps.length; i++) {
|
||||
const row = [timestamps[i]];
|
||||
@@ -141,15 +159,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// Expand modal logic
|
||||
const fullChartContainer = document.getElementById('fullChartContainer');
|
||||
let fullChart = null;
|
||||
const modal = document.getElementById('fullChartModal');
|
||||
modal.addEventListener('shown.bs.modal', () => {
|
||||
// Expand chart modal
|
||||
document.getElementById('fullChartModal').addEventListener('shown.bs.modal', () => {
|
||||
if (!currentChartData || !currentChartName) return;
|
||||
|
||||
if (!fullChart) {
|
||||
fullChart = echarts.init(fullChartContainer);
|
||||
fullChart = echarts.init(document.getElementById('fullChartContainer'));
|
||||
}
|
||||
|
||||
fullChart.setOption({
|
||||
@@ -158,7 +173,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: currentChartData.timestamps,
|
||||
axisLabel: { color: '#fff' },
|
||||
axisLabel: { color: '#fff', rotate: 45 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -170,6 +185,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
data: s.data,
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
showSymbol: false,
|
||||
})),
|
||||
legend: { textStyle: { color: '#fff' } }
|
||||
});
|
||||
@@ -179,6 +195,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (fullChart) fullChart.resize();
|
||||
if (currentChart) currentChart.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
196
meshview/web.py
196
meshview/web.py
@@ -403,202 +403,6 @@ async def packet(request):
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
|
||||
async def graph_telemetry(node_id, payload_type, graph_config):
|
||||
data = {'date': []}
|
||||
fields = []
|
||||
for c in graph_config:
|
||||
fields.extend(c['fields'])
|
||||
|
||||
for field in fields:
|
||||
data[field] = []
|
||||
|
||||
for p in await store.get_packets_from(node_id, PortNum.TELEMETRY_APP):
|
||||
_, payload = decode_payload.decode(p)
|
||||
if not payload:
|
||||
continue
|
||||
if not payload.HasField(payload_type):
|
||||
continue
|
||||
data_field = getattr(payload, payload_type)
|
||||
timestamp = p.import_time
|
||||
data['date'].append(timestamp)
|
||||
for field in fields:
|
||||
data[field].append(getattr(data_field, field))
|
||||
|
||||
if not data['date']:
|
||||
return web.Response(
|
||||
body=empty_png,
|
||||
status=404,
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
max_time = datetime.timedelta(days=4)
|
||||
newest = data['date'][0]
|
||||
for i, d in enumerate(data['date']):
|
||||
if d < newest - max_time:
|
||||
break
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
fig.autofmt_xdate()
|
||||
ax.set_xlabel('time')
|
||||
axes = {0: ax}
|
||||
|
||||
date = data.pop('date')
|
||||
df = DataFrame(data, index=date)
|
||||
|
||||
for i, ax_config in enumerate(graph_config):
|
||||
args = {}
|
||||
if 'color' in ax_config:
|
||||
args['color'] = 'tab:' + ax_config['color']
|
||||
if i:
|
||||
ax = ax.twinx()
|
||||
ax.set_ylabel(ax_config['label'], **args)
|
||||
ax_df = df[ax_config['fields']]
|
||||
args = {}
|
||||
if 'palette' in ax_config:
|
||||
args['palette'] = ax_config['palette']
|
||||
sns.lineplot(data=ax_df, ax=ax, **args)
|
||||
|
||||
png = io.BytesIO()
|
||||
plt.savefig(png, dpi=100)
|
||||
plt.close()
|
||||
|
||||
return web.Response(
|
||||
body=png.getvalue(),
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/power/{node_id}")
|
||||
async def graph_power(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'device_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'battery level',
|
||||
'fields': ['battery_level'],
|
||||
},
|
||||
{
|
||||
'label': 'voltage',
|
||||
'fields': ['voltage'],
|
||||
'palette': 'Set2',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/chutil/{node_id}")
|
||||
async def graph_chutil(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'device_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'utilization',
|
||||
'fields': ['channel_utilization', 'air_util_tx'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/wind_speed/{node_id}")
|
||||
async def graph_wind_speed(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'wind speed m/s',
|
||||
'fields': ['wind_speed'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/wind_direction/{node_id}")
|
||||
async def graph_wind_direction(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'wind direction',
|
||||
'fields': ['wind_direction'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/temperature/{node_id}")
|
||||
async def graph_temperature(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'temperature C',
|
||||
'fields': ['temperature'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/humidity/{node_id}")
|
||||
async def graph_humidity(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'humidity',
|
||||
'fields': ['relative_humidity'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/pressure/{node_id}")
|
||||
async def graph_pressure(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'barometric pressure',
|
||||
'fields': ['barometric_pressure'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/iaq/{node_id}")
|
||||
async def graph_pressure(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'IAQ',
|
||||
'fields': ['iaq'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/power_metrics/{node_id}")
|
||||
async def graph_power_metrics(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'power_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'voltage',
|
||||
'fields': ['ch1_voltage', 'ch2_voltage', 'ch3_voltage'],
|
||||
},
|
||||
{
|
||||
'label': 'current',
|
||||
'fields': ['ch1_current', 'ch2_current', 'ch3_current'],
|
||||
'palette': 'Set2',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/power_json/{node_id}")
|
||||
async def graph_power_json(request):
|
||||
return await graph_telemetry_json(
|
||||
|
||||
Reference in New Issue
Block a user