diff --git a/dp_wizard/app/__init__.py b/dp_wizard/app/__init__.py index 43e78ff..d0266ef 100644 --- a/dp_wizard/app/__init__.py +++ b/dp_wizard/app/__init__.py @@ -1,7 +1,7 @@ from pathlib import Path import logging -from shiny import App, ui, reactive, Inputs, Outputs, Session +from shiny import App, ui, reactive, Inputs, Outputs, Session, render from dp_wizard.utils.argparse_helpers import get_cli_info, CLIInfo from dp_wizard.app import analysis_panel, dataset_panel, results_panel, feedback_panel @@ -37,6 +37,11 @@ def server(input: Inputs, output: Outputs, session: Session): # pragma: no cove bin_counts = reactive.value({}) weights = reactive.value({}) epsilon = reactive.value(1.0) + current_panel = reactive.value(dataset_panel.dataset_panel_id) + + @render.text + def current_panel_text(): + return current_panel() dataset_panel.dataset_server( input, @@ -45,6 +50,7 @@ def server(input: Inputs, output: Outputs, session: Session): # pragma: no cove is_demo=cli_info.is_demo, csv_path=csv_path, contributions=contributions, + current_panel=current_panel, ) analysis_panel.analysis_server( input, @@ -58,6 +64,7 @@ def server(input: Inputs, output: Outputs, session: Session): # pragma: no cove bin_counts=bin_counts, weights=weights, epsilon=epsilon, + current_panel=current_panel, ) results_panel.results_server( input, @@ -70,6 +77,7 @@ def server(input: Inputs, output: Outputs, session: Session): # pragma: no cove bin_counts=bin_counts, weights=weights, epsilon=epsilon, + current_panel=current_panel, ) feedback_panel.feedback_server( input, diff --git a/dp_wizard/app/analysis_panel.py b/dp_wizard/app/analysis_panel.py index 8f32705..4d306ee 100644 --- a/dp_wizard/app/analysis_panel.py +++ b/dp_wizard/app/analysis_panel.py @@ -9,11 +9,16 @@ from dp_wizard.utils.dp_helper import confidence from dp_wizard.app.components.outputs import output_code_sample, demo_tooltip from dp_wizard.utils.code_generators import make_privacy_loss_block +from dp_wizard.app import results_panel +from dp_wizard.app.components.outputs import info_box + +analysis_panel_id = "2_analysis_panel" def analysis_ui(): return ui.nav_panel( "Define Analysis", + ui.output_ui("analysis_panel_warning"), ui.layout_columns( ui.card( ui.card_header("Columns"), @@ -71,7 +76,7 @@ def analysis_ui(): ), ui.output_ui("columns_ui"), ui.output_ui("download_results_button_ui"), - value="analysis_panel", + value=analysis_panel_id, ) @@ -97,7 +102,27 @@ def analysis_server( bin_counts: reactive.Value[dict[str, int]], weights: reactive.Value[dict[str, str]], epsilon: reactive.Value[float], + current_panel, ): # pragma: no cover + @render.ui + def analysis_panel_warning(): + if current_panel() > analysis_panel_id: + return info_box( + """ + Once you've confirmed your analysis settings + they are locked. The privacy budget should be considered + a finite resource. + """ + ) + if current_panel() < analysis_panel_id: + return info_box( + """ + This form is locked until you've confirmed your + dataset and unit of privacy. + """ + ) + return "" + @reactive.calc def button_enabled(): column_ids_selected = input.columns_checkbox_group() @@ -186,7 +211,8 @@ def privacy_loss_python(): @reactive.effect @reactive.event(input.go_to_results) def go_to_results(): - ui.update_navs("top_level_nav", selected="results_panel") + current_panel.set(results_panel.results_panel_id) + ui.update_navs("top_level_nav", selected=results_panel.results_panel_id) @render.ui def download_results_button_ui(): @@ -198,5 +224,5 @@ def download_results_button_ui(): return button return [ button, - "Select one or more columns before proceeding.", + info_box("Select one or more columns before proceeding."), ] diff --git a/dp_wizard/app/components/outputs.py b/dp_wizard/app/components/outputs.py index cb0f4b3..cd4bda7 100644 --- a/dp_wizard/app/components/outputs.py +++ b/dp_wizard/app/components/outputs.py @@ -19,6 +19,10 @@ def demo_tooltip(is_demo: bool, text: str): # pragma: no cover ) +def info_box(text): # pragma: no cover + return ui.div(text, class_="alert alert-info col-md-10 col-lg-8", role="alert") + + def hide_if(condition: bool, el): # pragma: no cover display = "none" if condition else "block" return ui.div(el, style=f"display: {display};") diff --git a/dp_wizard/app/dataset_panel.py b/dp_wizard/app/dataset_panel.py index cdfc9ae..0acbd50 100644 --- a/dp_wizard/app/dataset_panel.py +++ b/dp_wizard/app/dataset_panel.py @@ -5,6 +5,10 @@ from dp_wizard.utils.argparse_helpers import get_cli_info from dp_wizard.app.components.outputs import output_code_sample, demo_tooltip from dp_wizard.utils.code_generators import make_privacy_unit_block +from dp_wizard.app import analysis_panel +from dp_wizard.app.components.outputs import info_box + +dataset_panel_id = "1_dataset_panel" def dataset_ui(): @@ -13,6 +17,7 @@ def dataset_ui(): return ui.nav_panel( "Select Dataset", + ui.output_ui("dataset_panel_warning"), # Doesn't seem to be possible to preset the actual value, # but the placeholder string is a good substitute. ui.input_file( @@ -34,7 +39,7 @@ def dataset_ui(): ui.output_ui("python_tooltip_ui"), output_code_sample("Unit of Privacy", "unit_of_privacy_python"), ui.output_ui("define_analysis_button_ui"), - value="dataset_panel", + value=dataset_panel_id, ) @@ -45,7 +50,21 @@ def dataset_server( csv_path: reactive.Value[str], contributions: reactive.Value[int], is_demo: bool, + current_panel: reactive.Value[str], ): # pragma: no cover + @render.ui + def dataset_panel_warning(): + if current_panel() > dataset_panel_id: + return info_box( + """ + Once you've confirmed your dataset and the unit of privacy + they are locked. The unit of privacy is a characteristic + of your dataset and shouldn't be tweaked just to improve + utility. + """ + ) + return "" + @reactive.effect @reactive.event(input.csv_path) def _on_csv_path_change(): @@ -99,7 +118,7 @@ def define_analysis_button_ui(): return button return [ button, - "Choose CSV and Contributions before proceeding.", + info_box("Choose CSV and Contributions before proceeding."), ] @render.code @@ -109,4 +128,5 @@ def unit_of_privacy_python(): @reactive.effect @reactive.event(input.go_to_analysis) def go_to_analysis(): - ui.update_navs("top_level_nav", selected="analysis_panel") + current_panel.set(analysis_panel.analysis_panel_id) + ui.update_navs("top_level_nav", selected=analysis_panel.analysis_panel_id) diff --git a/dp_wizard/app/results_panel.py b/dp_wizard/app/results_panel.py index 3f69950..a0c03f7 100644 --- a/dp_wizard/app/results_panel.py +++ b/dp_wizard/app/results_panel.py @@ -7,11 +7,15 @@ AnalysisPlanColumn, ) from dp_wizard.utils.converters import convert_py_to_nb +from dp_wizard.app.components.outputs import info_box + +results_panel_id = "3_results_panel" def results_ui(): return ui.nav_panel( "Download results", + ui.output_ui("results_panel_warning"), ui.markdown("You can now make a differentially private release of your data."), ui.download_button( "download_script", @@ -21,7 +25,7 @@ def results_ui(): "download_notebook", "Download Notebook (.ipynb)", ), - value="results_panel", + value=results_panel_id, ) @@ -36,7 +40,19 @@ def results_server( bin_counts: reactive.Value[dict[str, int]], weights: reactive.Value[dict[str, str]], epsilon: reactive.Value[float], + current_panel: reactive.Value[str], ): # pragma: no cover + @render.ui + def results_panel_warning(): + if current_panel() < results_panel_id: + return info_box( + """ + This tab is locked until you've confirmed your + analysis details. + """ + ) + return "" + @reactive.calc def analysis_plan() -> AnalysisPlan: # weights().keys() will reflect the desired columns: