From 32437584a9b84c7ac14f6fb862ea735b95e3737e Mon Sep 17 00:00:00 2001 From: Pablo Revilla Date: Wed, 21 May 2025 11:56:17 -0700 Subject: [PATCH] Modified all graphs to eliminate the errors and use eCharts --- meshview/templates/node_graphs.html | 69 ++++++---- meshview/web.py | 196 ---------------------------- 2 files changed, 43 insertions(+), 222 deletions(-) diff --git a/meshview/templates/node_graphs.html b/meshview/templates/node_graphs.html index c4b2736..a41c8cb 100644 --- a/meshview/templates/node_graphs.html +++ b/meshview/templates/node_graphs.html @@ -1,3 +1,4 @@ + {% macro graph(name) %}
{% 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(); }); }); diff --git a/meshview/web.py b/meshview/web.py index b277bee..3d06c96 100644 --- a/meshview/web.py +++ b/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(