Skip to content

Commit 5dc65a5

Browse files
feat: update GUI column picker with a cleaner look (#296)
* feat: use cards layout for column picker * column picker: align grid and cards * use cards for parameters page to mage column picker layout somewhat
1 parent dd8f30e commit 5dc65a5

File tree

3 files changed

+154
-143
lines changed

3 files changed

+154
-143
lines changed

gui/components/analysis.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class AnalysisParamsCard:
77
"""
88
Card component for configuring analyzer parameters.
99
10-
Displays interactive controls for modifying analysis parameters
11-
similar to ImportOptionsDialog but as a card component instead of a dialog.
10+
Displays each parameter as an individual card, matching the visual style
11+
of the column picker cards for consistent UX.
1212
"""
1313

1414
def __init__(
@@ -31,47 +31,45 @@ def __init__(
3131
self._build_card()
3232

3333
def _build_card(self):
34-
"""Build the parameter configuration card."""
35-
with ui.card().classes("w-full"):
36-
if not self.params:
34+
"""Build the parameter configuration cards in a flex-wrap row."""
35+
if not self.params:
36+
with ui.column().classes("w-full items-center"):
3737
ui.label("This analyzer has no configurable parameters.").classes(
3838
"text-grey-7"
3939
)
40-
return
40+
return
4141

42-
# Build controls for each parameter
42+
with ui.row().classes("flex-wrap gap-6 justify-center w-full"):
4343
for param in self.params:
44-
self._build_param_control(param)
45-
46-
def _build_param_control(self, param: AnalyzerParam):
47-
"""Build UI control for a single parameter."""
48-
with ui.column().classes("w-full mb-2"):
49-
# Parameter label with description
50-
with ui.row().classes("items-center gap-4"):
51-
ui.label(param.print_name).classes("text-base font-bold")
44+
self._build_param_card(param)
45+
46+
def _build_param_card(self, param: AnalyzerParam):
47+
"""Build an individual card for a single parameter."""
48+
with ui.card().classes("w-72 p-4 no-shadow border border-gray-200"):
49+
with ui.row().classes("items-center gap-1"):
50+
ui.label(param.print_name).classes("text-bold")
5251
if param.description:
5352
with ui.icon("info").classes("text-grey-6 cursor-pointer"):
5453
ui.tooltip(param.description)
5554

56-
# Parameter input control based on type
57-
param_type = param.type
58-
default_value = self.default_values.get(param.id)
55+
param_type = param.type
56+
default_value = self.default_values.get(param.id)
5957

60-
if param_type.type == "integer":
61-
self._build_integer_control(param, param_type, default_value)
62-
elif param_type.type == "time_binning":
63-
self._build_time_binning_control(param, default_value)
58+
if param_type.type == "integer":
59+
self._build_integer_control(param, param_type, default_value)
60+
elif param_type.type == "time_binning":
61+
self._build_time_binning_control(param, default_value)
6462

6563
def _build_integer_control(
6664
self,
6765
param: AnalyzerParam,
6866
param_type: IntegerParam,
69-
default_value: int | None,
67+
default_value: ParamValue | None,
7068
):
7169
"""Build integer parameter control."""
70+
int_default = default_value if isinstance(default_value, int) else None
7271
number_input = ui.number(
73-
label=f"Enter value between {param_type.min} and {param_type.max}",
74-
value=default_value if default_value is not None else param_type.min,
72+
value=int_default if int_default is not None else param_type.min,
7573
min=param_type.min,
7674
max=param_type.max,
7775
step=1,
@@ -80,16 +78,18 @@ def _build_integer_control(
8078
f"Must be at least {param_type.min}": lambda v: v >= param_type.min,
8179
f"Must be at most {param_type.max}": lambda v: v <= param_type.max,
8280
},
83-
).classes("w-40")
81+
).classes("w-full mt-2")
8482

8583
self.param_widgets[param.id] = ("integer", number_input)
8684

8785
def _build_time_binning_control(
88-
self, param: AnalyzerParam, default_value: TimeBinningValue | None
86+
self, param: AnalyzerParam, default_value: ParamValue | None
8987
):
9088
"""Build time binning parameter control."""
91-
with ui.row().classes("gap-2"):
92-
# Unit selector
89+
tb_default = (
90+
default_value if isinstance(default_value, TimeBinningValue) else None
91+
)
92+
with ui.row().classes("gap-2 mt-2 w-full"):
9393
unit_select = ui.select(
9494
{
9595
"year": "Year",
@@ -100,14 +100,11 @@ def _build_time_binning_control(
100100
"minute": "Minute",
101101
"second": "Second",
102102
},
103-
label="Pick a time unit",
104-
value=default_value.unit if default_value else "day",
103+
value=tb_default.unit if tb_default else "day",
105104
).classes("w-32")
106105

107-
# Amount input
108106
amount_input = ui.number(
109-
label="How many?",
110-
value=default_value.amount if default_value else 1,
107+
value=tb_default.amount if tb_default else 1,
111108
min=1,
112109
max=1000,
113110
step=1,
@@ -116,7 +113,7 @@ def _build_time_binning_control(
116113
"Must be at least 1": lambda v: v >= 1,
117114
"Cannot exceed 1000": lambda v: v <= 1000,
118115
},
119-
).classes("w-32")
116+
).classes("w-24")
120117

121118
self.param_widgets[param.id] = ("time_binning", unit_select, amount_input)
122119

gui/pages/analysis_dataset.py

Lines changed: 104 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -90,99 +90,111 @@ def update_preview():
9090
else "Data Preview (all rows)"
9191
)
9292
ui.label(preview_title).classes("text-sm text-grey-7")
93-
ui.aggrid.from_polars(preview_df, theme="quartz").classes(
94-
"w-full h-64"
95-
)
9693

97-
# Create column mapping UI using grid
98-
with ui.grid(columns=2).classes("gap-2"):
99-
# create labels for grid header
100-
ui.label("Required Input Information") # populates row 1, column 1
101-
ui.label("Imported Dataset Columns") # pupolates row 1, column 2
102-
103-
# this then fills the rows with column information
104-
for input_col in input_columns:
105-
# Left column: Input column info card
106-
with ui.row().classes("items-center gap-1"):
107-
ui.label(input_col.human_readable_name_or_fallback()).classes(
108-
"text-bold text-lg"
109-
)
110-
if input_col.description:
111-
with ui.icon("info").classes("text-grey-6 cursor-pointer"):
112-
ui.tooltip(input_col.description)
113-
114-
# Right column: Dropdown for column selection
115-
# Get compatible user columns
116-
compatible_columns = [
117-
user_col
118-
for user_col in user_columns
119-
if get_data_type_compatibility_score(
120-
input_col.data_type, user_col.data_type
121-
)
122-
is not None
123-
]
124-
125-
# Create dropdown options
126-
dropdown_options = {
127-
f"{user_col.name}": user_col.name
128-
for user_col in compatible_columns
129-
}
130-
131-
# Pre-select the auto-mapped column
132-
default_value = None
133-
if input_col.name in draft_column_mapping:
134-
mapped_col_name = draft_column_mapping[input_col.name]
135-
default_value = next(
136-
(
137-
k
138-
for k, v in dropdown_options.items()
139-
if v == mapped_col_name
140-
),
141-
None,
94+
# create AGGRRID directly from df
95+
grid = ui.aggrid.from_polars(
96+
preview_df,
97+
theme="quartz",
98+
auto_size_columns=True,
99+
).classes("w-full h-64")
100+
grid.on(
101+
"firstDataRendered",
102+
lambda: grid.run_grid_method("sizeColumnsToFit"),
142103
)
143104

144-
# Create dropdown with on_change handler
145-
dropdown = (
146-
ui.select(
147-
options=list(dropdown_options.keys()),
148-
label="Select dataset column",
149-
value=default_value,
150-
on_change=lambda: update_preview(),
151-
)
152-
.classes("w-40")
153-
.props("use-chips")
105+
# Shared container for cards, grid, and button
106+
with (
107+
ui.column()
108+
.classes("w-full items-center gap-6")
109+
.style("max-width: 960px; margin: 0 auto;")
110+
):
111+
ui.label("Map Your Data Columns").classes("text-lg font-bold mb-4")
112+
113+
with ui.row().classes("flex-wrap gap-6 justify-center w-full"):
114+
for input_col in input_columns:
115+
with ui.card().classes(
116+
"w-52 p-4 no-shadow border border-gray-200"
117+
):
118+
with ui.row().classes("items-center gap-1"):
119+
ui.label(
120+
input_col.human_readable_name_or_fallback()
121+
).classes("text-bold")
122+
if input_col.description:
123+
with ui.icon("info").classes(
124+
"text-grey-6 cursor-pointer"
125+
):
126+
ui.tooltip(input_col.description)
127+
128+
compatible_columns = [
129+
user_col
130+
for user_col in user_columns
131+
if get_data_type_compatibility_score(
132+
input_col.data_type, user_col.data_type
133+
)
134+
is not None
135+
]
136+
137+
dropdown_options = {
138+
f"{user_col.name}": user_col.name
139+
for user_col in compatible_columns
140+
}
141+
142+
default_value = None
143+
if input_col.name in draft_column_mapping:
144+
mapped_col_name = draft_column_mapping[input_col.name]
145+
default_value = next(
146+
(
147+
k
148+
for k, v in dropdown_options.items()
149+
if v == mapped_col_name
150+
),
151+
None,
152+
)
153+
154+
dropdown = (
155+
ui.select(
156+
options=list(dropdown_options.keys()),
157+
value=default_value,
158+
on_change=lambda: update_preview(),
159+
)
160+
.classes("w-full mt-2")
161+
.props("use-chips")
162+
)
163+
164+
column_dropdowns[input_col.name] = (
165+
dropdown,
166+
dropdown_options,
167+
)
168+
169+
# Preview section (created after cards)
170+
preview_container = ui.column().classes("w-full")
171+
172+
# Initial preview render
173+
update_preview()
174+
175+
# Action button
176+
with ui.row().classes("w-full justify-end"):
177+
178+
def _on_proceed():
179+
"""Build column mapping and proceed."""
180+
final_mapping = {}
181+
for input_col_name, (
182+
dropdown,
183+
options,
184+
) in column_dropdowns.items():
185+
if dropdown.value:
186+
final_mapping[input_col_name] = options[dropdown.value]
187+
188+
# Store mapping in session
189+
self.session.column_mapping = final_mapping
190+
self.notify_success("Column mapping saved!")
191+
192+
# Navigate to parameters configuration page
193+
self.navigate_to(gui_routes.configure_analysis_parameters)
194+
195+
ui.button(
196+
"Configure Parameters",
197+
icon="arrow_forward",
198+
color="primary",
199+
on_click=_on_proceed,
154200
)
155-
156-
# Store reference for later
157-
column_dropdowns[input_col.name] = (dropdown, dropdown_options)
158-
159-
# Preview section (created after grid)
160-
ui.separator()
161-
preview_container = ui.column().classes("w-full")
162-
163-
# Initial preview render
164-
update_preview()
165-
166-
# Action button
167-
with ui.row().classes("w-full justify-end"):
168-
169-
def _on_proceed():
170-
"""Build column mapping and proceed."""
171-
final_mapping = {}
172-
for input_col_name, (dropdown, options) in column_dropdowns.items():
173-
if dropdown.value:
174-
final_mapping[input_col_name] = options[dropdown.value]
175-
176-
# Store mapping in session
177-
self.session.column_mapping = final_mapping
178-
self.notify_success("Column mapping saved!")
179-
180-
# Navigate to parameters configuration page
181-
self.navigate_to(gui_routes.configure_analysis_parameters)
182-
183-
ui.button(
184-
"Configure Parameters",
185-
icon="arrow_forward",
186-
color="primary",
187-
on_click=_on_proceed,
188-
)

gui/pages/analysis_params.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,29 +88,31 @@ def render_content(self):
8888
.classes("items-center justify-start gap-6")
8989
.style("width: 100%; max-width: 1200px; margin: 0 auto; padding: 2rem;")
9090
):
91-
ui.label(f"Configure {analyzer.name} Parameters").classes("text-xl")
92-
93-
# Create parameter configuration card
94-
params_card = AnalysisParamsCard(
95-
params=analyzer.params, default_values=param_values
96-
)
91+
with (
92+
ui.column()
93+
.classes("w-full items-center gap-6")
94+
.style("max-width: 960px; margin: 0 auto;")
95+
):
96+
ui.label(f"Configure {analyzer.name} Parameters").classes(
97+
"text-lg font-bold mb-4"
98+
)
9799

98-
# Action button
99-
with ui.row().classes("w-full justify-end mt-6"):
100+
params_card = AnalysisParamsCard(
101+
params=analyzer.params, default_values=param_values
102+
)
100103

101104
def _on_proceed():
102105
"""Retrieve parameter values and proceed."""
103106
final_params = params_card.get_param_values()
104107

105-
# Store parameters in session
106108
self.session.analysis_params = final_params
107109

108-
# TODO: Navigate to next step (run analysis or review page)
109110
self.navigate_to(gui_routes.run_analysis)
110111

111-
ui.button(
112-
"Proceed to Run Analysis",
113-
icon="arrow_forward",
114-
color="primary",
115-
on_click=_on_proceed,
116-
)
112+
with ui.row().classes("w-full justify-end mt-6"):
113+
ui.button(
114+
"Proceed to Run Analysis",
115+
icon="arrow_forward",
116+
color="primary",
117+
on_click=_on_proceed,
118+
)

0 commit comments

Comments
 (0)