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,
}