@@ -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