diff --git a/logs/logs.py b/logs/view.py similarity index 52% rename from logs/logs.py rename to logs/view.py index 387f946..8b4f83e 100644 --- a/logs/logs.py +++ b/logs/view.py @@ -1,14 +1,13 @@ -# File: logs.py +# File: logs_view.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 +from dash import html, dash_table -def render_logs_table(df: pd.DataFrame): +def prepare_logs_data(df: pd.DataFrame) -> pd.DataFrame: if df is None or df.empty: - return html.Div("No data available") + return pd.DataFrame() 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_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=["" + str(c) + "" 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 display_df.fillna("") +def get_logs_table_component(): 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'}) + html.Div(id='logs-info-text', className="text-muted mb-2"), + 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' + } + ) ]) diff --git a/main.py b/main.py index ed62910..fcfb9ea 100644 --- a/main.py +++ b/main.py @@ -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.correlation import calculate_correlation, plot_correlation_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" @@ -86,6 +86,7 @@ for log_file in Path(RAW_LOG_DIR).glob("*.txt"): PRECOMPUTED_FIGURES = {} DATA_BY_ID = {} CORR_CACHE = {} +LOGS_CACHE = {} def process_bus_data(vehicle, bus, df): precomp = {} @@ -146,7 +147,7 @@ app.layout = dbc.Container([ clearable=False ), width=2), ], 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.Row([ @@ -177,14 +178,30 @@ app.layout = dbc.Container([ ], fluid=True) @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-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]) + return [], [], "No data available" + + 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( Output('tab-content', 'children'),