Replace Plotly graph-based tables with Dash DataTable
This commit is contained in:
+38
-33
@@ -1,14 +1,13 @@
|
|||||||
# File: logs.py
|
# File: logs_view.py
|
||||||
# Copyright (C) 2026 Erick Ahmed
|
# Copyright (C) 2026 Erick Ahmed
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import plotly.graph_objects as go
|
from dash import html, dash_table
|
||||||
from dash import html, dcc
|
|
||||||
|
|
||||||
def render_logs_table(df: pd.DataFrame):
|
def prepare_logs_data(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return html.Div("No data available")
|
return pd.DataFrame()
|
||||||
|
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
|
|
||||||
@@ -36,34 +35,40 @@ def render_logs_table(df: pd.DataFrame):
|
|||||||
display_cols = ['Timestamp', 'ID', 'DLC', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'Priority', 'PF', 'PS', 'SA', 'DA', 'PGN']
|
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 = df[[c for c in display_cols if c in df.columns]]
|
||||||
|
|
||||||
display_df = display_df.fillna("")
|
return 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def get_logs_table_component():
|
||||||
return html.Div([
|
return html.Div([
|
||||||
html.Div(f"Displaying first {len(display_df)} of {total_rows} total frames.", className="text-muted mb-2"),
|
html.Div(id='logs-info-text', className="text-muted mb-2"),
|
||||||
dcc.Graph(figure=fig, style={'height': '80vh'})
|
dash_table.DataTable(
|
||||||
|
id='logs-table',
|
||||||
|
virtualization=True,
|
||||||
|
page_action='none',
|
||||||
|
style_table={'overflowX': 'auto', 'height': '75vh', 'overflowY': 'auto'},
|
||||||
|
style_header={
|
||||||
|
'backgroundColor': '#1a1a1a',
|
||||||
|
'color': 'white',
|
||||||
|
'fontWeight': 'bold',
|
||||||
|
'textAlign': 'center',
|
||||||
|
'position': 'sticky',
|
||||||
|
'top': 0
|
||||||
|
},
|
||||||
|
style_data={
|
||||||
|
'backgroundColor': '#f8f9fa',
|
||||||
|
'color': '#2a2a2a',
|
||||||
|
'textAlign': 'center'
|
||||||
|
},
|
||||||
|
style_data_conditional=[
|
||||||
|
{
|
||||||
|
'if': {'row_index': 'odd'},
|
||||||
|
'backgroundColor': 'rgb(240, 240, 240)'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
style_cell={
|
||||||
|
'minWidth': '80px',
|
||||||
|
'padding': '5px',
|
||||||
|
'textAlign': 'center',
|
||||||
|
'fontFamily': 'Segoe UI, Arial, sans-serif'
|
||||||
|
}
|
||||||
|
)
|
||||||
])
|
])
|
||||||
@@ -19,7 +19,7 @@ from stats.id_viewer import _format_can_id_vec, plot_bits
|
|||||||
from stats.frequency import calculate_frequency, plot_frequency
|
from stats.frequency import calculate_frequency, plot_frequency
|
||||||
from stats.correlation import calculate_correlation, plot_correlation_heatmap
|
from stats.correlation import calculate_correlation, plot_correlation_heatmap
|
||||||
from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap
|
from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap
|
||||||
from logs import render_logs_table
|
from logs.view import prepare_logs_data, get_logs_table_component
|
||||||
|
|
||||||
RAW_LOG_DIR = "data/logs"
|
RAW_LOG_DIR = "data/logs"
|
||||||
|
|
||||||
@@ -86,6 +86,7 @@ for log_file in Path(RAW_LOG_DIR).glob("*.txt"):
|
|||||||
PRECOMPUTED_FIGURES = {}
|
PRECOMPUTED_FIGURES = {}
|
||||||
DATA_BY_ID = {}
|
DATA_BY_ID = {}
|
||||||
CORR_CACHE = {}
|
CORR_CACHE = {}
|
||||||
|
LOGS_CACHE = {}
|
||||||
|
|
||||||
def process_bus_data(vehicle, bus, df):
|
def process_bus_data(vehicle, bus, df):
|
||||||
precomp = {}
|
precomp = {}
|
||||||
@@ -146,7 +147,7 @@ app.layout = dbc.Container([
|
|||||||
clearable=False
|
clearable=False
|
||||||
), width=2),
|
), width=2),
|
||||||
], className="mb-3 mt-3", align="end"),
|
], className="mb-3 mt-3", align="end"),
|
||||||
html.Div(id='logs-table-container')
|
get_logs_table_component()
|
||||||
]),
|
]),
|
||||||
dbc.Tab(label="Statistics", tab_id="statistics", children=[
|
dbc.Tab(label="Statistics", tab_id="statistics", children=[
|
||||||
dbc.Row([
|
dbc.Row([
|
||||||
@@ -177,14 +178,30 @@ app.layout = dbc.Container([
|
|||||||
], fluid=True)
|
], fluid=True)
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output('logs-table-container', 'children'),
|
[Output('logs-table', 'data'),
|
||||||
|
Output('logs-table', 'columns'),
|
||||||
|
Output('logs-info-text', 'children')],
|
||||||
Input('logs-vehicle-selector', 'value'),
|
Input('logs-vehicle-selector', 'value'),
|
||||||
Input('logs-bus-selector', 'value')
|
Input('logs-bus-selector', 'value')
|
||||||
)
|
)
|
||||||
def update_logs_table(vehicle, bus):
|
def update_logs_table(vehicle, bus):
|
||||||
if not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]:
|
if not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]:
|
||||||
return html.Div("No data available")
|
return [], [], "No data available"
|
||||||
return render_logs_table(DATA[vehicle][bus])
|
|
||||||
|
cache_key = (vehicle, bus)
|
||||||
|
if cache_key not in LOGS_CACHE:
|
||||||
|
display_df = prepare_logs_data(DATA[vehicle][bus])
|
||||||
|
LOGS_CACHE[cache_key] = display_df
|
||||||
|
else:
|
||||||
|
display_df = LOGS_CACHE[cache_key]
|
||||||
|
|
||||||
|
total_rows = len(display_df)
|
||||||
|
columns = [{"name": col, "id": col} for col in display_df.columns]
|
||||||
|
data = display_df.to_dict('records')
|
||||||
|
|
||||||
|
info_text = f"Displaying {total_rows} frames."
|
||||||
|
|
||||||
|
return data, columns, info_text
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output('tab-content', 'children'),
|
Output('tab-content', 'children'),
|
||||||
|
|||||||
Reference in New Issue
Block a user