Implement infinite scroll for logs table
This commit is contained in:
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
# File: logs_view.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
|
||||||
|
|
||||||
@@ -70,5 +70,7 @@ def get_logs_table_component():
|
|||||||
'textAlign': 'center',
|
'textAlign': 'center',
|
||||||
'fontFamily': 'Segoe UI, Arial, sans-serif'
|
'fontFamily': 'Segoe UI, Arial, sans-serif'
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
html.Button("Load More", id="load-more-logs-btn", n_clicks=0, style={'display': 'none'}),
|
||||||
|
html.Div(id='dummy-output', style={'display': 'none'})
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
|
|
||||||
import polars as pl
|
import polars as pl
|
||||||
import dash
|
import dash
|
||||||
from dash import dcc, html, Input, Output
|
from dash import dcc, html, Input, Output, State
|
||||||
import dash_bootstrap_components as dbc
|
import dash_bootstrap_components as dbc
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -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.view import prepare_logs_data, get_logs_table_component
|
from logs.view import get_logs_table_component, prepare_logs_data
|
||||||
|
|
||||||
RAW_LOG_DIR = "data/logs"
|
RAW_LOG_DIR = "data/logs"
|
||||||
|
|
||||||
@@ -86,7 +86,6 @@ 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 = {}
|
||||||
@@ -177,31 +176,85 @@ app.layout = dbc.Container([
|
|||||||
], id="main-tabs", active_tab="statistics")
|
], id="main-tabs", active_tab="statistics")
|
||||||
], fluid=True)
|
], fluid=True)
|
||||||
|
|
||||||
@app.callback(
|
app.clientside_callback(
|
||||||
[Output('logs-table', 'data'),
|
"""
|
||||||
Output('logs-table', 'columns'),
|
function(data) {
|
||||||
Output('logs-info-text', 'children')],
|
if (!data) return '';
|
||||||
Input('logs-vehicle-selector', 'value'),
|
setTimeout(function() {
|
||||||
Input('logs-bus-selector', 'value')
|
const btn = document.getElementById('load-more-logs-btn');
|
||||||
|
const info = document.getElementById('logs-info-text');
|
||||||
|
if (!btn || !info || info.innerText.toLowerCase().includes('all')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.dataset.loading = "false";
|
||||||
|
|
||||||
|
const containers = document.querySelectorAll('.dash-spreadsheet-container, .dash-spreadsheet-inner');
|
||||||
|
if (containers.length === 0) return;
|
||||||
|
|
||||||
|
containers.forEach(container => {
|
||||||
|
container.onscroll = function() {
|
||||||
|
if (container.scrollHeight - container.scrollTop - container.clientHeight < 200) {
|
||||||
|
if (btn && btn.dataset.loading === "false") {
|
||||||
|
btn.dataset.loading = "true";
|
||||||
|
btn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (container.scrollHeight - container.scrollTop - container.clientHeight < 200) {
|
||||||
|
if (btn && btn.dataset.loading === "false") {
|
||||||
|
btn.dataset.loading = "true";
|
||||||
|
btn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
Output('dummy-output', 'children'),
|
||||||
|
Input('logs-table', 'data')
|
||||||
)
|
)
|
||||||
def update_logs_table(vehicle, bus):
|
|
||||||
|
@app.callback(
|
||||||
|
Output('logs-table', 'data'),
|
||||||
|
Output('logs-table', 'columns'),
|
||||||
|
Output('logs-info-text', 'children'),
|
||||||
|
Input('logs-vehicle-selector', 'value'),
|
||||||
|
Input('logs-bus-selector', 'value'),
|
||||||
|
Input('load-more-logs-btn', 'n_clicks'),
|
||||||
|
State('logs-table', 'data'),
|
||||||
|
)
|
||||||
|
def update_logs_table(vehicle, bus, n_clicks, current_data):
|
||||||
|
ctx = dash.callback_context
|
||||||
|
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else ''
|
||||||
|
|
||||||
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 [], [], "No data available"
|
return [], [], "No data available"
|
||||||
|
|
||||||
cache_key = (vehicle, bus)
|
df = DATA[vehicle][bus]
|
||||||
if cache_key not in LOGS_CACHE:
|
prepared_df = prepare_logs_data(df)
|
||||||
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)
|
total_rows = len(prepared_df)
|
||||||
columns = [{"name": col, "id": col} for col in display_df.columns]
|
columns = [{"name": i, "id": i} for i in prepared_df.columns]
|
||||||
data = display_df.to_dict('records')
|
|
||||||
|
|
||||||
info_text = f"Displaying {total_rows} frames."
|
if trigger_id in ['logs-vehicle-selector', 'logs-bus-selector', '']:
|
||||||
|
current_data = []
|
||||||
|
|
||||||
return data, columns, info_text
|
offset = len(current_data) if current_data else 0
|
||||||
|
chunk_size = 1000
|
||||||
|
|
||||||
|
if offset >= total_rows:
|
||||||
|
return current_data, columns, f"Displaying all {total_rows} total frames."
|
||||||
|
|
||||||
|
next_chunk = prepared_df.iloc[offset:offset + chunk_size].to_dict('records')
|
||||||
|
new_data = current_data + next_chunk
|
||||||
|
new_offset = len(new_data)
|
||||||
|
|
||||||
|
info_text = f"Displaying {new_offset} of {total_rows} total frames."
|
||||||
|
|
||||||
|
return new_data, columns, info_text
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output('tab-content', 'children'),
|
Output('tab-content', 'children'),
|
||||||
|
|||||||
Reference in New Issue
Block a user