Skip to content

Commit dd8f30e

Browse files
feat: use ui.input in ngrams-dashboard for filter rather than ui.select (#295)
* use ui.input rather than ui.select, add code to handle autocomplete * remove filtering autocomplete * increase label fontsizes
1 parent 9dbf7e4 commit dd8f30e

File tree

2 files changed

+82
-20
lines changed

2 files changed

+82
-20
lines changed

analyzers/ngrams/ngrams_web/plots.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,10 @@ def plot_scatter_echart(data: pl.DataFrame) -> dict:
202202
"name": "Nr. unique posters",
203203
"nameLocation": "middle",
204204
"nameGap": 30,
205+
"nameTextStyle": {"fontSize": 14},
205206
"axisLabel": {
206207
":formatter": "function(value) { return value >= 1 ? value : ''; }",
208+
"fontSize": 12,
207209
},
208210
},
209211
"yAxis": {
@@ -212,8 +214,10 @@ def plot_scatter_echart(data: pl.DataFrame) -> dict:
212214
"name": "N-gram frequency",
213215
"nameLocation": "middle",
214216
"nameGap": 40,
217+
"nameTextStyle": {"fontSize": 14},
215218
"axisLabel": {
216219
":formatter": "function(value) { return value >= 1 ? value : ''; }",
220+
"fontSize": 12,
217221
},
218222
},
219223
"series": series,

gui/dashboards/ngrams.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ def __init__(self, session: GuiSession):
4949
self._selected_data_index: int | None = None
5050
# State for filter
5151
self._filter_text: str | None = None
52+
self._filter_applied: bool = False
53+
self._all_ngram_options: list[str] = []
5254
# DataFrames
5355
self._df_stats: pl.DataFrame | None = None
5456
self._df_full: pl.DataFrame | None = None
5557
# UI component references (set during render)
5658
self._chart: ui.echart | None = None
5759
self._grid: ui.aggrid | None = None
5860
self._info_label: ui.label | None = None
59-
self._ngram_select: ui.select | None = None
61+
self._ngram_select: ui.input | None = None
6062

6163
def _get_parquet_path(self, output_id: str) -> str | None:
6264
"""
@@ -171,15 +173,22 @@ def _update_info_label(self) -> None:
171173
f"N-gram: '{self._selected_words}' — {count:,} total repetitions"
172174
)
173175
elif self._filter_text:
174-
# Show filter results summary
176+
# Show filter status
175177
df_filtered = self._get_filtered_stats()
176178
count = df_filtered.height
177179
if count == 0:
178180
self._info_label.text = (
179181
f"No n-grams found matching '{self._filter_text}'. "
180182
"Try a different search term."
181183
)
184+
elif not self._filter_applied:
185+
# User is typing, show hint to press Enter
186+
self._info_label.text = (
187+
f"Filter: '{self._filter_text}' — {count:,} matches found. "
188+
"Press Enter to apply filter to chart and grid."
189+
)
182190
else:
191+
# Filter has been applied
183192
self._info_label.text = (
184193
f"Showing {min(count, 100):,} of {count:,} n-grams "
185194
f"matching '{self._filter_text}'. "
@@ -189,7 +198,8 @@ def _update_info_label(self) -> None:
189198
# Default summary view
190199
self._info_label.text = (
191200
"Showing top 100 n-grams by frequency. "
192-
"Click a point on the scatter plot to view all occurrences."
201+
"Type to search, then press Enter to filter. "
202+
"Click a point to view all occurrences."
193203
)
194204

195205
def _update_grid(self) -> None:
@@ -307,25 +317,46 @@ def _handle_point_click(self, e) -> None:
307317

308318
def _handle_filter_change(self, e) -> None:
309319
"""
310-
Handle n-gram filter selection/input changes.
320+
Handle n-gram filter input changes (fires on every keystroke).
311321
312-
Updates the filter text and refreshes the chart and grid to show
313-
only n-grams containing the filter text (substring match).
322+
Updates info label to guide user.
323+
Does NOT update chart/grid (expensive operations) - those happen on Enter.
314324
315325
Args:
316-
e: Change event from ui.select with value attribute
326+
e: Change event from ui.input with value attribute
317327
"""
318328
self._filter_text = e.value if e.value else None
319-
# Clear selection when filter changes
329+
self._filter_applied = False
330+
# Update info label to show hint
331+
self._update_info_label()
332+
333+
def _handle_enter_press(self, e) -> None:
334+
"""
335+
Handle Enter key press in search input.
336+
337+
Updates the expensive visualizations (chart and grid) with the
338+
current filter text. This provides good performance by avoiding
339+
continuous redraws on every keystroke.
340+
341+
Args:
342+
e: Keydown event from ui.input
343+
"""
344+
# Clear any previous selection
320345
self._selected_words = None
321346
self._selected_series_index = None
322347
self._selected_data_index = None
323348
self._clear_all_highlights()
324-
# Update chart and grid with filter
349+
350+
# Mark filter as applied
351+
self._filter_applied = True
352+
353+
# Update expensive visualizations
325354
self._update_chart_with_filter()
326-
self._update_info_label()
327355
self._update_grid()
328356

357+
# Update info label to show results
358+
self._update_info_label()
359+
329360
def _get_filtered_stats(self) -> pl.DataFrame:
330361
"""
331362
Get df_stats filtered by the current filter text.
@@ -344,6 +375,29 @@ def _get_filtered_stats(self) -> pl.DataFrame:
344375
pl.col(COL_NGRAM_WORDS).str.contains(f"(?i){self._filter_text}")
345376
)
346377

