Add log visualization tab to dashboard
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# File: logs.py
|
||||
# Copyright (C) 2026 Erick Ahmed
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from dash import html, dcc
|
||||
|
||||
def render_logs_table(df: pd.DataFrame):
|
||||
if df is None or df.empty:
|
||||
return html.Div("No data available")
|
||||
|
||||
df = df.copy()
|
||||
|
||||
if 'j1939_metadata' in df.columns:
|
||||
df['Priority'] = df['j1939_metadata'].apply(lambda x: x.get('Priority') if isinstance(x, dict) else None)
|
||||
df['PF'] = df['j1939_metadata'].apply(lambda x: x.get('PF') if isinstance(x, dict) else None)
|
||||
df['PS'] = df['j1939_metadata'].apply(lambda x: x.get('PS') if isinstance(x, dict) else None)
|
||||
df['SA'] = df['j1939_metadata'].apply(lambda x: x.get('SA') if isinstance(x, dict) else None)
|
||||
df['DA'] = df['j1939_metadata'].apply(lambda x: x.get('DA') if isinstance(x, dict) else None)
|
||||
df['PGN'] = df['j1939_metadata'].apply(lambda x: x.get('PGN') if isinstance(x, dict) else None)
|
||||
else:
|
||||
for col in ['Priority', 'PF', 'PS', 'SA', 'DA', 'PGN']:
|
||||
df[col] = None
|
||||
|
||||
for i in range(8):
|
||||
col = f'b{i}'
|
||||
if col in df.columns:
|
||||
df[col] = df[col].apply(lambda x: f"{int(x):02X}" if pd.notna(x) else "")
|
||||
else:
|
||||
df[col] = ""
|
||||
|
||||
if 'ID' in df.columns:
|
||||
df['ID'] = df['ID'].astype(str)
|
||||
|
||||
display_cols = ['Timestamp', 'ID', 'DLC', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'Priority', 'PF', 'PS', 'SA', 'DA', 'PGN']
|
||||
display_df = df[[c for c in display_cols if c in df.columns]]
|
||||
|
||||
display_df = display_df.fillna("")
|
||||
|
||||
max_rows = 1000
|
||||
total_rows = len(display_df)
|
||||
if total_rows > max_rows:
|
||||
display_df = display_df.iloc[:max_rows]
|
||||
|
||||
fig = go.Figure(data=[go.Table(
|
||||
header=dict(
|
||||
values=["<b>" + str(c) + "</b>" for c in display_df.columns],
|
||||
fill_color='#1a1a1a',
|
||||
font=dict(color='white', size=12),
|
||||
align='center'
|
||||
),
|
||||
cells=dict(
|
||||
values=[display_df[col] for col in display_df.columns],
|
||||
fill_color='#f8f9fa',
|
||||
font=dict(color='#2a2a2a', size=11),
|
||||
align='center'
|
||||
)
|
||||
)])
|
||||
|
||||
fig.update_layout(
|
||||
height=800,
|
||||
margin=dict(l=0, r=0, t=10, b=0)
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
html.Div(f"Displaying first {len(display_df)} of {total_rows} total frames.", className="text-muted mb-2"),
|
||||
dcc.Graph(figure=fig, style={'height': '80vh'})
|
||||
])
|
||||
@@ -19,6 +19,7 @@ from stats.id_viewer import _format_can_id_vec, plot_bits
|
||||
from stats.frequency import calculate_frequency, plot_frequency
|
||||
from stats.correlation import calculate_correlation, plot_correlation_heatmap
|
||||
from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap
|
||||
from logs import render_logs_table
|
||||
|
||||
RAW_LOG_DIR = "data/logs"
|
||||
|
||||
@@ -128,6 +129,25 @@ app.layout = dbc.Container([
|
||||
dbc.Tab(label="Overview", tab_id="overview", children=[
|
||||
html.Div(id="overview-content")
|
||||
]),
|
||||
dbc.Tab(label="Logs", tab_id="logs", children=[
|
||||
dbc.Row([
|
||||
dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"),
|
||||
dbc.Col(dcc.Dropdown(
|
||||
id='logs-vehicle-selector',
|
||||
options=[{'label': v, 'value': v} for v in DATA.keys()],
|
||||
value=list(DATA.keys())[0] if DATA else None,
|
||||
clearable=False
|
||||
), width=3, className="me-4"),
|
||||
dbc.Col(html.Label("Select Bus:", className="mt-2"), width="auto"),
|
||||
dbc.Col(dcc.Dropdown(
|
||||
id='logs-bus-selector',
|
||||
options=[{'label': 'Bus 1', 'value': 'Bus 1'}, {'label': 'Bus 2', 'value': 'Bus 2'}],
|
||||
value='Bus 1',
|
||||
clearable=False
|
||||
), width=2),
|
||||
], className="mb-3 mt-3", align="end"),
|
||||
html.Div(id='logs-table-container')
|
||||
]),
|
||||
dbc.Tab(label="Statistics", tab_id="statistics", children=[
|
||||
dbc.Row([
|
||||
dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"),
|
||||
@@ -156,6 +176,16 @@ app.layout = dbc.Container([
|
||||
], id="main-tabs", active_tab="statistics")
|
||||
], fluid=True)
|
||||
|
||||
@app.callback(
|
||||
Output('logs-table-container', 'children'),
|
||||
Input('logs-vehicle-selector', 'value'),
|
||||
Input('logs-bus-selector', 'value')
|
||||
)
|
||||
def update_logs_table(vehicle, bus):
|
||||
if not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]:
|
||||
return html.Div("No data available")
|
||||
return render_logs_table(DATA[vehicle][bus])
|
||||
|
||||
@app.callback(
|
||||
Output('tab-content', 'children'),
|
||||
Input('tabs', 'active_tab'),
|
||||
|
||||
Reference in New Issue
Block a user