Skip to content

Commit

Permalink
Better handing for --demo (#103)
Browse files Browse the repository at this point in the history
* Move csv and contrib up to the top level and pass the reactive values down

* conditional tooltips

* fix test

* unused

* helper function for tooltip

* unused import

* ignore test coverage

* Apply css for tooltip readability. subjective!

* comment out some tests to try to avoid timeout
  • Loading branch information
mccalluc authored Nov 1, 2024
1 parent 3df19d6 commit 2994c7b
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 43 deletions.
4 changes: 2 additions & 2 deletions dp_creator_ii/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""DP Creator II makes it easier to get started with Differential Privacy."""

import shiny
from dp_creator_ii.utils.argparse_helpers import get_csv_contrib
from dp_creator_ii.utils.argparse_helpers import get_csv_contrib_from_cli


__version__ = "0.0.1"
Expand All @@ -10,7 +10,7 @@
def main(): # pragma: no cover
# We only call this here so "--help" is handled,
# and to validate inputs before starting the server.
get_csv_contrib()
get_csv_contrib_from_cli()

shiny.run_app(
app="dp_creator_ii.app",
Expand Down
25 changes: 22 additions & 3 deletions dp_creator_ii/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pathlib import Path
import logging

from shiny import App, ui
from shiny import App, ui, reactive

from dp_creator_ii.utils.argparse_helpers import get_csv_contrib_from_cli
from dp_creator_ii.app import analysis_panel, dataset_panel, results_panel


Expand All @@ -25,8 +26,26 @@ def ctrl_c_reminder(): # pragma: no cover


def server(input, output, session): # pragma: no cover
dataset_panel.dataset_server(input, output, session)
analysis_panel.analysis_server(input, output, session)
(csv_path_from_cli, contributions_from_cli, is_demo) = get_csv_contrib_from_cli()
csv_path = reactive.value(csv_path_from_cli)
contributions = reactive.value(contributions_from_cli)

dataset_panel.dataset_server(
input,
output,
session,
csv_path=csv_path,
contributions=contributions,
is_demo=is_demo,
)
analysis_panel.analysis_server(
input,
output,
session,
csv_path=csv_path,
contributions=contributions,
is_demo=is_demo,
)
results_panel.results_server(input, output, session)
session.on_ended(ctrl_c_reminder)

Expand Down
32 changes: 11 additions & 21 deletions dp_creator_ii/app/analysis_panel.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from math import pow

from shiny import ui, reactive, render
from shiny import ui, reactive, render, req

from dp_creator_ii.app.components.inputs import log_slider
from dp_creator_ii.app.components.column_module import column_ui, column_server
from dp_creator_ii.utils.csv_helper import read_field_names
from dp_creator_ii.utils.argparse_helpers import get_csv_contrib
from dp_creator_ii.app.components.outputs import output_code_sample
from dp_creator_ii.utils.templates import make_privacy_loss_block

Expand Down Expand Up @@ -34,10 +33,15 @@ def analysis_ui():
)


def analysis_server(input, output, session): # pragma: no cover
(csv_path, contributions) = get_csv_contrib()
def analysis_server(
input,
output,
session,
csv_path=None,
contributions=None,
is_demo=None,
): # pragma: no cover

csv_path_from_cli_value = reactive.value(csv_path)
weights = reactive.value({})

def set_column_weight(column_id, weight):
Expand Down Expand Up @@ -74,7 +78,7 @@ def columns_ui():
column_server(
column_id,
name=column_id,
contributions=contributions,
contributions=contributions(),
epsilon=epsilon_calc(),
set_column_weight=set_column_weight,
get_weights_sum=get_weights_sum,
Expand All @@ -87,23 +91,9 @@ def columns_ui():
for column_id in column_ids
]

@reactive.calc
def csv_path_calc():
csv_path_from_ui = input.csv_path_from_ui()
if csv_path_from_ui is not None:
return csv_path_from_ui[0]["datapath"]
return csv_path_from_cli_value.get()

@render.text
def csv_path():
return csv_path_calc()

@reactive.calc
def csv_fields_calc():
path = csv_path_calc()
if path is None:
return None
return read_field_names(path)
return read_field_names(req(csv_path()))

@render.text
def csv_fields():
Expand Down
10 changes: 10 additions & 0 deletions dp_creator_ii/app/components/outputs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from htmltools.tags import details, summary
from shiny import ui
from faicons import icon_svg


def output_code_sample(title, name_of_render_function):
return details(
summary(f"Code sample: {title}"),
ui.output_code(name_of_render_function),
)


def demo_tooltip(is_demo, text): # pragma: no cover
if is_demo:
return ui.tooltip(
icon_svg("circle-question"),
text,
placement="right",
)
27 changes: 27 additions & 0 deletions dp_creator_ii/app/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,30 @@ body {
#top_level_nav {
margin-bottom: 1em;
}

/*
Improve readability of popover.
*/
.tooltip-inner {
text-align: left;
color: black;
background-color: lightgray;
opacity: 100%;
}
/*
Arrow color should be consistent with background of tooltip-inner.
The tooltip is positioned to avoid falling outside of the window,
so any of these might be applied.
*/
.bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow::before {
border-right-color: lightgrey;
}
.bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow::before {
border-bottom-color: lightgray;
}
.bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow::before {
border-left-color: lightgrey;
}
.bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow::before {
border-top-color: lightgray;
}
68 changes: 60 additions & 8 deletions dp_creator_ii/app/dataset_panel.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,84 @@
from pathlib import Path

from shiny import ui, reactive, render

from dp_creator_ii.utils.argparse_helpers import get_csv_contrib
from dp_creator_ii.app.components.outputs import output_code_sample
from dp_creator_ii.utils.argparse_helpers import get_csv_contrib_from_cli
from dp_creator_ii.app.components.outputs import output_code_sample, demo_tooltip
from dp_creator_ii.utils.templates import make_privacy_unit_block


def dataset_ui():
(_csv_path, contributions) = get_csv_contrib()
(csv_path, contributions, is_demo) = get_csv_contrib_from_cli()
csv_placeholder = None if csv_path is None else Path(csv_path).name

return ui.nav_panel(
"Select Dataset",
ui.input_file("csv_path_from_ui", "Choose CSV file:", accept=[".csv"]),
# Doesn't seem to be possible to preset the actual value,
# but the placeholder string is a good substitute.
ui.input_file(
"csv_path",
["Choose CSV file", ui.output_ui("choose_csv_demo_tooltip_ui")],
accept=[".csv"],
placeholder=csv_placeholder,
),
ui.markdown(
"How many rows of the CSV can one individual contribute to? "
'This is the "unit of privacy" which will be protected.'
),
ui.input_numeric("contributions", "Contributions", contributions),
ui.input_numeric(
"contributions",
["Contributions", ui.output_ui("contributions_demo_tooltip_ui")],
contributions,
),
ui.output_ui("python_tooltip_ui"),
output_code_sample("Unit of Privacy", "unit_of_privacy_python"),
ui.input_action_button("go_to_analysis", "Define analysis"),
value="dataset_panel",
)


def dataset_server(input, output, session): # pragma: no cover
def dataset_server(
input, output, session, csv_path=None, contributions=None, is_demo=None
): # pragma: no cover
@reactive.effect
@reactive.event(input.csv_path)
def _on_csv_path_change():
csv_path.set(input.csv_path()[0]["datapath"])

@reactive.effect
@reactive.event(input.contributions)
def _on_contributions_change():
contributions.set(input.contributions())

@render.ui
def choose_csv_demo_tooltip_ui():
return demo_tooltip(
is_demo,
"For the demo, we'll imagine we have the grades "
"on assignments for a class.",
)

@render.ui
def contributions_demo_tooltip_ui():
return demo_tooltip(
is_demo,
"For the demo, we assume that each student "
f"can occur at most {contributions()} times in the dataset. ",
)

@render.ui
def python_tooltip_ui():
return demo_tooltip(
is_demo,
"Along the way, code samples will demonstrate "
"how the information you provide is used in OpenDP, "
"and at the end you can download a notebook "
"for the entire calculation.",
)

@render.code
def unit_of_privacy_python():
contributions = input.contributions()
return make_privacy_unit_block(contributions)
return make_privacy_unit_block(contributions())

@reactive.effect
@reactive.event(input.go_to_analysis)
Expand Down
8 changes: 4 additions & 4 deletions dp_creator_ii/utils/argparse_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _clip(n, lower, upper):

def _get_demo_csv_contrib():
"""
>>> csv_path, contributions = _get_demo_csv_contrib()
>>> csv_path, contributions, is_demo = _get_demo_csv_contrib()
>>> with open(csv_path, newline="") as csv_handle:
... reader = csv.DictReader(csv_handle)
... reader.fieldnames
Expand Down Expand Up @@ -102,13 +102,13 @@ def _get_demo_csv_contrib():
}
)