378+
def _handle_clear(self) -> None:
379+
"""
380+
Handle clear button click on search input.
381+
382+
Resets the chart and grid to show the initial unfiltered state.
383+
"""
384+
# Clear filter state
385+
self._filter_text = None
386+
self._filter_applied = False
387+
388+
# Clear any selection
389+
self._selected_words = None
390+
self._selected_series_index = None
391+
self._selected_data_index = None
392+
self._clear_all_highlights()
393+
394+
# Re-render chart with full dataset
395+
self._update_chart_with_filter()
396+
397+
# Update grid and info label
398+
self._update_grid()
399+
self._update_info_label()
400+
347401
def _update_chart_with_filter(self) -> None:
348402
"""
349403
Re-render the chart with filtered data.
@@ -372,13 +426,17 @@ def render_content(self) -> None:
372426
with ui.column().classes("w-3/4 q-pa-md gap-4"):
373427
# Scatter plot card
374428
with ui.card().classes("w-full"):
375-
self._ngram_select = ui.select(
376-
options=[],
377-
with_input=True,
378-
clearable=True,
379-
label="Search N-gram",
380-
on_change=self._handle_filter_change,
381-
).classes("w-1/4")
429+
self._ngram_select = (
430+
ui.input(
431+
autocomplete=[],
432+
label="Search N-gram",
433+
on_change=self._handle_filter_change,
434+
)
435+
.props('clearable autocomplete="off"')
436+
.classes("w-1/4")
437+
.on("keydown.enter", self._handle_enter_press)
438+
.on("clear", self._handle_clear)
439+
)
382440

383441
# Create chart with empty options and click handler
384442
self._chart = (
@@ -460,14 +518,14 @@ async def load_and_render() -> None:
460518

461519
# Populate filter select with unique n-gram values
462520
if self._ngram_select is not None:
463-
ngram_options = (
521+
self._all_ngram_options = (
464522
self._df_stats.select(pl.col(COL_NGRAM_WORDS).unique())
465523
.sort(COL_NGRAM_WORDS)
466524
.to_series()
467525
.to_list()
468526
)
469-
self._ngram_select.options = ngram_options
470-
self._ngram_select.update()
527+
# Set all n-grams as autocomplete options
528+
self._ngram_select.set_autocomplete(self._all_ngram_options)
471529

472530
# Build ECharts option and update chart
473531
option = plot_scatter_echart(self._df_stats)

0 commit comments

Comments
 (0)