diff --git a/dash_app/components/form_inputs.py b/dash_app/components/form_inputs.py index 2ceb795..770be80 100644 --- a/dash_app/components/form_inputs.py +++ b/dash_app/components/form_inputs.py @@ -357,6 +357,15 @@ def manual_filename_input(id): ) +def normalize_scores_line_level(id): + return dbc.Col( + dbc.Checkbox( + id=id, + label="Normalize scores", + ), + ) + + def redirect_to_results_input(id): return dbc.Col( [ diff --git a/dash_app/components/forms.py b/dash_app/components/forms.py index 285329d..0c01955 100644 --- a/dash_app/components/forms.py +++ b/dash_app/components/forms.py @@ -23,6 +23,7 @@ columns_to_include_input, analysis_name_input, base_path_input, + normalize_scores_line_level, ) @@ -389,6 +390,7 @@ def project_settings_form( color_by_directory_id, line_display_mode_id, manual_filename_id, + normalize_scores_id, ): submit_btn = submit_button(submit_id, "Apply") form = dbc.Form( @@ -401,6 +403,7 @@ def project_settings_form( color_by_directory_input(color_by_directory_id), manual_filename_input(manual_filename_id), line_display_mode_input(line_display_mode_id), + normalize_scores_line_level(normalize_scores_id), ], width=8, ), diff --git a/dash_app/components/layouts.py b/dash_app/components/layouts.py index 5b427cb..e33d911 100644 --- a/dash_app/components/layouts.py +++ b/dash_app/components/layouts.py @@ -42,6 +42,7 @@ def create_ano_line_level_result_layout( error_toast_id, success_toast_id, grid_image_link_id, + normalize_scores_id, ): error_toast_row = dbc.Row(error_toast(error_toast_id)) @@ -126,9 +127,17 @@ def create_ano_line_level_result_layout( className="dbc mt-3 ms-4 me-4", ) + normalize_scores_store = dbc.Row(dcc.Store(id=normalize_scores_id)) + layout = [ dbc.Container( - [metadata_row, create_grid_image_link, error_toast_row, success_toast_row] + [ + metadata_row, + create_grid_image_link, + error_toast_row, + success_toast_row, + normalize_scores_store, + ] ), dbc.Container(plot_selector_row), dbc.Container(plot_row, fluid=True), @@ -292,6 +301,7 @@ def create_project_layout( edit_name_input_id, submit_edit_name_id, task_logs_modal_id, + normalize_scores_id, ): error_toast_row = dbc.Row(error_toast(error_toast_id)) @@ -351,6 +361,7 @@ def create_project_layout( color_by_directory_id, line_display_mode_input_id, manual_filename_id, + normalize_scores_id, ), body=True, ), diff --git a/dash_app/pages/project.py b/dash_app/pages/project.py index 3a21dff..324761f 100644 --- a/dash_app/pages/project.py +++ b/dash_app/pages/project.py @@ -68,6 +68,7 @@ def layout(project_id=None, **kwargs): "edit-name-input", "submit-edit-name", "task-logs-modal-project", + "normalize-scores-project", ) @@ -148,9 +149,11 @@ def get_analyses(_, project_id): Output("color-by-directory-project", "value"), Output("line-display-mode-project", "value"), Output("manual-filename-project", "value"), + Output("normalize-scores-project", "value"), Input("project-id", "data"), ) def get_settings(project_id): + # TODO: what is return True? if not project_id: return True @@ -164,6 +167,7 @@ def get_settings(project_id): settings.get("color_by_directory"), settings.get("line_level_display_mode"), settings.get("manual_filename_input"), + settings.get("line_level_normalization"), ) @@ -281,6 +285,7 @@ def edit_analysis_name(submit_n_clicks, new_name, analysis_id, url_path): State("color-by-directory-project", "value"), State("line-display-mode-project", "value"), State("manual-filename-project", "value"), + State("normalize-scores-project", "value"), State("project-id", "data"), prevent_initial_call=True, ) @@ -290,6 +295,7 @@ def apply_settings( color_by_directory, line_display_mode, manual_filename_input, + normalize_scores, project_id, ): if not n_clicks: @@ -300,6 +306,7 @@ def apply_settings( "color_by_directory": color_by_directory, "line_level_display_mode": line_display_mode, "manual_filename_input": manual_filename_input, + "line_level_normalization": normalize_scores, } response, error = make_api_call( payload, f"projects/{project_id}/settings", requests_type="PATCH" diff --git a/dash_app/pages/result_pages/ano_line_level.py b/dash_app/pages/result_pages/ano_line_level.py index e8a571e..c74ef1f 100644 --- a/dash_app/pages/result_pages/ano_line_level.py +++ b/dash_app/pages/result_pages/ano_line_level.py @@ -35,6 +35,7 @@ def layout(analysis_id=None, **kwargs): "error-toast-ano-line-res", "success-toats-ano-line-res", "multi-plot-link-ano-line-res", + "normalize-scores-store-ano-line-res", ) return base + content @@ -74,6 +75,7 @@ def get_data(analysis_id): Output("error-toast-ano-line-res", "children", allow_duplicate=True), Output("error-toast-ano-line-res", "is_open", allow_duplicate=True), Output("multi-plot-link-ano-line-res", "href"), + Output("normalize-scores-store-ano-line-res", "data"), Input("stored-data-ano-line-res", "data"), State("analysis-id-ano-line-res", "data"), prevent_initial_call=True, @@ -102,12 +104,27 @@ def generate_dropdown_and_metadata(encoded_df, analysis_id): str(error), True, dash.no_update, + dash.no_update, ) metadata = response.json() project_id = metadata.get("project_id") metadata_rows = format_metadata_rows(metadata) + response_settings, error_settings = make_api_call( + {}, f"projects/{project_id}/settings", "GET" + ) + if error_settings or response_settings is None: + return ( + dash.no_update, + dash.no_update, + dash.no_update, + str(error_settings), + True, + dash.no_update, + dash.no_update, + ) + return ( options, [html.Tbody(metadata_rows)], @@ -115,6 +132,7 @@ def generate_dropdown_and_metadata(encoded_df, analysis_id): dash.no_update, False, f"/dash/analysis/ano-line-level/{analysis_id}/grid", + response_settings.json().get("line_level_normalization"), ) @@ -124,9 +142,10 @@ def generate_dropdown_and_metadata(encoded_df, analysis_id): Input("plot-selector-ano-line-res", "value"), Input("switch", "value"), State("stored-data-ano-line-res", "data"), + State("normalize-scores-store-ano-line-res", "data"), prevent_initial_call=True, ) -def render_plot(selected_plot, switch_on, encoded_df): +def render_plot(selected_plot, switch_on, encoded_df, normalize_scores): if not encoded_df or not selected_plot: return dash.no_update, dash.no_update @@ -141,7 +160,9 @@ def render_plot(selected_plot, switch_on, encoded_df): "minWidth": "600px", "width": "90%", } - fig = create_line_level_plot(df, selected_plot, theme) + fig = create_line_level_plot( + df, selected_plot, theme, normalize_scores=normalize_scores + ) return fig, style diff --git a/dash_app/utils/plots.py b/dash_app/utils/plots.py index 3c2a349..fb4aa86 100644 --- a/dash_app/utils/plots.py +++ b/dash_app/utils/plots.py @@ -7,7 +7,19 @@ def get_options(df) -> list[dict]: return [{"label": seq_id, "value": seq_id} for seq_id in seq_ids] -def create_line_level_plot(df, selected_plot, theme="plotly_white"): +def create_line_level_plot( + df, selected_plot, theme="plotly_white", normalize_scores=True +): + df = df.filter(pl.col("seq_id") == selected_plot) + + if df.get_column("line_number", default=None) is None: + df = df.with_row_index() + xaxis_title = "Index" + x_column = "index" + else: + xaxis_title = "Line Number" + x_column = "line_number" + prediction_columns = [ col for col in df.columns @@ -31,18 +43,10 @@ def create_line_level_plot(df, selected_plot, theme="plotly_white"): ], ] - df = df.filter(pl.col("seq_id") == selected_plot) - if df.get_column("line_number", default=None) is None: - df = df.with_row_index() - xaxis_title = "Index" - x_column = "index" - else: - xaxis_title = "Line Number" - x_column = "line_number" - - for columns in measure_groups: - if columns: - df = _normalize_prediction_columns(df, columns) + if normalize_scores: + for columns in measure_groups: + if columns: + df = _normalize_prediction_columns(df, columns) # polars documentation says that map_elements is slow. # Change if it becomes an issue. @@ -72,13 +76,21 @@ def create_line_level_plot(df, selected_plot, theme="plotly_white"): ) ) + title = ( + f"Normalized Anomaly Score
File: {file_name}
Directory: {directory_name}" + if normalize_scores + else f"Anomaly Score
File: {file_name}
Directory: {directory_name}" + ) fig.update_layout( - title=f"Normalized Anomaly Score
File: {file_name}
Directory: {directory_name}", + title=title, xaxis_title=xaxis_title, - yaxis_title="Anomaly Score (0 - 1)", + yaxis_title="Anomaly Score (0 - 1)" if normalize_scores else "Anomaly Score", template=theme, ) + if not normalize_scores: + fig.update_yaxes(type="log") + return fig diff --git a/migrations/versions/1fce494a5a86_add_normalization_option_to_line_level_.py b/migrations/versions/1fce494a5a86_add_normalization_option_to_line_level_.py new file mode 100644 index 0000000..a579c07 --- /dev/null +++ b/migrations/versions/1fce494a5a86_add_normalization_option_to_line_level_.py @@ -0,0 +1,32 @@ +"""Add normalization option to line level analysis + +Revision ID: 1fce494a5a86 +Revises: a9fe74ac20ca +Create Date: 2025-08-27 13:25:04.838544 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1fce494a5a86' +down_revision = 'a9fe74ac20ca' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('settings', schema=None) as batch_op: + batch_op.add_column(sa.Column('line_level_normalization', sa.Boolean(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('settings', schema=None) as batch_op: + batch_op.drop_column('line_level_normalization') + + # ### end Alembic commands ### diff --git a/server/api/crud_routes.py b/server/api/crud_routes.py index b224ba9..329a6c4 100644 --- a/server/api/crud_routes.py +++ b/server/api/crud_routes.py @@ -93,6 +93,8 @@ def update_settings(project_id: int): settings.color_by_directory = validated_data.color_by_directory settings.line_level_display_mode = validated_data.line_level_display_mode settings.manual_filename_input = validated_data.manual_filename_input + settings.line_level_normalization = validated_data.line_level_normalization + db.session.commit() return {}, 200 diff --git a/server/api/validator_models/crud_params.py b/server/api/validator_models/crud_params.py index a764309..a4d7ccc 100644 --- a/server/api/validator_models/crud_params.py +++ b/server/api/validator_models/crud_params.py @@ -37,3 +37,4 @@ class SettingsParams(BaseModel): "data_points_only" ) manual_filename_input: bool + line_level_normalization: bool diff --git a/server/models/settings.py b/server/models/settings.py index d749d94..dde6e6e 100644 --- a/server/models/settings.py +++ b/server/models/settings.py @@ -9,6 +9,7 @@ class Settings(db.Model): color_by_directory = db.Column(db.Boolean, default=True) line_level_display_mode = db.Column(db.String, default="data_points_only") manual_filename_input = db.Column(db.Boolean, default=False) + line_level_normalization = db.Column(db.Boolean, default=True) project_id = db.Column(db.Integer, db.ForeignKey("projects.id"), nullable=False) project = db.relationship("Project", back_populates="settings") @@ -21,4 +22,5 @@ def to_dict(self): "color_by_directory": self.color_by_directory, "line_level_display_mode": self.line_level_display_mode, "manual_filename_input": self.manual_filename_input, + "line_level_normalization": self.line_level_normalization, }