# File: frequency.py # Copyright (C) 2026 Erick Ahmed # SPDX-License-Identifier: AGPL-3.0-or-later import argparse from pathlib import Path import pandas as pd import plotly.express as px import plotly.graph_objects as go from utils.extractor import load_data def calc_freq(df: pd.DataFrame) -> pd.DataFrame: """Calculates frequency counts and percentages for identifiers.""" freq_df = df['Identifier'].value_counts().reset_index() freq_df.columns = ['Identifier', 'Count'] total = freq_df['Count'].sum() freq_df['Percentage'] = (freq_df['Count'] / total * 100).round(2) return freq_df.sort_values('Count', ascending=True) def plot_freq(stats_df: pd.DataFrame, title: str) -> go.Figure: """Generates interactive horizontal bar chart with log x-axis.""" fig = px.bar( stats_df, y='Identifier', x='Count', orientation='h', title=title, log_x=True, labels={'Identifier': 'PGN / CAN ID', 'Count': 'Message Count'}, color='Count', color_continuous_scale='Turbo', range_color=(stats_df['Count'].min(), stats_df['Count'].max()), hover_data={'Percentage': ':.2f', 'Count': ':,', 'Identifier': True} ) fig.update_layout( height=max(600, len(stats_df) * 18), autosize=True, template='plotly_white', xaxis=dict( title=dict(text="Message count [log scale]", font=dict(size=13, color="#1a1a1a")), side="top", dtick=1, showgrid=False, linecolor="#bdbdbd", tickfont=dict(size=12, color="#2a2a2a"), ticks="outside", ticklen=4, tickcolor="#cccccc", ), yaxis=dict( title=dict(text="PGN or CAN ID", font=dict(size=13, color="#1a1a1a")), #autorange="", showgrid=False, linecolor="#bdbdbd", tickfont=dict(size=12, color="#2a2a2a"), ticks="outside", ticklen=4, tickcolor="#cccccc", automargin=True, ), font=dict(family="Segoe UI, Arial, sans-serif", size=12, color='#2a2a2a'), hoverlabel=dict(bgcolor="white", font_size=13, font_family="Segoe UI", bordercolor='#cccccc'), margin=dict(l=200, r=40, t=120, b=60), bargap=0.35, coloraxis_colorbar=dict( title=dict(text='Message Count', side='top'), orientation='h', thickness=15, len=0.35, x=1.0, xanchor='right', y=1.02, yanchor='bottom', tickformat=',', outlinecolor='#cccccc', outlinewidth=0.5 ), title=dict(font=dict(size=20, color='#1a1a1a'), x=0.5, xanchor='center', pad=dict(b=20)) ) fig.update_xaxes( showgrid=True, gridwidth=0.5, gridcolor='#e8e8e8', zeroline=False, linecolor='#bdbdbd', mirror=False, tickformat=',', minor=dict(showgrid=True, gridcolor='#f4f4f4', gridwidth=0.5) ) fig.update_yaxes( showgrid=False, zeroline=False, linecolor='#bdbdbd', ticks='outside', ticklen=4, tickcolor='#cccccc', automargin=True ) fig.update_traces( hovertemplate="%{y}
Count: %{x:,}
Share: %{customdata[0]}%", marker_line_width=0, texttemplate='%{x:,}', textposition='outside', textfont=dict(size=10, color='#666666'), cliponaxis=False, selected=dict(marker=dict(opacity=0.6)), unselected=dict(marker=dict(opacity=0.2)) ) return fig if __name__ == "__main__": parser = argparse.ArgumentParser(description="Analyze CAN bus message frequency") parser.add_argument("input", type=Path, help="Path to the input CAN log file") parser.add_argument("output", type=Path, nargs="?", default=Path("freq_report.html"), help="Path to the output HTML report") parser.add_argument("title", nargs="?", default="CAN Bus Message Frequency", help="Title for the HTML report") args = parser.parse_args() df = load_data(args.input) stats = calc_freq(df) fig = plot_freq(stats, title=args.title) config = { 'responsive': True, 'displaylogo': False, 'scrollZoom': True, 'modeBarButtonsToAdd': ['toggleSpikelines'], 'toImageButtonOptions': {'format': 'png', 'scale': 2} } fig.write_html(str(args.output), include_plotlyjs='cdn', config=config)