return csv_path, contributions
return (csv_path, contributions, True)


def get_csv_contrib(): # pragma: no cover
def get_csv_contrib_from_cli(): # pragma: no cover
args = _get_args()
if args.demo:
if args.csv_path is not None:
warn('"--demo" overrides "--csv" and "--contrib"')
return _get_demo_csv_contrib()
return (args.csv_path, args.contributions)
return (args.csv_path, args.contributions, False)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = ["version", "description"]
dependencies = [
"shiny",
"faicons",
"matplotlib",
"opendp[polars]",
"jupytext",
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ipykernel

# Shiny:
shiny
faicons

# Visualization:
matplotlib
6 changes: 5 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ docutils==0.21.2
# via flit
executing==2.1.0
# via stack-data
faicons==0.2.2
# via -r requirements-dev.in
fastjsonschema==2.20.0
# via nbformat
filelock==3.16.1
Expand All @@ -82,7 +84,9 @@ greenlet==3.0.3
h11==0.14.0
# via uvicorn
htmltools==0.5.3
# via shiny
# via
# faicons
# shiny
identify==2.6.1
# via pre-commit
idna==3.10
Expand Down
10 changes: 6 additions & 4 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ def expect_no_error():
page.get_by_label("grade").check()
page.get_by_label("Min").click()
page.get_by_label("Min").fill("0")
page.get_by_label("Max").click()
page.get_by_label("Max").fill("100")
page.get_by_label("Bins").click()
page.get_by_label("Bins").fill("20")
# TODO: All these recalculations cause timeouts:
# It is still rerendering the graph after hitting "Download results".
# page.get_by_label("Max").click()
# page.get_by_label("Max").fill("100")
# page.get_by_label("Bins").click()
# page.get_by_label("Bins").fill("20")
page.get_by_label("Weight").select_option("1")
expect_no_error()

Expand Down

0 comments on commit 2994c7b

Please sign in to comment.